使用委托作为LINQ的Distinct()方法中的相等比较器。

52

我有一个使用自己的自定义比较器的LINQ Distinct()语句,就像这样:

class MyComparer<T> : IEqualityComparer<T> where T : MyType
{
    public bool Equals(T x, T y)
    {
        return x.Id.Equals(y.Id);
    }

    public int GetHashCode(T obj)
    {
        return obj.Id.GetHashCode();
    }
}

...

var distincts = bundle.GetAllThings.Distinct(new MyComparer<MySubType>());

这一切都很好,按照我想要的方式工作。出于好奇,我需要定义自己的Comparer,还是可以用委托来替换它?我认为我应该能够像这样做:

var distincts = bundle.GetAllThings.Distinct((a,b) => a.Id == b.Id);

但是这段代码无法编译。有没有什么巧妙的方法?


在你的Equals实现中,你应该对xy进行ReferenceEquals检查以防止空引用。 - nicodemus13
5个回答

108

Distinct方法的第二个参数需要一个IEqualityComparer,因此您需要一个IEqualityComparer。不过,制作一个通用的可以接受委托的IEqualityComparer并不太难。当然,这可能已经在一些地方实现了,例如MoreLINQ建议在其他答案中提到。

您可以像这样实现它:

public static class Compare
{
    public static IEnumerable<T> DistinctBy<T, TIdentity>(this IEnumerable<T> source, Func<T, TIdentity> identitySelector)
    {
        return source.Distinct(Compare.By(identitySelector));
    }

    public static IEqualityComparer<TSource> By<TSource, TIdentity>(Func<TSource, TIdentity> identitySelector)
    {
        return new DelegateComparer<TSource, TIdentity>(identitySelector);
    }

    private class DelegateComparer<T, TIdentity> : IEqualityComparer<T>
    {
        private readonly Func<T, TIdentity> identitySelector;

        public DelegateComparer(Func<T, TIdentity> identitySelector)
        {
            this.identitySelector = identitySelector;
        }

        public bool Equals(T x, T y)
        {
            return Equals(identitySelector(x), identitySelector(y));
        }

        public int GetHashCode(T obj)
        {
            return identitySelector(obj).GetHashCode();
        }
    }
}

这里是语法:

source.DistinctBy(a => a.Id);

或者,如果您觉得这样更清楚:
source.Distinct(Compare.By(a => a.Id));

2
完成后,您还可以编写自己的扩展方法,以接受委托。 - Amanda Mitchell
3
好的!如果有人错过了,你可以这样做:source.DistinctBy(x => new { x.A, x.B }); 该代码可以实现去重功能,根据对象的 A 和 B 属性来判断是否为重复项。 - watbywbarif
TIdentity 应该有一个 IEquatable<TIdentity> 约束,因此使用 identitySelector(x).Equals(identitySelector(y)) 以避免由于泛型无法推断构造类型重载而导致的引用比较。 - Miguel A. Arilla

11

很遗憾,Distinct没有提供这样的重载,所以您现有的选项是不错的。

使用MoreLinq,您可以使用DistinctBy运算符。

var distincts = bundle.GetAllThings.DistinctBy(a => a.Id); 

你可能还想考虑编写一个通用的 ProjectionEqualityComparer,该类可以将适当的委托转换为IEqualityComparer<T>实现。可以参考这里的实现。


7

这是我的一种奇怪而又有趣的C#技巧:

entities
    .GroupBy(e => e.Id)
    .Select(g => g.First())

2
根据https://dev59.com/BnM_5IYBdhLWcg3w4njb#1183877 - 实际上可以简化为单个GroupBy调用。它有一种重载,可以提供选择器:entities.GroupBy(e => e.Id, (id, g) => g.First()) - Alain

2

这个链接展示了如何创建扩展方法以便像你所说的那样使用Distinct。你需要编写两个Distinct扩展方法和一个IEqualityComparer

以下是来自该网站的代码:

public static class Extensions
    {
        public static IEnumerable<T> Distinct<T>(this IEnumerable<T> source, Func<T, T, bool> comparer)
        {           
            return source.Distinct(new DelegateComparer<T>(comparer));
        }

        public static IEnumerable<T> Distinct<T>(this IEnumerable<T> source, Func<T, T, bool> comparer, Func<T,int> hashMethod)
        {
            return source.Distinct(new DelegateComparer<T>(comparer,hashMethod));
        }
    }

    public class DelegateComparer<T> : IEqualityComparer<T>
    {
        private Func<T, T, bool> _equals;
        private Func<T,int> _getHashCode;

        public DelegateComparer(Func<T, T, bool> equals)
        {
            this._equals = equals;
        }

        public DelegateComparer(Func<T, T, bool> equals, Func<T,int> getHashCode)
        {
            this._equals = equals;
            this._getHashCode = getHashCode;
        }

        public bool Equals(T a, T b)
        {
            return _equals(a, b);
        }

        public int GetHashCode(T a)
        {
            if (_getHashCode != null)       
                return _getHashCode(a);       
            else
                return a.GetHashCode();
        }
    }

5
这段代码存在问题,它没有遵循 GetHashCode 和 Equals 的规则(请参阅msdn.microsoft.com/en-us/library/system.object.gethashcode.aspx)。每当你在自定义类上使用第一个重载时,很可能会得到错误的结果。GetHashCode 必须 在两个对象的 Equals 返回 true 时返回相同的值。 - Gideon Engelberth

1
截至NET6,System.Linq库中添加了一个名为DistinctBy的扩展方法。
示例:
Planet[] planets =
{
    Planet.Mercury,
    Planet.Venus,
    Planet.Earth,
    Planet.Mars,
    Planet.Jupiter,
    Planet.Saturn,
    Planet.Uranus,
    Planet.Neptune,
    Planet.Pluto
};

foreach (Planet planet in planets.DistinctBy(p => p.Type))
{
    Console.WriteLine(planet);
}

// This code produces the following output:
//     Planet { Name = Mercury, Type = Rock, OrderFromSun = 1 }
//     Planet { Name = Jupiter, Type = Gas, OrderFromSun = 5 }
//     Planet { Name = Uranus, Type = Liquid, OrderFromSun = 7 }
//     Planet { Name = Pluto, Type = Ice, OrderFromSun = 9 }

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