Перетворити список у словник за допомогою 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.