使用LINQ和对象进行Distinct操作

17
直到最近,我使用了LINQ中的Distinct来从表格中选择一个不同的类别(一个枚举)。这个方法一直很好用。
现在我需要按照一个包含类别和国家(两个枚举)的类进行去重。但是Distinct不再起作用了。
我做错了什么?
5个回答

29

请勿仅通过链接回答。在答案中包含相关代码。 - Dan Bailiff
6
是的,我不再这样做了,但在2010年我没有注意到这个做法。 - Stilgar
链接已失效。关于问题的解释永远丧失了。 - undefined
1
@Guillaume 幸运的是,它已经被存档了- https://web.archive.org/web/20201203072646/http://blog.jordanterrell.com/post/LINQ-Distinct()-does-not-work-as-expected.aspx有人应该去阅读它,并将内容提取到答案中,但我现在无法做到。 - undefined

4

尝试使用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);

我喜欢这个解决方案,但是我遇到了一个问题。 GetHashCode 导致它无法找到匹配项。 我不得不将其更改为类似于 return obj.Category.GetHashCode() + obj.Country.GetHashCode() 的东西。 - James R.

4

你没有做错,只是.NET Framework中.Distinct()的实现有问题。

修复它的一种方法已经在其他答案中展示了,但也有一种更简短的解决方案,它的优点是你可以将其作为一个扩展方法轻松地在任何地方使用,而不必调整对象的哈希值。

看看这个:


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

如果您希望更简短,这与上面相同,但是可以作为“一行代码”:

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

它适用于所有的东西,包括对象和实体。如果需要,您可以通过仅替换上面示例中的返回类型和第一个参数类型来创建第二个重载的扩展方法IQueryable<T>


测试数据: 您可以使用以下测试数据进行测试:

List<A> GetData() 
    => new List<A>()
    {
        new A() { X="1", Y="2" }, new A() { X="1", Y="2" },
        new A() { X="2", Y="3" }, new A() { X="2", Y="3" },
        new A() { X="1", Y="3" }, new A() { X="1", Y="3" },
    };

class A
{
    public string X;
    public string Y;
}

例子:

void Main()
{
    // returns duplicate rows:
    GetData().Distinct().Dump();
    // Gets distinct rows by i.X
    GetData().MyDistinct(i => i.X).Dump();
}

3

为了解释,请查看其他答案。我只提供一种处理这个问题的方法。

你可能会喜欢这个

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

注意:仅在Linq2Objects下可靠地工作。

1

我知道这是一个老问题,但我对任何答案都不满意。我花时间自己弄清楚了这个问题,并想分享我的发现。

首先,重要的是要阅读并理解以下两个内容:

  1. IEqualityComparer
  2. EqualityComparer

长话短说,为了让 .Distinct() 扩展方法理解如何确定您的对象的相等性 - 您必须为对象 T 定义一个 "EqualityComparer"。当您阅读 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
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接