使用匿名类型的LINQ Select Distinct

162

我有一个对象的集合,确切的类型并不重要。我想从中提取出一对特定属性的所有唯一对,方法如下:

myObjectCollection.Select(item=>new
                                {
                                     Alpha = item.propOne,
                                     Bravo = item.propTwo
                                }
                 ).Distinct();

所以我的问题是:在这种情况下,Distinct会使用默认的对象equals(对我来说无用,因为每个对象都是新的),还是可以告诉它使用不同的equals(在这种情况下,Alpha和Bravo的相等值=>相等的实例)?如果这样做不能实现结果,是否有其他方法可以实现?


这是LINQ-to-Objects还是LINQ-to-SQL?如果只是对象,那么你可能没有什么好运。然而,如果是L2S,那么它可能会起作用,因为DISTINCT会传递到SQL语句中。 - James Curran
你的问题的解决方案看起来像这样:https://dev59.com/omgv5IYBdhLWcg3wSewe#60412781 - Paul Alexeev
8个回答

207

请阅读K. Scott Allen的这篇博客,了解更多信息。

简而言之,C#编译器覆盖了匿名类型的Equals和GetHashCode方法。 这两种重载方法的实现使用类型上的所有公共属性来计算对象的哈希码并测试相等性。 如果两个具有相同属性值的匿名类型对象,则它们相等。因此,在返回匿名类型的查询上使用Distinct()方法是完全安全的。


3
只有当属性本身是值类型或实现了值相等性时,才会是正确的——请参阅我的回答。 - tvanfosson
是的,因为它在每个属性上使用了 GetHashCode,所以仅在每个属性具有其自己的独特实现时才能工作。我认为大多数用例只涉及简单类型作为属性,因此通常是安全的。 - Matt Hamilton
4
最终的含义是,两个匿名类型的相等性取决于其成员的相等性,对我来说这很好,因为我可以在能够访问和重写相等性的地方定义成员。我只是不想必须为此创建一个类来覆盖equals方法。 - GWLlosa
4
或许向微软请愿,要求在C#中引入类似VB的“关键字”语法(可以指定匿名类型的某些属性作为“主键”——请参阅我提供的博客文章),这值得一试。 - Matt Hamilton
@tvanfosson 我对将类对象分配给匿名类型的属性进行了快速测试,似乎对于这些属性,它会退回到对象引用相等性,正如我所预期的那样。 - angularsen

14
public class DelegateComparer<T> : IEqualityComparer<T>
{
    private Func<T, T, bool> _equals;
    private Func<T, int> _hashCode;
    public DelegateComparer(Func<T, T, bool> equals, Func<T, int> hashCode)
    {
        _equals= equals;
        _hashCode = hashCode;
    }
    public bool Equals(T x, T y)
    {
        return _equals(x, y);
    }

    public int GetHashCode(T obj)
    {
        if(_hashCode!=null)
            return _hashCode(obj);
        return obj.GetHashCode();
    }       
}

public static class Extensions
{
    public static IEnumerable<T> Distinct<T>(this IEnumerable<T> items, 
        Func<T, T, bool> equals, Func<T,int> hashCode)
    {
        return items.Distinct(new DelegateComparer<T>(equals, hashCode));    
    }
    public static IEnumerable<T> Distinct<T>(this IEnumerable<T> items,
        Func<T, T, bool> equals)
    {
        return items.Distinct(new DelegateComparer<T>(equals,null));
    }
}

var uniqueItems=students.Select(s=> new {FirstName=s.FirstName, LastName=s.LastName})
            .Distinct((a,b) => a.FirstName==b.FirstName, c => c.FirstName.GetHashCode()).ToList();

之前的格式混乱,非常抱歉。

涉及IT技术相关内容,请您耐心等待。

此扩展无法处理类型为 objectobject 的情况。如果这两个 object 都是 string,它仍然会返回重复的行。尝试将 FirstName 定义为 object 类型,并将其分配为相同的 string - CallMeLaNN
这是一个关于有类型对象的很好的回答,但对于匿名类型并不需要。 - crokusek

5
有趣的是它在C#中可行,但在VB中不可行。
返回26个字母:
var MyBet = "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ";
MyBet.ToCharArray()
.Select(x => new {lower = x.ToString().ToLower(), upper = x.ToString().ToUpper()})
.Distinct()
.Dump();

返回52...
Dim MyBet = "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ"
MyBet.ToCharArray() _
.Select(Function(x) New With {.lower = x.ToString.ToLower(), .upper = x.ToString.ToUpper()}) _
.Distinct() _
.Dump()

