2010-07-23 21 views
105

Tengo una lista de objetos Person. Quiero convertir a un diccionario donde la clave es el nombre y el apellido (concatenado) y el valor es el objeto Persona.Convertir la lista al diccionario utilizando linq y no preocuparse por los duplicados

La cuestión es que tengo algunas personas duplicadas, por lo que este explota si utilizo este código:

private Dictionary<string, Person> _people = new Dictionary<string, Person>(); 

_people = personList.ToDictionary(
    e => e.FirstandLastName, 
    StringComparer.OrdinalIgnoreCase); 

Sé que suena extraño, pero en realidad no me importa nombres duplicados por ahora. Si hay varios nombres, solo quiero tomar uno. ¿Hay alguna forma de que pueda escribir este código arriba, así que solo toma uno de los nombres y no explota en los duplicados?

+1

Los duplicados (en base a la clave), que no estoy seguro si quieres mantenerlos o perderlos?Mantenerlos requeriría un 'Diccionario >' (o equivalente). –

+0

@Anthony Pegram: solo quiero conservar uno de ellos. he actualizado la pregunta para ser más explícito – leora

Respuesta

41

Aquí está la solución obvia, no LINQ:

foreach(var person in personList) 
{ 
    if(!myDictionary.Keys.Contains(person.FirstAndLastName)) 
    myDictionary.Add(person.FirstAndLastName, person); 
} 
+143

eso es tan 2007 :) – leora

+2

que no ignora el caso – onof

+0

Sí, a la hora de actualizar desde el marco de trabajo de .NET 2.0 en el trabajo ... no es exactamente difícil ignorar el caso. Solo agrega todas las teclas en mayúsculas. – Carra

305

solución LINQ:

// Use the first value in group 
var _people = personList 
    .GroupBy(p => p.FirstandLastName, StringComparer.OrdinalIgnoreCase) 
    .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase); 

// Use the last value in group 
var _people = personList 
    .GroupBy(p => p.FirstandLastName, StringComparer.OrdinalIgnoreCase) 
    .ToDictionary(g => g.Key, g => g.Last(), StringComparer.OrdinalIgnoreCase); 

Si prefiere una solución no LINQ entonces se podría hacer algo como esto:

// Use the first value in list 
var _people = new Dictionary<string, Person>(StringComparer.OrdinalIgnoreCase); 
foreach (var p in personList) 
{ 
    if (!_people.ContainsKey(p.FirstandLastName)) 
     _people[p.FirstandLastName] = p; 
} 

// Use the last value in list 
var _people = new Dictionary<string, Person>(StringComparer.OrdinalIgnoreCase); 
foreach (var p in personList) 
{ 
    _people[p.FirstandLastName] = p; 
} 
+1

+1 muy elegante (votaré lo antes posible - no tengo más votos para hoy :)) – onof

+5

@LukeH Nota secundaria: sus dos fragmentos no son equivalentes: la variante LINQ conserva el primer elemento, el fragmento no de LINQ conserva el último elemento? – toong

+4

@toong: Es cierto y definitivamente vale la pena señalarlo. (Aunque en este caso al OP no parece importarle con qué elemento terminan) – LukeH

7

Para manejar la eliminación de duplicados, implemente un IEqualityComparer<Person> que se puede usar en el Distinct() método, y luego obtener su diccionario será fácil. Dada :

class PersonComparer : IEqualityComparer<Person> 
{ 
    public bool Equals(Person x, Person y) 
    { 
     return x.FirstAndLastName.Equals(y.FirstAndLastName, StringComparison.OrdinalIgnoreCase); 
    } 

    public int GetHashCode(Person obj) 
    { 
     return obj.FirstAndLastName.ToUpper().GetHashCode(); 
    } 
} 

class Person 
{ 
    public string FirstAndLastName { get; set; } 
} 

Obtenga su diccionario:

List<Person> people = new List<Person>() 
{ 
    new Person() { FirstAndLastName = "Bob Sanders" }, 
    new Person() { FirstAndLastName = "Bob Sanders" }, 
    new Person() { FirstAndLastName = "Jane Thomas" } 
}; 

Dictionary<string, Person> dictionary = 
    people.Distinct(new PersonComparer()).ToDictionary(p => p.FirstAndLastName, p => p); 
34

Un LINQ solución utilizando distintas() y ni agrupación es:

var _people = personList 
    .Select(item => new { Key = item.Key, FirstAndLastName = item.FirstAndLastName }) 
    .Distinct() 
    .ToDictionary(item => item.Key, item => item.FirstFirstAndLastName, StringComparer.OrdinalIgnoreCase); 

No sé si se trata de mejor que la solución de LukeH, pero también funciona.

+0

¿Estás seguro de que funciona? ¿Cómo va a comparar Distinct el nuevo tipo de referencia que crea? Creo que tendrías que pasar algún tipo de IEqualityComparer a Distinct para que funcione como está previsto. –

+5

Ignora mi comentario anterior. Consulte http://stackoverflow.com/questions/543482/linq-select-distinct-with-anonymous-types –

+0

Si desea anular qué tan distinto está determinado, consulte http://stackoverflow.com/questions/489258/linqs- distinct-on-a-particular-property –

20

Esto debería funcionar con la expresión lambda:

personList.Distinct().ToDictionary(i => i.FirstandLastName, i => i); 
+2

Debe ser: 'personList.Distinct(). ToDictionary (i => i.FirstandLastName, i => i);' – Gh61

+0

Esto solo funcionará si el IEqualityComparer predeterminado para la persona la clase se compara por nombre y apellido, ignorando el caso. De lo contrario, escriba dicho IEqualityComparer y use la sobrecarga Distinct relevante. Además, su método ToDionary debe tomar un comparador que no distinga entre mayúsculas y minúsculas para que coincida con los requisitos del OP. – Joe

5

Usted puede crear un método de extensión similar a ToDictionary() con la diferencia de que permite duplicados. Algo así como:

public static Dictionary<TKey, TElement> SafeToDictionary<TSource, TKey, TElement>(
     this IEnumerable<TSource> source, 
     Func<TSource, TKey> keySelector, 
     Func<TSource, TElement> elementSelector, 
     IEqualityComparer<TKey> comparer = null) 
    { 
     var dictionary = new Dictionary<TKey, TElement>(comparer); 

     if (source == null) 
     { 
      return dictionary; 
     } 

     foreach (TSource element in source) 
     { 
      dictionary[keySelector(element)] = elementSelector(element); 
     } 

     return dictionary; 
    } 

En este caso, si hay duplicados, entonces las victorias del último valor.

7

También puede usar la función LINQ ToLookup, que luego puede usar casi de forma intercambiable con un diccionario.

_people = personList 
    .ToLookup(e => e.FirstandLastName, StringComparer.OrdinalIgnoreCase); 
_people.ToDictionary(kl => kl.Key, kl => kl.First()); // Potentially unnecessary 

En esencia, esto hará el GroupBy en LukeH's answer, pero dará el hash que proporciona un diccionario. Por lo tanto, probablemente no necesite convertirlo a un diccionario, solo utilice la función LINQ First cada vez que necesite acceder al valor de la clave.

0

A partir de la solución de Carra también se puede escribir como:

foreach(var person in personList.Where(el => !myDictionary.ContainsKey(el.FirstAndLastName))) 
{ 
    myDictionary.Add(person.FirstAndLastName, person); 
} 
+0

No es que alguien trate de usar esto, pero no intente usarlo. Modificar las colecciones a medida que las itera es una mala idea. – kidmosey

2
 DataTable DT = new DataTable(); 
     DT.Columns.Add("first", typeof(string)); 
     DT.Columns.Add("second", typeof(string)); 

     DT.Rows.Add("ss", "test1"); 
     DT.Rows.Add("sss", "test2"); 
     DT.Rows.Add("sys", "test3"); 
     DT.Rows.Add("ss", "test4"); 
     DT.Rows.Add("ss", "test5"); 
     DT.Rows.Add("sts", "test6"); 

     var dr = DT.AsEnumerable().GroupBy(S => S.Field<string>("first")).Select(S => S.First()). 
      Select(S => new KeyValuePair<string, string>(S.Field<string>("first"), S.Field<string>("second"))). 
      ToDictionary(S => S.Key, T => T.Value); 

     foreach (var item in dr) 
     { 
      Console.WriteLine(item.Key + "-" + item.Value); 
     } 
+0

Le sugiero que mejore su ejemplo leyendo el [Ejemplo mínimo, completo y verificable] (http://stackoverflow.com/help/mcve). – IlGala

Cuestiones relacionadas