如何使用IEqualityComparer接口

120

我的数据库里有一些编号相同的铃铛,我想把它们全部获取而不重复。我创建了一个比较类来完成这项工作,但是执行该函数时,与没有去重的函数相比,延迟很大,从0.6秒增加到3.2秒!

我是否做得对,还是需要使用另一种方法?

reg.AddRange(
    (from a in this.dataContext.reglements
     join b in this.dataContext.Clients on a.Id_client equals b.Id
     where a.date_v <= datefin && a.date_v >= datedeb
     where a.Id_client == b.Id
     orderby a.date_v descending 
     select new Class_reglement
     {
         nom  = b.Nom,
         code = b.code,
         Numf = a.Numf,
     })
    .AsEnumerable()
    .Distinct(new Compare())
    .ToList());

class Compare : IEqualityComparer<Class_reglement>
{
    public bool Equals(Class_reglement x, Class_reglement y)
    {
        if (x.Numf == y.Numf)
        {
            return true;
        }
        else { return false; }
    }
    public int GetHashCode(Class_reglement codeh)
    {
        return 0;
    }
}

17
你可能想要查看GetHashCode的指南和规则 - Conrad Frix
4
这篇博客解释了如何完美使用IEqualityComparer:http://blog.alex-turok.com/2013/03/c-linq-and-iequalitycomparer.html - Jeremy Ray Brown
7个回答

188

你的 GetHashCode 实现总是返回相同的值。因为 Distinct 内部构建了一个哈希表,所以它依赖于良好的哈希函数来高效工作。

在实现类的接口时,重要的是要阅读文档,了解应该实现哪个契约。1

在你的代码中,解决方案是将 GetHashCode 转发到 Class_reglement.Numf.GetHashCode 并在那里适当地实现它。

此外,你的 Equals 方法充满了不必要的代码。 它可以重写为以下形式(具有相同的语义,代码量减少了4分之3,更易读):

public bool Equals(Class_reglement x, Class_reglement y)
{
    return x.Numf == y.Numf;
}

最后,ToList 调用是不必要的且耗时的: AddRange 接受任何 IEnumerable,因此不需要转换为 List。在这里,AsEnumerable 也是多余的,因为在 AddRange 中处理结果会自动进行。


1 在不知道代码实际作用的情况下编写代码被称为模仿航空公司式编程。这是一种令人吃惊地普遍的做法,但基本上行不通。


21
当 x 或 y 为空时,您的 Equals 将失败。 - dzendras
4
GetHashCode同样如此。但请注意,IEqualityComparer<T>的文档没有说明对于null参数应该怎么做 - 但是文章中提供的示例也不会处理null - Konrad Rudolph
67
哇,"Abomination"(丑恶、可憎)这个词用得太过严厉了。我们在这里是要互相帮助,而不是互相侮辱。我猜有些人可能会觉得好笑,但我建议把它删掉。 - Jess
5
感谢让我读了有关“货物崇拜式编程”的维基百科文章并将我的Skype标签更改为“// 深度魔法从这里开始......接着进行一些重量级的巫术”,加1。请问需要翻译的是这段话吗? - Alex
5
@NeilBenn,你误将坦率的建议视为粗鲁。由于提问者接受了答案(而且我要注意的是,这个答案更加严厉!),他们似乎没有犯同样的错误。我不确定为什么你认为给出建议是粗鲁的,但当你说“这个人不需要听讲座”时,你是错误的。我强烈反对:这场讲座确实是必需的,并且它被深深铭记在心中。代码原本写得很差,基于糟糕的工作实践。如果不指出这一点,那就是一种失职行为,也不会有任何帮助,因为提问者无法改进他们的工作方式。 - Konrad Rudolph
显示剩余7条评论

55
尝试以下代码:

试试这段代码:

public class GenericCompare<T> : IEqualityComparer<T> where T : class
{
    private Func<T, object> _expr { get; set; }
    public GenericCompare(Func<T, object> expr)
    {
        this._expr = expr;
    }
    public bool Equals(T x, T y)
    {
        var first = _expr.Invoke(x);
        var sec = _expr.Invoke(y);
        if (first != null && first.Equals(sec))
            return true;
        else
            return false;
    }
    public int GetHashCode(T obj)
    {
        return obj.GetHashCode();
    }
}

它的使用示例如下:

collection = collection
    .Except(ExistedDataEles, new GenericCompare<DataEle>(x=>x.Id))
    .ToList(); 

22
GetHashCode需要使用以下表达式:return _expr.Invoke(obj).GetHashCode(); 查看此帖子了解其用法。 - orad
1
如果集合包含空值,这不应该失败吗?然而,在 VS C# 交互式的快速实验中似乎没有抛出空引用异常! - Social Developer

10
如果你想要一个通用的解决方案,可以基于类的属性(作为键)创建一个 IEqualityComparer。请看下面这个示例:
public class KeyBasedEqualityComparer<T, TKey> : IEqualityComparer<T>
{
    private readonly Func<T, TKey> _keyGetter;

