Convierta la lista a diccionario usando linq y sin preocuparse por los duplicados

163

Tengo una lista de objetos Persona. Quiero convertir a un diccionario donde la clave es el nombre y el apellido (concatenados) y el valor es el objeto Persona.

El problema es que tengo algunas personas duplicadas, por lo que esto explota si uso 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 por ahora no me importan los nombres duplicados. Si hay varios nombres, solo quiero tomar uno. ¿Hay alguna forma de escribir este código arriba para que solo tome uno de los nombres y no explote en duplicados?

leora
fuente
1
Los duplicados (basados ​​en la clave), ¿no estoy seguro de querer conservarlos o perderlos? Mantenerlos requeriría un Dictionary<string, List<Person>>(o equivalente).
Anthony Pegram
@Anthony Pegram: solo quiero conservar uno de ellos. He actualizado la cuestión de ser más explícita
Leora
bueno, puedes usar distinto antes de hacer ToDictionary. pero tendría que anular los métodos Equals () y GetHashCode () para la clase de persona para que CLR sepa cómo comparar objetos de persona
Sujit.Warrier
@ Sujit.Warrier - También podrías crear un comparador de igualdad para pasarDistinct
Kyle Delaney

Respuestas:

71

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

foreach(var person in personList)
{
  if(!myDictionary.Keys.Contains(person.FirstAndLastName))
    myDictionary.Add(person.FirstAndLastName, person);
}
Carra
fuente
207
eso es así 2007 :)
leora
3
eso no ignora el caso
2010
Sí, ya es hora de actualizar desde el marco .net 2.0 en el trabajo ... @onof No es exactamente difícil ignorar el caso. Simplemente agregue todas las claves en mayúsculas.
Carra
¿Cómo puedo hacer que este caso insensible
Leora
11
O cree el diccionario con un StringComparer que ignorará el caso, si eso es lo que necesita, entonces a su código de agregar / verificar no le importa si está ignorando el caso o no.
Binario Worrier
423

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 que no sea LINQ, puede 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;
}
LukeH
fuente
2
+1 muy elegante (votaré lo antes posible - no tengo más votos para hoy :))
2010
66
@LukeH Nota menor: ¿sus dos fragmentos no son equivalentes: la variante LINQ retiene el primer elemento, el fragmento no LINQ retiene el último elemento?
toong
44
@toong: Eso es cierto y definitivamente vale la pena señalarlo. (Aunque en este caso al OP no parece importarle con qué elemento terminan.)
LukeH
1
Para el caso del "primer valor": la solución nonLinq realiza búsquedas de diccionario dos veces, pero Linq realiza instancias de objetos redundantes e iteraciones. Ambos no son ideales.
SerG
@SerG Afortunadamente, la búsqueda de diccionario generalmente se considera una operación O (1) y tiene un impacto insignificante.
MHollis
42

Una solución de Linq usando Distinct () y sin 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 es mejor que la solución de LukeH, pero también funciona.

Tillito
fuente
¿Estás seguro de que funciona? ¿Cómo va a comparar Distinct el nuevo tipo de referencia que crea? Creo que necesitaría pasar algún tipo de IEqualityComparer a Distinct para obtener este trabajo según lo previsto.
Simon Gillbee
55
Ignora mi comentario anterior. Ver stackoverflow.com/questions/543482/…
Simon Gillbee
Si desea anular qué tan distinta se determina, consulte stackoverflow.com/questions/489258/…
James McMahon
30

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

personList.Distinct().ToDictionary(i => i.FirstandLastName, i => i);
Ankit Dass
fuente
2
Debe ser:personList.Distinct().ToDictionary(i => i.FirstandLastName, i => i);
Gh61
44
Esto solo funcionará si el IEqualityComparer predeterminado para la clase Person se compara por nombre y apellido, ignorando mayúsculas y minúsculas. De lo contrario, escriba un IEqualityComparer y use la sobrecarga Distinct correspondiente. Además, su método ToDictionary debe tomar un comparador de mayúsculas y minúsculas para que coincida con los requisitos del OP.
Joe
13

También puede usar la ToLookupfunción LINQ, que luego puede usar casi indistintamente con un Diccionario.

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

Esto esencialmente hará el GroupBy en la respuesta de LukeH , pero dará el hash que proporciona un Diccionario. Por lo tanto, es probable que no necesite convertirlo a un diccionario, sino que simplemente use la Firstfunción LINQ cuando necesite acceder al valor de la clave.

palswim
fuente
8

Puede crear un método de extensión similar a ToDictionary () con la diferencia de que permite duplicados. Algo 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, gana el último valor.

Eric
fuente
7

Para manejar la eliminación de duplicados, implemente uno IEqualityComparer<Person>que pueda usarse en el Distinct()método, y luego obtener su diccionario será fácil. Dado:

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; }
}

Consigue tu 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);
Anthony Pegram
fuente
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);
        }
Rey
fuente
Le sugiero que mejore su ejemplo leyendo el ejemplo Mínimo, Completo y verificable .
IlGala
2

En caso de que queramos toda la Persona (en lugar de una sola Persona) en el diccionario que regresa, podríamos:

var _people = personList
.GroupBy(p => p.FirstandLastName)
.ToDictionary(g => g.Key, g => g.Select(x=>x));
Shane Lu
fuente
1
Lo sentimos, ignore mi revisión-edición (No puedo encontrar dónde eliminar mi revisión-edición). Solo quería agregar una sugerencia sobre el uso de g.First () en lugar de g.Select (x => x).
Alex 75
1

El problema con la mayoría de las otras respuestas es que usan Distinct, GroupByo ToLookup, lo que crea un diccionario adicional debajo del capó. Igualmente ToUpper crea una cadena extra. Esto es lo que hice, que es casi una copia exacta del código de Microsoft, excepto por un cambio:

    public static Dictionary<TKey, TSource> ToDictionaryIgnoreDup<TSource, TKey>
        (this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer = null) =>
        source.ToDictionaryIgnoreDup(keySelector, i => i, comparer);

    public static Dictionary<TKey, TElement> ToDictionaryIgnoreDup<TSource, TKey, TElement>
        (this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector, IEqualityComparer<TKey> comparer = null)
    {
        if (keySelector == null)
            throw new ArgumentNullException(nameof(keySelector));
        if (elementSelector == null)
            throw new ArgumentNullException(nameof(elementSelector));
        var d = new Dictionary<TKey, TElement>(comparer ?? EqualityComparer<TKey>.Default);
        foreach (var element in source)
            d[keySelector(element)] = elementSelector(element);
        return d;
    }

Debido a que un conjunto en el indexador hace que agregue la clave, no arrojará, y también realizará solo una búsqueda de clave. También puedes darle un IEqualityComparer, por ejemploStringComparer.OrdinalIgnoreCase

Charlie
fuente
0

A partir de la solución de Carra, también puede escribirlo como:

foreach(var person in personList.Where(el => !myDictionary.ContainsKey(el.FirstAndLastName)))
{
    myDictionary.Add(person.FirstAndLastName, person);
}
Cinquo
fuente
3
No es que alguien intente usar esto, pero no intentes usarlo. Modificar colecciones a medida que las itera es una mala idea.
kidmosey