13
如果在匿名类型中添加Key关键字,.Distinct()将按预期工作(例如:New With { Key .lower = x.ToString.ToLower(), Key .upper = x.ToString.ToUpper()})。 - Cᴏʀʏ
3
Cory正确。C#代码new {A = b}的正确翻译是New {Key .A = b}。匿名VB类中的非关键属性是可变的,这就是它们被引用比较的原因。在C#中,匿名类的所有属性都是不可变的。 - Heinzi

4

我进行了一项测试,发现如果属性是值类型,则似乎可以正常工作。 如果它们不是值类型,则该类型需要提供自己的Equals和GetHashCode实现才能正常工作。 我认为字符串会起作用。


2
你可以创建自己的Distinct扩展方法,该方法接受lambda表达式。以下是一个示例:
创建一个从IEqualityComparer接口派生的类。
public class DelegateComparer<T> : IEqualityComparer<T>
{
    private Func<T, T, bool> _equals;
    private Func<T, int> _hashCode;
    public DelegateComparer(Func<T, T, bool> equals, Func<T, int> hashCode)
    {
        _equals= equals;
        _hashCode = hashCode;
    }
    public bool Equals(T x, T y)
    {
        return _equals(x, y);
    }

    public int GetHashCode(T obj)
    {
        if(_hashCode!=null)
            return _hashCode(obj);
        return obj.GetHashCode();
    }       
}

然后创建您的Distinct扩展方法。

public static class Extensions
{
    public static IEnumerable<T> Distinct<T>(this IEnumerable<T> items, 
        Func<T, T, bool> equals, Func<T,int> hashCode)
    {
        return items.Distinct(new DelegateComparer<T>(equals, hashCode));    
    }
    public static IEnumerable<T> Distinct<T>(this IEnumerable<T> items,
        Func<T, T, bool> equals)
    {
        return items.Distinct(new DelegateComparer<T>(equals,null));
    }
}

你可以使用这种方法来查找不同的项。

var uniqueItems=students.Select(s=> new {FirstName=s.FirstName, LastName=s.LastName})
            .Distinct((a,b) => a.FirstName==b.FirstName, c => c.FirstName.GetHashCode()).ToList();

此扩展无法处理类型为 objectobject 的情况。如果这两个 object 都是 string,它仍然会返回重复的行。尝试将 FirstName 定义为 object 类型,并将其赋值为相同的 string - CallMeLaNN

0

嘿,我遇到了同样的问题并找到了解决方案。 你需要实现IEquatable接口或简单地重写(Equals & GetHashCode)方法。但这不是关键,关键在于GetHashCode方法。你不应该返回你的类对象的哈希码,而应该返回你想要比较的属性的哈希值,就像这样。

public override bool Equals(object obj)
    {
        Person p = obj as Person;
        if ( obj == null )
            return false;
        if ( object.ReferenceEquals( p , this ) )
            return true;
        if ( p.Age == this.Age && p.Name == this.Name && p.IsEgyptian == this.IsEgyptian )
            return true;
        return false;
        //return base.Equals( obj );
    }
    public override int GetHashCode()
    {
        return Name.GetHashCode();
    }

你看到了我有一个名为Person的类,它有三个属性(Name、Age、IsEgyptian"Because I am")。在GetHashCode中,我返回了Name属性的哈希值而不是整个Person对象的哈希值。

试一下,应该可以工作。祝好! 谢谢, Modather Sadik


1
GetHashCode应该使用在相等比较中使用的所有字段和属性,而不仅仅是其中一个。例如:public override int GetHashCode() { return this.Name.GetHashCode() ^ this.Age.GetHashCode() ^ this.IsEgyptian.GetHashCode(); } - JG in SD
有关生成良好哈希算法的信息,请访问:https://dev59.com/EnVC5IYBdhLWcg3wihqv - JG in SD

0
为了让它在VB.NET中工作,您需要在匿名类型的每个属性之前指定Key关键字,就像这样:
myObjectCollection.Select(Function(item) New With
{
    Key .Alpha = item.propOne,
    Key .Bravo = item.propTwo
}).Distinct()

我曾经为此苦恼,认为VB.NET不支持这种功能,但实际上它是支持的。


0

如果 AlphaBravo 都继承自一个共同的类,您将能够通过实现 IEquatable<T> 在父类中指定相等性检查。

例如:

public class CommonClass : IEquatable<CommonClass>
{
    // needed for Distinct()
    public override int GetHashCode() 
    {
        return base.GetHashCode();
    }

    public bool Equals(CommonClass other)
    {
        if (other == null) return false;
        return [equality test];
    }
}

如果您在匿名类型的属性中使用实现了IEquatable<T>接口的类,则会调用Equals方法,而不是默认行为(通过反射检查所有公共属性)。 - D_Guidi

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