    public KeyBasedEqualityComparer(Func<T, TKey> keyGetter)
    {
        if (default(T) == null)
        {
            _keyGetter = (x) => x == null ? default : keyGetter(x);
        }
        else
        {
            _keyGetter = keyGetter;
        }
    }

    public bool Equals(T x, T y)
    {
        return EqualityComparer<TKey>.Default.Equals(_keyGetter(x), _keyGetter(y));
    }

    public int GetHashCode(T obj)
    {
        TKey key = _keyGetter(obj);

        return key == null ? 0 : key.GetHashCode();
    }
}

public static class KeyBasedEqualityComparer<T>
{
    public static KeyBasedEqualityComparer<T, TKey> Create<TKey>(Func<T, TKey> keyGetter)
    {
        return new KeyBasedEqualityComparer<T, TKey>(keyGetter);
    }
}

为了更好地提高结构体的性能,不需要进行任何装箱操作。

使用方法如下:

IEqualityComparer<Class_reglement> equalityComparer =
  KeyBasedEqualityComparer<Class_reglement>.Create(x => x.Numf);

你能详细说明一下“无需装箱”部分吗?与其他解决方案相比,你具体做了什么来避免装箱? - Kirikan
@Kirikan 这是一个通用解决方案,适用于任何类型(不仅限于“Class_reglement”)。而且没有对“object”或任何类型的转换,因此没有性能开销。如果类型是结构体,则尤其相关。 - user764754

3
本答案的目的是在之前的答案基础上进行改进:
  • 使构造函数中的lambda表达式可选,以便默认情况下可以检查完整对象的相等性,而不仅仅是其中一个属性。
  • 操作不同类型的类,甚至包括子对象或嵌套列表等复杂类型。而不仅仅是由原始类型属性组成的简单类。
  • 不考虑可能存在的列表容器差异。
  • 这里有一个第一个简单代码示例,仅适用于简单类型(仅由原始属性组成的类型),以及第二个完整代码示例(适用于更广泛范围的类和复杂类型)。
以下是我的尝试:
public class GenericEqualityComparer<T> : IEqualityComparer<T> where T : class
{
    private Func<T, object> _expr { get; set; }

    public GenericEqualityComparer() => _expr = null;

    public GenericEqualityComparer(Func<T, object> expr) => _expr = expr;

    public bool Equals(T x, T y)
    {
        var first = _expr?.Invoke(x) ?? x;
        var sec = _expr?.Invoke(y) ?? y;

        if (first == null && sec == null)
            return true;

        if (first != null && first.Equals(sec))
            return true;

        var typeProperties = typeof(T).GetProperties();

        foreach (var prop in typeProperties)
        {
            var firstPropVal = prop.GetValue(first, null);
            var secPropVal = prop.GetValue(sec, null);

            if (firstPropVal != null && !firstPropVal.Equals(secPropVal))
                return false;
        }

        return true;
    }

    public int GetHashCode(T obj) =>
        _expr?.Invoke(obj).GetHashCode() ?? obj.GetHashCode();
}

我知道我们仍然可以进行优化(也许使用递归?).. 但是这种方法在很多类上运行起来非常好,而且不需要过多的复杂性。 ;) 编辑: 一天后,这是我的$10尝试: 首先,在一个单独的静态扩展类中,你需要:
public static class CollectionExtensions
{
    public static bool HasSameLengthThan<T>(this IEnumerable<T> list, IEnumerable<T> expected)
    {
        if (list.IsNullOrEmptyCollection() && expected.IsNullOrEmptyCollection())
            return true;

        if ((list.IsNullOrEmptyCollection() && !expected.IsNullOrEmptyCollection()) || (!list.IsNullOrEmptyCollection() && expected.IsNullOrEmptyCollection()))
            return false;

        return list.Count() == expected.Count();
    }

    /// <summary>
    /// Used to find out if a collection is empty or if it contains no elements.
    /// </summary>
    /// <typeparam name="T">Type of the collection's items.</typeparam>
    /// <param name="list">Collection of items to test.</param>
    /// <returns><c>true</c> if the collection is <c>null</c> or empty (without items), <c>false</c> otherwise.</returns>
    public static bool IsNullOrEmptyCollection<T>(this IEnumerable<T> list) => list == null || !list.Any();
}

接下来,这是更新后的类,可以适用于更广泛的类:

public class GenericComparer<T> : IEqualityComparer<T> where T : class
{
    private Func<T, object> _expr { get; set; }

    public GenericComparer() => _expr = null;

    public GenericComparer(Func<T, object> expr) => _expr = expr;

    public bool Equals(T x, T y)
    {
        var first = _expr?.Invoke(x) ?? x;
        var sec = _expr?.Invoke(y) ?? y;

        if (ObjEquals(first, sec))
            return true;

        var typeProperties = typeof(T).GetProperties();

        foreach (var prop in typeProperties)
        {
            var firstPropVal = prop.GetValue(first, null);
            var secPropVal = prop.GetValue(sec, null);

            if (!ObjEquals(firstPropVal, secPropVal))
            {
                var propType = prop.PropertyType;

                if (IsEnumerableType(propType) && firstPropVal is IEnumerable && !ArrayEquals(firstPropVal, secPropVal))
                    return false;

                if (propType.IsClass)
                {
                    if (!DeepEqualsFromObj(firstPropVal, secPropVal, propType))
                        return false;

                    if (!DeepObjEquals(firstPropVal, secPropVal))
                        return false;
                }
            }
        }

        return true;
    }

