为什么 Contains() 返回 false 但包装在一个列表中,而 Intersect() 返回 true

本文关键字:返回 列表 一个 Intersect true false Contains 包装 为什么 | 更新日期: 2023-09-27 18:18:52

问题:

当我对正确实现IEquatable并覆盖GetHashCode类的IEnumerable<T>使用Contains()时,它会返回 false。如果我将匹配目标包装在列表中并执行Intersect()则匹配工作正常。我更喜欢使用Contains().

在 MSDN 的IEnumerable.Contains()

使用默认相等比较器将元素与指定值进行比较

在 MSDN 的EqualityComparer<T>.Default属性上:

Default 属性检查类型 T 是否实现 System.IEquatable 泛型接口,如果是,则返回使用该实现的 EqualityComparer。否则,它将返回一个 EqualityComparer,该比较器使用 Object.Equals 和 Object.GetHashCode 由 T 提供的

覆盖。

据我了解,在我的类上实现IEquatable<T>应该意味着默认比较器在尝试查找匹配项时使用 Equals 方法。我想使用 Equals,因为我希望只有一种方式使两个对象相同,我不想要开发人员必须记住的策略。

我觉得奇怪的是,如果我将匹配目标包装在List中,然后执行Intersect,那么匹配就会正确找到。

我错过了什么?我是否也必须创建一个相等比较器,如 MSDN 文章?MSDN 建议拥有IEquatable就足够了,它会为我包装它。

示例控制台应用

注意:来自Jon Skeet的GetHashCode()在这里

using System;
using System.Collections.Generic;
using System.Linq;
namespace ContainsNotDoingWhatIThoughtItWould
{
    class Program
    {
        public class MyEquatable : IEquatable<MyEquatable>
        {
            string[] tags;
            public MyEquatable(params string[] tags)
            {
                this.tags = tags;
            }
            public bool Equals(MyEquatable other)
            {
                if (other == null)
                {
                    return false;
                }
                if (this.tags.Count() != other.tags.Count())
                {
                    return false;
                }
                var commonTags = this.tags.Intersect(other.tags);
                return commonTags.Count() == this.tags.Count();
            }
            public override int GetHashCode()
            {
                int hash = 17;
                foreach (string element in this.tags.OrderBy(x => x))
                {
                    hash = unchecked(hash * element.GetHashCode());
                }
                return hash;
            }
        }
        static void Main(string[] args)
        {
            // Two objects for the search list
            var a = new MyEquatable("A");
            var ab = new MyEquatable("A", "B");
            IEnumerable<MyEquatable> myList = new MyEquatable[] 
            { 
                a, 
                ab 
            };
            // This is the MyEquatable that we want to find
            var target = new MyEquatable("A", "B");
            // Check that the equality and hashing works
            var isTrue1 = target.GetHashCode() == ab.GetHashCode();
            var isTrue2 = target.Equals(ab);

            var isFalse1 = target.GetHashCode() == a.GetHashCode();
            var isFalse2 = target.Equals(a);
            // Why is this false?
            var whyIsThisFalse = myList.Contains(target);
            // If that is false, why is this true?
            var wrappedChildTarget = new List<MyEquatable> { target };
            var thisIsTrue = myList.Intersect(wrappedChildTarget).Any();
        }
    }
}

.NET 4.5 小提琴示例

为什么 Contains() 返回 false 但包装在一个列表中,而 Intersect() 返回 true

好的 - 问题实际上出在ICollection<T>.Contains数组实现中。您可以像这样简单地看到:

static void Main(string[] args)
{
    var ab = new MyEquatable("A", "B");
    var target = new MyEquatable("A", "B");
    var array = new[] { ab };
    Console.WriteLine(array.Contains(target)); // False
    var list = new List<MyEquatable> { ab };
    Console.WriteLine(list.Contains(target));  // True
    var sequence = array.Select(x => x);
    Console.WriteLine(sequence.Contains(target)); // True
}

Enumerable.Contains委托ICollection<T>.Contains源代码是否实现ICollection<T>,这就是为什么你在代码中获得数组行为而不是Enumerable.Contains"长手"实现的原因。

现在ICollection<T>.Contains确实说由实现来选择使用哪个比较器:

实现在确定对象相等性的方式上可能有所不同;例如,List<T>使用 Comparer<T>.Default ,而Dictionary<TKey, TValue>允许用户指定用于比较键的IComparer<T>实现。

但:

  • 该文档已经损坏,因为它应该谈论EqualityComparer<T>IEqualityComparer<T>,而不是Comparer<T>IEqualityComparer<T>
  • 数组决定使用既没有明确指定也没有默认EqualityComparer<T>的比较器对我来说似乎非常不自然。

解决方案是覆盖object.Equals(object)

public override bool Equals(object other)
{
    return Equals(other as MyEquatable);
}
为了

保持一致性,同时实现IEquatable<T>覆盖object.Equals(object)通常是令人愉快的。因此,虽然您的代码在我看来应该已经可以工作