为什么Contains()返回false,但将其包装在列表中并使用Intersect()返回true?

8

问题:

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

关于MSDN上的IEnumerable.Contains()

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

关于EqualityComparer<T>.Default属性来自MSDN

默认属性检查类型T是否实现了System.IEquatable泛型接口,如果是,则返回使用该实现的EqualityComparer。否则,它将返回一个使用T提供的Object.Equals和Object.GetHashCode覆盖的EqualityComparer。
据我所知,在我的类上实现IEquatable应该意味着默认比较器在尝试查找匹配项时使用Equals方法。我想使用Equals,因为我希望只有一种方式可以使两个对象相同,我不希望开发人员记住要放入策略。
我觉得奇怪的是,如果我将匹配目标包装在List中,然后执行Intersect,那么匹配就会被正确找到。
我错过了什么?我也需要创建一个相等比较器吗,就像MSDN文章中所述?MSDN建议拥有IEquatable就足够了,它会为我包装它。
示例控制台应用程序:
注意:GetHashCode() 来自 Jon Skeet 这里
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 Fiddle 示例

1个回答

7
好的 - 实际上问题出在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
}

如果源实现了`ICollection`,则`Enumerable.Contains`会委托给`ICollection.Contains`,这就是为什么您在代码中获取了数组行为而不是`Enumerable.Contains`“长手写”实现的原因。
现在,如果使用方确定相等性的方法,则`ICollection.Contains`会使用该实现:
实现可以因确定对象的相等性方式而异;例如,`List`使用`Comparer.Default`,而`Dictionary`允许用户指定用于比较键的`IComparer`实现。
但是:
- 该文档已经有误,应该谈论`EqualityComparer`和`IEqualityComparer`,而不是`Comparer`和`IEqualityComparer` - 我认为数组使用既未明确指定也不是默认的`EqualityComparer`来比较是非常不自然的。
解决方案是重写`object.Equals(object)`:
public override bool Equals(object other)
{
    return Equals(other as MyEquatable);
}

通常情况下,为了保持一致性,实现IEquatable<T>和重写object.Equals(object)都是很愉快的事情。因此,在我看来,您的代码应该已经可以正常工作了。


在我看来,这是一个错误,可能是在文档中或者实现上出了问题,不是吗? - Tim Schmelter
谢谢!这个解决方案在我的玩具问题和企业级应用程序问题中都起作用,它最初是在那里被发现的。 - Dr Rob Lang

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