Converti l'elenco in dizionario usando linq e non preoccuparti dei duplicati


163

Ho un elenco di oggetti Person. Voglio convertire in un dizionario in cui la chiave è il nome e il cognome (concatenati) e il valore è l'oggetto Person.

Il problema è che ho alcune persone duplicate, quindi questo esplode se uso questo codice:

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

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

So che suona strano ma per il momento non mi interessa davvero i nomi dei duplicati. Se ci sono più nomi, voglio solo prenderne uno. Posso comunque scrivere questo codice sopra in modo da prendere solo uno dei nomi e non esplodere in duplicati?


1
I duplicati (in base alla chiave), non sono sicuro se si desidera mantenerli o perderli? Mantenerli richiederebbe un Dictionary<string, List<Person>>(o equivalente).
Anthony Pegram,

@Anthony Pegram - voglio solo tenerne uno. ho aggiornato la domanda per essere più espliciti
leora,

bene puoi usare distinti prima di fare ToDictionary. ma dovresti sovrascrivere i metodi Equals () e GetHashCode () per la classe persona in modo che CLR sappia come confrontare gli oggetti persona
Sujit.Warrier

@ Sujit.Warrier - Potresti anche creare un comparatore di uguaglianza per passare aDistinct
Kyle Delaney,

Risposte:


71

Ecco la soluzione ovvia, non linq:

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

208
è così 2007 :)
Leora,

3
che non ignora caso
onof

Sì, è ora di aggiornare dal framework .net 2.0 al lavoro ... @onof Non è esattamente difficile ignorare il caso. Aggiungi tutte le chiavi in ​​maiuscolo.
Carra,

come
renderei

11
Oppure crea il dizionario con StringComparer che ignorerà il caso, se è quello che ti serve, il tuo codice di aggiunta / controllo non ti interessa se stai ignorando il caso o meno.
Binary Worrier,

423

Soluzione 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);

Se preferisci una soluzione non LINQ, potresti fare qualcosa del genere:

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

6
@LukeH Nota minore: i tuoi due frammenti non sono equivalenti: la variante LINQ conserva il primo elemento, lo snippet non LINQ conserva l'ultimo elemento?
Toong

4
@toong: è vero e sicuramente vale la pena notare. (Anche se in questo caso all'OP non sembra importare quale elemento finisca.)
Luca

1
Nel caso del "primo valore": la soluzione nonLinq esegue due volte la ricerca nel dizionario ma Linq esegue istanze e iterazioni ridondanti degli oggetti. Entrambi non sono l'ideale.
SerG

@SerG Per fortuna la ricerca nel dizionario è generalmente considerata un'operazione O (1) e ha un impatto trascurabile.
MHollis,

43

Una soluzione Linq che utilizza Distinct () e nessun raggruppamento è:

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

Non so se sia più bello della soluzione di LukeH ma funziona anche.


Sei sicuro che funzioni? In che modo Distinct confronterà il nuovo tipo di riferimento che crei? Penserei che dovresti passare una specie di IEqualityComparer in Distinct per ottenere questo lavoro come previsto.
Simon Gillbee,

5
Ignora il mio commento precedente. Vedere stackoverflow.com/questions/543482/...
Simon Gillbee

Se vuoi sovrascrivere quanto sia determinato il distacco,
James McMahon,

30

Questo dovrebbe funzionare con l'espressione lambda:

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

2
Deve essere:personList.Distinct().ToDictionary(i => i.FirstandLastName, i => i);
Gh61

4
Funzionerà solo se IEqualityComparer predefinito per la classe Person confronta per nome e cognome, ignorando il caso. Altrimenti scrivi un tale IEqualityComparer e usa il relativo sovraccarico Distinct. Inoltre, il metodo ToDIctionary dovrebbe prendere un comparatore senza distinzione tra maiuscole e minuscole per soddisfare i requisiti del PO.
Joe,

13

Puoi anche usare la ToLookupfunzione LINQ, che puoi quindi usare quasi in modo intercambiabile con un Dizionario.

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

Questo essenzialmente farà il GroupBy nella risposta di LukeH , ma fornirà l'hash che fornisce un Dizionario. Pertanto, probabilmente non è necessario convertirlo in un dizionario, ma è sufficiente utilizzare la Firstfunzione LINQ ogni volta che è necessario accedere al valore per la chiave.


8

È possibile creare un metodo di estensione simile a ToDictionary () con la differenza che consente duplicati. Qualcosa di simile a:

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

In questo caso, se ci sono duplicati, vince l'ultimo valore.


7

Per gestire l'eliminazione dei duplicati, implementa un metodo IEqualityComparer<Person>che può essere utilizzato nel Distinct()metodo e quindi ottenere il tuo dizionario sarà facile. Dato:

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

Ottieni il tuo dizionario:

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

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

Ti suggerisco di migliorare il tuo esempio leggendo l'esempio Minimo, Completo e verificabile .
IlGala,

2

Nel caso in cui desideriamo tutta la Persona (anziché una sola Persona) nel dizionario di ritorno, potremmo:

var _people = personList
.GroupBy(p => p.FirstandLastName)
.ToDictionary(g => g.Key, g => g.Select(x=>x));

1
Spiacente, ignora la mia revisione-modifica (non riesco a trovare dove eliminare la mia revisione-modifica). Volevo solo aggiungere un suggerimento sull'uso di g.First () invece di g.Select (x => x).
Alex 75

1

Il problema con la maggior parte delle altre risposte è che usano Distinct, GroupByo ToLookup, che crea un extra dizionario sotto il cofano. Allo stesso modo ToUpper crea una stringa aggiuntiva. Questo è quello che ho fatto, che è una copia quasi esatta del codice di Microsoft ad eccezione di una modifica:

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

Poiché un set sull'indicizzatore fa sì che aggiunga la chiave, non verrà lanciato e eseguirà anche una sola ricerca della chiave. Puoi anche dargli un IEqualityComparer, per esempioStringComparer.OrdinalIgnoreCase


0

A partire dalla soluzione di Carra puoi anche scriverlo come:

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

3
Non che nessuno proverebbe mai a usarlo, ma non tenterà di usarlo. Modificare le raccolte mentre le stai ripetendo è una cattiva idea.
Kidmosey,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.