Перетворити список у словник за допомогою linq, не переживаючи за дублікати


163

У мене є список об'єктів Person. Я хочу перетворити у словник, де ключ - це ім’я та прізвище (об'єднане), а значення - об'єкт Person.

Проблема полягає в тому, що у мене є кілька дублюваних людей, тому це може вибухнути, якщо я використовую цей код:

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

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

Я знаю, що це звучить дивно, але зараз я не дуже переймаюся дублікатами імен. Якщо є кілька імен, я просто хочу вхопити одне. Чи все-таки я можу написати цей код вище, щоб він просто взяв одне з імен і не підірвався на дублікатах?


1
Дублікати (на основі ключа), я не впевнений, хочете ви зберегти їх чи втратити? Збереження їх вимагає Dictionary<string, List<Person>>(або еквівалента).
Ентоні Пеграм

@Anthony Pegram - просто хочеться зберегти одну з них. я оновив питання, щоб бути більш явним
leora

добре ви можете використовувати різні, перш ніж робити ToDictionary. але вам доведеться перекрити методи Equals () та GetHashCode () для класу person, щоб CLR знав, як порівнювати об'єкти людини
Sujit.Warrier

@ Sujit.Warrier - Ви також можете створити порівняння рівності, щоб перейти доDistinct
Кайл Делані,

Відповіді:


71

Ось очевидне, не пов'язане з рішенням:

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

208
ось так 2007 :)
leora

3
що не ігнорує випадок
onof

Так, про час, коли ми оновлюємось із .net 2.0 на роботі ... @onof Не зовсім важко ігнорувати випадок. Просто додайте всі великі регістри.
Carra

як би я зробив цю справу нечутливою
leora

11
Або створіть словник за допомогою StringComparer, який ігнорує випадок, якщо це те, що вам потрібно, то ваш додавання / перевірку код не має значення, якщо ви ігноруєте регістр чи ні.
Бінарний занепокоєння

423

Рішення 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);

Якщо ви віддаєте перевагу не-LINQ-рішенню, тоді ви можете зробити щось подібне:

// 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 Незначна примітка: ваші два фрагменти не еквівалентні: варіант LINQ зберігає перший елемент, а фрагмент не LINQ зберігає останній елемент?
вдень

4
@toong: Це правда і, безумовно, варто зазначити. (Хоча в цьому випадку ОП, здається, не байдуже, з яким елементом вони закінчуються.)
Лук

1
У випадку "першого значення": рішення nonLinq здійснює пошук словника двічі, але Linq робить зайві екземпляри об'єкта та ітерацію. Обидва не ідеальні.
Серг

@SerG На щастя пошук словника, як правило, вважається операцією O (1) і має незначний вплив.
MHollis

43

Рішення Linq, що використовує Distinct () і без групування:

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

Я не знаю, чи він кращий за рішення LukeH, але він також працює.


Ви впевнені, що це працює? Як Distinct збирається порівняти новий створений вами тип посилання? Я думаю, вам потрібно передати якусь IEqualityComparer в Distinct, щоб отримати цю роботу за призначенням.
Simon Gillbee

5
Ігноруйте мій попередній коментар. Див stackoverflow.com/questions/543482 / ...
Simon Gillbee

Якщо ви хочете переосмислити, наскільки визначається чіткість, перегляньте stackoverflow.com/questions/489258/…
Джеймс Макмахон

30

Це повинно працювати з лямбда-виразом:

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

2
Це повинно бути:personList.Distinct().ToDictionary(i => i.FirstandLastName, i => i);
Gh61

4
Це буде працювати лише в тому випадку, якщо за замовчуванням IEqualityComparer для класу Person порівнюється за іменем та прізвищем, ігноруючи регістр. В іншому випадку напишіть такий IEqualityComparer і використовуйте відповідне перевантажене розрізнення. Крім того, ваш метод ToDIctionary повинен прийняти порівняно з обставинами, що не стосуються справ, щоб відповідати вимогам ОП.
Джо

13

Ви також можете використовувати функцію ToLookupLINQ, яку потім ви можете майже взаємозамінно використовувати зі словником.

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

Це по суті буде робити GroupBy у відповіді Лукаха , але дасть хеширование , яке надає Словник. Отже, вам, мабуть, не потрібно перетворювати його у словник, а просто використовувати функцію LINQ, Firstколи вам потрібно отримати доступ до значення для ключа.


8

Ви можете створити метод розширення, схожий на ToDictionary (), з тією різницею, що він дозволяє дублювати. Щось на зразок:

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

У цьому випадку, якщо є дублікати, виграє останнє значення.


7

Для обробки усунення дублікатів реалізуйте додаток, IEqualityComparer<Person>який можна використовувати в Distinct()методі, і тоді отримати ваш словник буде просто. Подано:

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

Отримайте свій словник:

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

Я пропоную вам вдосконалити свій приклад, прочитавши приклад " Мінімальний", "Повний" і "перевірений" .
IlGala

2

У випадку, якщо ми хочемо, щоб вся особа (а не лише одна особа) у словнику, що повертається, ми могли б:

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

1
На жаль, ігноруйте мою редагування огляду (я не можу знайти, де видалити рецензію-редагування). Я просто хотів додати пропозицію щодо використання g.First () замість g.Select (x => x).
Алекс 75

1

Проблема з більшістю інших відповідей є те , що вони використовують Distinct, GroupByабо ToLookup, що створює додатковий словник під капотом. Так само ToUpper створює додатковий рядок. Це я зробив, і це майже точна копія коду Microsoft, за винятком однієї зміни:

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

Оскільки набір індексатора змушує його додавати ключ, він не буде кидатись, а також зробить лише один пошук ключів. Ви також можете надати його IEqualityComparer, наприкладStringComparer.OrdinalIgnoreCase


0

Починаючи з рішення Карри, ви також можете написати це як:

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

3
Не те, щоб хтось ніколи не намагався цим скористатись, але не намагайся використати це. Змінювати колекції під час їх повторення - це погана ідея.
kidmosey
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.