    public int GetHashCode(T obj) =>
        _expr?.Invoke(obj).GetHashCode() ?? obj.GetHashCode();

    #region Private Helpers

    private bool DeepObjEquals(object x, object y) =>
        new GenericComparer<object>().Equals(x, y);

    private bool DeepEquals<U>(U x, U y) where U : class =>
        new GenericComparer<U>().Equals(x, y);

    private bool DeepEqualsFromObj(object x, object y, Type type)
    {
        dynamic a = Convert.ChangeType(x, type);
        dynamic b = Convert.ChangeType(y, type);
        return DeepEquals(a, b);
    }

    private bool IsEnumerableType(Type type) =>
        type.GetInterface(nameof(IEnumerable)) != null;

    private bool ObjEquals(object x, object y)
    {
        if (x == null && y == null) return true;
        return x != null && x.Equals(y);
    }

    private bool ArrayEquals(object x, object y)
    {
        var firstList = new List<object>((IEnumerable<object>)x);
        var secList = new List<object>((IEnumerable<object>)y);

        if (!firstList.HasSameLengthThan(secList))
            return false;

        var elementType = firstList?.FirstOrDefault()?.GetType();
        int cpt = 0;
        foreach (var e in firstList)
        {
            if (!DeepEqualsFromObj(e, secList[cpt++], elementType))
                return false;
        }

        return true;
    }

    #endregion Private Helpers

我们仍然可以进行优化,但值得尝试^^。

我刚刚编辑了一个GenericComparer类,它也适用于具有其他对象或列表作为嵌套成员的类型。 - Islem BEZZARGA
@GertArnold,除了重复我已经说过的话外,即这应该适用于更广泛的类,包括嵌套对象或数组的对象,我真的不知道还要加些什么...... 如果您的唯一关注是烹饪,那么它并不能为您烹饪。 :) - Islem BEZZARGA
@GertArnold:我刚刚为您编辑了这个答案的顶部;)并添加了一个_Edit 2_部分。我真的认为从我的代码片段中这是很明显的。无论如何,我现在会养成这个好习惯,谢谢。 - Islem BEZZARGA

3

只需要编写代码,实现GetHashCodeNULL验证:

public class Class_reglementComparer : IEqualityComparer<Class_reglement>
{
    public bool Equals(Class_reglement x, Class_reglement y)
    {
        if (x is null || y is null))
            return false;

        return x.Numf == y.Numf;
    }

    public int GetHashCode(Class_reglement product)
    {
        //Check whether the object is null 
        if (product is null) return 0;

        //Get hash code for the Numf field if it is not null. 
        int hashNumf = product.hashNumf == null ? 0 : product.hashNumf.GetHashCode();

        return hashNumf;
    }
}

示例: 按 Numf 分组,列出不同的 Class_reglement 列表。

List<Class_reglement> items = items.Distinct(new Class_reglementComparer());

2
你需要使用AsEnumerable方法来获取比较类的内容,这会使得排序逻辑从数据库服务器转移到了数据库客户端(即你的应用程序)。这意味着你的客户端现在需要检索和处理更多的记录,这总是比在数据库执行查找要低效,因为适当的索引可以在数据库中使用。
你应该尝试编写一个满足你需求的where子句,详见使用IEqualityComparer与LINQ to Entities Except子句

1
"

IEquatable<T>可以是现代框架中更简单的方法。

您将获得一个简单明了的bool Equals(T other)函数,并且无需烦恼转换或创建单独的类。"

public class Person : IEquatable<Person>
{
    public Person(string name, string hometown)
    {
        this.Name = name;
        this.Hometown = hometown;
    }

    public string Name { get; set; }
    public string Hometown { get; set; }

    // can't get much simpler than this!
    public bool Equals(Person other)
    {
        return this.Name == other.Name && this.Hometown == other.Hometown;
    }

    public override int GetHashCode()
    {
        return Name.GetHashCode();  // see other links for hashcode guidance 
    }
}

请注意,如果在字典或类似于“Distinct”的东西中使用此方法,则必须实现GetHashCode
PS。我认为任何自定义的Equals方法都不能直接在数据库端上使用entity framework(我认为您知道这一点,因为您使用了AsEnumerable),但这是一个更简单的方法来进行简单的等于操作。
如果似乎有问题(例如在执行ToDictionary时出现重复键错误),请在Equals内设置断点以确保它被命中,并确保已定义GetHashCode(带有override关键字)。

1
你仍然需要检查是否为空。 - disklosr
我从未遇到过这种情况,但下次我会记住的。你在List<T>中有一个null或类似的东西吗? - Simon_Weaver
1
.Equals()方法中,您似乎将other.Hometown与其自身进行了比较,而不是与this.Hometown进行比较。 - Jake Stokes
糟糕,修正了打字错误 :) - Simon_Weaver

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