Konwertuj listę na słownik używając linq i nie martw się o duplikaty

163

Mam listę obiektów Person. Chcę przekonwertować na słownik, w którym kluczem jest imię i nazwisko (połączone), a wartością jest obiekt Person.

Problem polega na tym, że mam kilka zduplikowanych osób, więc jeśli użyję tego kodu, to wybucha:

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

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

Wiem, że to brzmi dziwnie, ale na razie nie obchodzą mnie nazwy duplikatów. Jeśli jest wiele nazw, chcę tylko jedną. Czy w ogóle mogę napisać ten kod powyżej, aby pobierał tylko jedną z nazw i nie wysadzał się na duplikatach?

leora
źródło
1
Duplikaty (na podstawie klucza), nie jestem pewien, czy chcesz je zachować, czy zgubić? Utrzymanie ich wymagałoby Dictionary<string, List<Person>>(lub odpowiednika).
Anthony Pegram
@Anthony Pegram - po prostu chcę zatrzymać jeden z nich. zaktualizowałem pytanie, aby było bardziej wyraźne
leora
Cóż, możesz użyć różnicy przed wykonaniem ToDictionary. ale musiałbyś przesłonić metody Equals () i GetHashCode () dla klasy osoby, aby CLR wiedział, jak porównywać obiekty osób
Sujit.Warrier
@ Sujit.Warrier - Możesz także utworzyć porównywanie równości do przekazaniaDistinct
Kyle Delaney

Odpowiedzi:

71

Oto oczywiste rozwiązanie niezwiązane z linq:

foreach(var person in personList)
{
  if(!myDictionary.Keys.Contains(person.FirstAndLastName))
    myDictionary.Add(person.FirstAndLastName, person);
}
Carra
źródło
208
to tak 2007 :)
leora
3
że nie ignoruje sprawę
onof
Tak, najwyższy czas, aby zaktualizować środowisko .net 2.0 w pracy ... @onof Niezupełnie trudno zignorować wielkość liter. Po prostu dodaj wszystkie klucze wielkimi literami.
Carra
jak uczynić tę sprawę niewrażliwą
leora
11
Lub utwórz słownik z StringComparer, który zignoruje wielkość liter, jeśli tego potrzebujesz, wtedy twój kod dodawania / sprawdzania nie obchodzi, czy ignorujesz wielkość liter, czy nie.
Binary Worrier
423

Rozwiązanie 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);

Jeśli wolisz rozwiązanie inne niż LINQ, możesz zrobić coś takiego:

// 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
źródło
6
@LukeH Drobna uwaga: Twoje dwa fragmenty nie są równoważne: wariant LINQ zachowuje pierwszy element, fragment spoza LINQ zachowuje ostatni element?
toong
4
@toong: To prawda i zdecydowanie warte odnotowania. (Chociaż w tym przypadku OP wydaje się nie obchodzić, z jakim elementem kończą.)
LukeH
1
Dla przypadku „pierwszej wartości”: rozwiązanie nonLinq przeszukuje słownik dwukrotnie, ale Linq wykonuje redundantne instancje obiektów i wykonuje iteracje. Obie nie są idealne.
SerG
@SerG Na szczęście wyszukiwanie w słowniku jest ogólnie uważane za operację O (1) i ma znikomy wpływ.
MHollis,
43

Rozwiązanie Linq używające Distinct () i bez grupowania to:

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

Nie wiem, czy jest ładniejsze niż rozwiązanie LukeHa, ale też działa.

Tillito
źródło
Czy na pewno to działa? Jak Distinct porówna nowy typ odniesienia, który utworzysz? Myślę, że musisz przekazać jakiś rodzaj IEqualityComparer do Distinct, aby uzyskać tę pracę zgodnie z przeznaczeniem.
Simon Gillbee,
5
Zignoruj ​​mój poprzedni komentarz. Zobacz stackoverflow.com/questions/543482/…
Simon Gillbee
Jeśli chcesz zmienić stopień określania odrębności, sprawdź stackoverflow.com/questions/489258/ ...
James McMahon
30

To powinno działać z wyrażeniem lambda:

personList.Distinct().ToDictionary(i => i.FirstandLastName, i => i);
Ankit Dass
źródło
2
Musi to być:personList.Distinct().ToDictionary(i => i.FirstandLastName, i => i);
Gh61
4
Będzie to działać tylko wtedy, gdy domyślny IEqualityComparer dla klasy Person porównuje imię i nazwisko, ignorując wielkość liter. W przeciwnym razie napisz taki IEqualityComparer i użyj odpowiedniego Distinct przeciążenia. Również Twoja metoda ToDIctionary powinna uwzględniać metodę porównującą bez rozróżniania wielkości liter, aby spełnić wymagania OP.
Joe
13

Możesz również użyć ToLookupfunkcji LINQ, której możesz następnie używać prawie zamiennie ze słownikiem.

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

Zasadniczo zrobi to GroupBy w odpowiedzi LukeH , ale da hash , który zapewnia słownik. Tak więc prawdopodobnie nie musisz konwertować go na słownik, ale po prostu użyj Firstfunkcji LINQ, gdy potrzebujesz uzyskać dostęp do wartości klucza.

palswim
źródło
8

Możesz utworzyć metodę rozszerzającą podobną do ToDictionary (), z tą różnicą, że zezwala na duplikaty. Coś jak:

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

W takim przypadku, jeśli istnieją duplikaty, wygrywa ostatnia wartość.

Eric
źródło
7

Aby poradzić sobie z eliminacją duplikatów, zaimplementuj metodę, IEqualityComparer<Person>która może być używana w Distinct()metodzie, a wtedy uzyskanie słownika będzie łatwe. Dany:

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

Pobierz słownik:

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
źródło
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);
        }
Król
źródło
Proponuję poprawić swój przykład, czytając przykład minimalny, kompletny i weryfikowalny .
IlGala
2

W przypadku, gdy chcemy, aby wszystkie osoby (zamiast tylko jednej osoby) znajdowały się w zwracanym słowniku, możemy:

var _people = personList
.GroupBy(p => p.FirstandLastName)
.ToDictionary(g => g.Key, g => g.Select(x=>x));
Shane Lu
źródło
1
Przepraszamy, zignoruj ​​moją zmianę w recenzji (nie mogę znaleźć miejsca, w którym można ją usunąć). Chciałem tylko dodać sugestię dotyczącą używania g.First () zamiast g.Select (x => x).
Alex 75
1

Problem z większością innych odpowiedzi jest to, że oni używają Distinct, GroupByalbo ToLookup, co stwarza dodatkowy słownik pod maską. Equally ToUpper tworzy dodatkowy ciąg. Oto, co zrobiłem, co jest prawie dokładną kopią kodu Microsoftu, z wyjątkiem jednej zmiany:

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

Ponieważ zestaw indeksatora powoduje dodanie klucza, nie zostanie zgłoszony, a także przeprowadzi tylko jedno wyszukiwanie klucza. Możesz też nadać mu IEqualityComparernpStringComparer.OrdinalIgnoreCase

Charlie
źródło
0

Zaczynając od rozwiązania Carra możesz również napisać to jako:

foreach(var person in personList.Where(el => !myDictionary.ContainsKey(el.FirstAndLastName)))
{
    myDictionary.Add(person.FirstAndLastName, person);
}
Cinquo
źródło
3
Nie żeby ktokolwiek próbował tego użyć, ale nie próbuj tego używać. Modyfikowanie kolekcji podczas ich iteracji to zły pomysł.
kidmosey