Создайте словарь с LINQ и не добавьте "элемент с тем же ключом уже добавлен"

Я хочу найти ключ в словаре и заменить значение, если оно найдено, или добавить ключ/значение, если это не так.

код:

public class MyObject
{

    public string UniqueKey { get; set; }
    public string Field1 { get; set; }
    public string Field2 { get; set; }

}

LINQ Solution (бросает An item with the same key has already been added.):

Dictionary<string, MyObject> objectDict = csvEntries.ToDictionary(csvEntry => csvEntry.ToMyObject().UniqueKey, csvEntry => csvEntry.ToMyObject());

Решение ForEach (работает):

Dictionary<string, MyObject> objectDict = new Dictionary<string, MyObject>();
foreach (CSVEntry csvEntry in csvEntries)
{

    MyObject obj = csvEntry.ToMyObject();

    if (objectDict.ContainsKey(obj.UniqueKey))
    {
        objectDict[obj.UniqueKey] = obj;
    }
    else {
        objectDict.Add(obj.UniqueKey, obj);
    }

}

Мне действительно понравилось решение LINQ, но, поскольку оно стоит, оно выдает вышеупомянутую ошибку. Есть ли хороший способ избежать ошибки и использовать LINQ?

Ответ 1

Вы можете использовать GroupBy для создания уникальных ключей:

Dictionary<string, MyObject> objectDict = csvEntries
    .Select(csvEntry => csvEntry.ToMyObject())
    .GroupBy(x => x.UniqueKey)
    .ToDictionary(grp => grp.Key, grp => grp.First());

Однако вместо grp.First() вы можете создать коллекцию с ToList или ToArray. Таким образом, вы не берете суровый объект в случае дублирования ключей.

Другой вариант - использовать Lookup<TKey, TValue>, который позволяет дублировать ключи и даже несуществующие ключи, в этом случае вы получите пустую последовательность.

var uniqueKeyLookup = csvEntries
    .Select(csvEntry => csvEntry.ToMyObject())
    .ToLookup(x => x.UniqueKey);
IEnumerable<MyObject> objectsFor1234 = uniqueKeyLookup["1234"]; // empty if it doesn't exist

Ответ 2

Опираясь на ответ Rango, вот метод расширения, который вы можете использовать, чтобы вам не нужно было дублировать реализацию во всем проекте:

public static class DictionaryExtensions
{
    public static Dictionary<TKey, TValue> ToDictionaryWithDupSelector<TKey, TValue>(
        this IEnumerable<TValue> enumerable,
        Func<TValue, TKey> groupBy, Func<IEnumerable<TValue>, TValue> selector = null) {

        if (selector == null)
            selector = new Func<IEnumerable<TValue>, TValue>(grp => grp.First());

        return enumerable
            .GroupBy(e => groupBy(e))
            .ToDictionary(grp => grp.Key, grp => selector(grp));
    }
}

По умолчанию он выберет первый элемент при наличии дубликатов, но я предоставил необязательный параметр, в котором вы можете указать альтернативный селектор. Пример вызова метода расширения:

var objList = new List<string[]> {
    new string[2] {"1", "first"},
    new string[2] {"1", "last"},
    new string[2] {"2", "you"},
};
var asDict = objList.ToDictionary(
    arr => arr[0],
    grp => grp.Last()
);

Ответ 3

Я хочу найти ключ в словаре и заменить значение, если оно найдено, или добавить ключ/значение, если это не так

В принципе, вы хотите использовать ToDictionary где вы берете последнее вхождение объекта с тем же ключом. Предполагая, что csvEntiries равен IEnumerable<MyObject>, можно сделать следующее:

Dictionary<string, MyObject> objectDict = csvEntries
    .Reverse()
    .Distinct(new MyObjectComparer())
    .ToDictionary(x => x.UniqueKey);

где MyObjectComparer

public class MyObjectComparer : IEqualityComparer<MyObject>
{
    public bool Equals(MyObject x, MyObject y) 
        => x.UniqueKey == y.UniqueKey;

    public int GetHashCode(MyObject obj)
        => obj.UniqueKey.GetHashCode();
}