Использование Distinct с LINQ и объектами

До недавнего времени я использовал Distinct в LINQ для выбора отдельной категории (перечисления) из таблицы. Это прекрасно работает.

Теперь мне нужно, чтобы он отличался от класса, содержащего категорию и страну (обе перечисления). Теперь Distinct не работает.

Что я делаю неправильно?

Ответ 1

Я считаю, что этот пост объясняет вашу проблему: http://blog.jordanterrell.com/post/LINQ-Distinct()-does-not-work-as-expected.aspx

Содержимое указанной ссылки можно суммировать, сказав, что метод Distinct() можно заменить, выполнив следующее.

var distinctItems = items
       .GroupBy(x => x.PropertyToCompare)
       .Select(x => x.First());

Ответ 2

попробуйте IQualityComparer

public class MyObjEqualityComparer : IEqualityComparer<MyObj>
{
    public bool Equals(MyObj x, MyObj y)
    {
        return x.Category.Equals(y.Category) &&
               x.Country.Equals(y.Country);
    }

    public int GetHashCode(MyObj obj)
    {
        return obj.GetHashCode();
    }
}

затем используйте здесь

var comparer = new MyObjEqualityComparer();
myObjs.Where(m => m.SomeProperty == "whatever").Distinct(comparer);

Ответ 3

Для объяснения взгляните на другие ответы. Я просто предоставляю один из способов решения этой проблемы.

Вам может понравиться this:

public class LambdaComparer<T>:IEqualityComparer<T>{
  private readonly Func<T,T,bool> _comparer;
  private readonly Func<T,int> _hash;
  public LambdaComparer(Func<T,T,bool> comparer):
    this(comparer,o=>0) {}
  public LambdaComparer(Func<T,T,bool> comparer,Func<T,int> hash){
    if(comparer==null) throw new ArgumentNullException("comparer");
    if(hash==null) throw new ArgumentNullException("hash");
    _comparer=comparer;
    _hash=hash;
  }
  public bool Equals(T x,T y){
    return _comparer(x,y);
  }
  public int GetHashCode(T obj){
    return _hash(obj);
  }
}

Использование:

public void Foo{
  public string Fizz{get;set;}
  public BarEnum Bar{get;set;}
}

public enum BarEnum {One,Two,Three}

var lst=new List<Foo>();
lst.Distinct(new LambdaComparer<Foo>(
  (x1,x2)=>x1.Fizz==x2.Fizz&&
           x1.Bar==x2.Bar));

Вы можете даже обернуть его вокруг, чтобы не писать шумную new LambdaComparer<T>(...) вещь:

public static class EnumerableExtensions{
 public static IEnumerable<T> SmartDistinct<T>
  (this IEnumerable<T> lst, Func<T, T, bool> pred){
   return lst.Distinct(new LambdaComparer<T>(pred));
 }
}

Использование:

lst.SmartDistinct((x1,x2)=>x1.Fizz==x2.Fizz&&x1.Bar==x2.Bar);

NB: работает надежно только для Linq2Objects

Ответ 4

Вы не ошибаетесь, это просто плохая реализация .Distinct() в .NET Framework.

Один из способов исправить это уже показан в других ответах, но есть также более короткое решение, которое имеет то преимущество, что вы можете легко использовать его как метод расширения, без необходимости настроить значения хеш-объекта.

Взгляните на это:


Применение:

var myQuery=(from x in Customers select x).MyDistinct(d => d.CustomerID);

Примечание. В этом примере используется запрос базы данных, но он также работает с перечислимым списком объектов.


Объявление MyDistinct:

public static class Extensions
{
    public static IEnumerable<T> MyDistinct<T, V>(this IEnumerable<T> query, 
                                                    Func<T, V> f)
    {
        return query.GroupBy(f).Select(x=>x.First());
    }
}

И он работает для всего, как объектов, так и сущностей. При необходимости вы можете создать второй метод перегруженного расширения для IQueryable<T>, просто заменив тип возвращаемого значения и первый тип параметра в приведенном выше примере.

Ответ 5

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

Сначала важно прочитать и понять эти две вещи:

  1. IEqualityComparer
  2. EqualityComparer

Короче говоря, чтобы расширение .Distinct() понимало, как определить равенство вашего объекта - вы должны определить "EqualityComparer" для вашего объекта T. Когда вы читаете документы Microsoft, оно буквально утверждает:

Мы рекомендуем вам наследовать от класса EqualityComparer вместо реализации интерфейса IEqualityComparer...

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

Чтобы расширение .Distinct() работало успешно, вы должны обеспечить точное сравнение ваших объектов. В случае .Distinct() метод GetHashCode() - это то, что действительно имеет значение.

Вы можете проверить это сами, написав реализацию GetHashCode(), которая просто возвращает текущий хэш-код передаваемого объекта, и вы увидите, что результаты плохие, потому что это значение меняется при каждом запуске. Это делает ваши объекты слишком уникальными, поэтому важно написать правильную реализацию этого метода.

Ниже приведена точная копия примера кода со страницы IEqualityComparer<T> с тестовыми данными, небольшая модификация метода GetHashCode() и комментарии для демонстрации сути.

//Did this in LinqPad
void Main()
{
    var lst = new List<Box>
    {
        new Box(1, 1, 1),
        new Box(1, 1, 1),
        new Box(1, 1, 1),
        new Box(1, 1, 1),
        new Box(1, 1, 1)
    };

    //Demonstration that the hash code for each object is fairly 
    //random and won't help you for getting a distinct list
    lst.ForEach(x => Console.WriteLine(x.GetHashCode()));

    //Demonstration that if your EqualityComparer is setup correctly
    //then you will get a distinct list
    lst = lst
        .Distinct(new BoxEqualityComparer())
        .ToList();

    lst.Dump();
}

public class Box
{
    public Box(int h, int l, int w)
    {
        this.Height = h;
        this.Length = l;
        this.Width = w;
    }

    public int Height { get; set; }
    public int Length { get; set; }
    public int Width { get; set; }

    public override String ToString()
    {
        return String.Format("({0}, {1}, {2})", Height, Length, Width);
    }
}

public class BoxEqualityComparer 
    : EqualityComparer<Box>
{
    public override bool Equals(Box b1, Box b2)
    {
        if (b2 == null && b1 == null)
            return true;
        else if (b1 == null || b2 == null)
            return false;
        else if (b1.Height == b2.Height && b1.Length == b2.Length
                            && b1.Width == b2.Width)
            return true;
        else
            return false;
    }

    public override int GetHashCode(Box bx)
    {
        #region This works
        //In this example each component of the box object are being XOR'd together
        int hCode = bx.Height ^ bx.Length ^ bx.Width;

        //The hashcode of an integer, is that same integer
        return hCode.GetHashCode();
        #endregion

        #region This won't work
        //Comment the above lines and uncomment this line below if you want to see Distinct() not work
        //return bx.GetHashCode();
        #endregion
    }
}