Фильтрация дубликатов из IEnumerable

У меня есть этот код:

class MyObj {
    int Id;
    string Name;
    string Location;
}

IEnumerable<MyObj> list;

Я хочу преобразовать список в словарь следующим образом:

list.ToDictionary(x => x.Name);

но это говорит мне, что у меня есть дубликаты ключей. Как я могу сохранить только первый элемент для каждой клавиши?

Ответы

Ответ 1

Я предполагаю, что самым простым способом было бы группировать по ключу и принимать первый элемент каждой группы:

list.GroupBy(x => x.name).Select(g => g.First()).ToDictionary(x => x.name);

Или вы можете использовать Distinct, если ваши объекты реализуют IEquatable для сравнения между собой по ключу:

// I'll just randomly call your object Person for this example.
class Person : IEquatable<Person> 
{
    public string Name { get; set; }

    public bool Equals(Person other)
    {
        if (other == null)
            return false;

        return Name == other.Name;
    }

    public override bool Equals(object obj)
    {
        return base.Equals(obj as Person);
    }

    public override int GetHashCode()
    {
        return Name.GetHashCode();
    }
}

...

list.Distinct().ToDictionary(x => x.Name);

Или, если вы не хотите этого делать (возможно, потому, что вы обычно хотите сравнивать для равенства по-другому, поэтому Equals уже используется) вы можете сделать собственную реализацию IEqualityComparer только для этого случай:

class PersonComparer : IEqualityComparer<Person>
{
    public bool Equals(Person x, Person y)
    {
        if (x == null)
            return y == null;

        if (y == null)
            return false;

        return x.Name == y.Name;
    }

    public int GetHashCode(Person obj)
    {
        return obj.Name.GetHashCode();
    }
}

...

list.Distinct(new PersonComparer()).ToDictionary(x => x.Name);

Ответ 2

Вы также можете создать свой собственный метод перераспределения расширенных расширений, который принял Func < > для выбора отдельного ключа:

public static class EnumerationExtensions
{
    public static IEnumerable<TSource> Distinct<TSource,TKey>(
        this IEnumerable<TSource> source, Func<TSource,TKey> keySelector)
    {
        KeyComparer comparer = new KeyComparer(keySelector);

        return source.Distinct(comparer);
    }

    private class KeyComparer<TSource,TKey> : IEqualityComparer<TSource>
    {
        private Func<TSource,TKey> keySelector;

        public DelegatedComparer(Func<TSource,TKey> keySelector)
        {
            this.keySelector = keySelector;
        }

        bool IEqualityComparer.Equals(TSource a, TSource b)
        {
            if (a == null && b == null) return true;
            if (a == null || b == null) return false;

            return keySelector(a) == keySelector(b);
        }

        int IEqualityComparer.GetHashCode(TSource obj)
        {
            return keySelector(obj).GetHashCode();
        }
    }
}

Извинения за неправильное форматирование кода, я хотел уменьшить размер кода на странице. В любом случае, вы можете использовать ToDictionary:

 var dictionary = list.Distinct(x => x.Name).ToDictionary(x => x.Name);

Ответ 3

list.Distinct().ToDictionary(x => x.Name);

Ответ 4

Может ли сделать свой собственный, возможно? Например:

public static class Extensions
{
    public static IDictionary<TKey, TValue> ToDictionary2<TKey, TValue>(
        this IEnumerable<TValue> subjects, Func<TValue, TKey> keySelector)
    {
        var dictionary = new Dictionary<TKey, TValue>();
        foreach(var subject in subjects)
        {
            var key = keySelector(subject);
            if(!dictionary.ContainsKey(key))
                dictionary.Add(key, subject);
        }
        return dictionary;
    }
}

var dictionary = list.ToDictionary2(x => x.Name);

Не проверял, но должен работать. (и он должен, вероятно, иметь лучшее имя, чем ToDictionary2: p)

В качестве альтернативы вы можете реализовать метод DistinctBy, например, например:

public static IEnumerable<TSubject> DistinctBy<TSubject, TValue>(this IEnumerable<TSubject> subjects, Func<TSubject, TValue> valueSelector)
{
    var set = new HashSet<TValue>();
    foreach(var subject in subjects)
        if(set.Add(valueSelector(subject)))
            yield return subject;
}

var dictionary = list.DistinctBy(x => x.Name).ToDictionary(x => x.Name);

Ответ 5

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

public static Dictionary<TKey,TValue> ToDictionaryAllowDuplicateKeys<TKey,TValue>(
  this IEnumerable<TValue> values,
  Func<TValue,TKey> keyFunc) {
  var map = new Dictionary<TKey,TValue>();
  foreach ( var cur in values ) {
    var key = keyFunc(cur);
    map[key] = cur;
  }
  return map;
}

Теперь преобразование в словарь прямолинейно

var map = list.ToDictionaryAllowDuplicateKeys(x => x.Name);

Ответ 6

Следующие действия будут работать, если у вас есть разные экземпляры MyObj с тем же значением для свойства Name. Сначала будет найден первый экземпляр для каждого дубликата (извините за ноту obj - obj2, это всего лишь образец кода):

list.SelectMany(obj => new MyObj[] {list.Where(obj2 => obj2.Name == obj.Name).First()}).Distinct();

EDIT: решение Joren лучше, так как оно не создает ненужных массивов в процессе.