12

这是对此的后续问题: List<T>.Contains 和 T[].Contains 表现不同

T[].ContainsT当是类和结构时表现不同。假设我有这个结构

public struct Animal : IEquatable<Animal>
{
    public string Name { get; set; }

    public bool Equals(Animal other) //<- he is the man
    {
        return Name == other.Name;
    }
    public override bool Equals(object obj)
    {
        return Equals((Animal)obj);
    }
    public override int GetHashCode()
    {
        return Name == null ? 0 : Name.GetHashCode();
    }
}

var animals = new[] { new Animal { Name = "Fred" } };

animals.Contains(new Animal { Name = "Fred" }); // calls Equals(Animal)

在这里,如我所料,通用Equals被正确地称为。

但如果是一

public class Animal : IEquatable<Animal>
{
    public string Name { get; set; }

    public bool Equals(Animal other)
    {
        return Name == other.Name;
    }
    public override bool Equals(object obj) //<- he is the man
    {
        return Equals((Animal)obj);
    }
    public override int GetHashCode()
    {
        return Name == null ? 0 : Name.GetHashCode();
    }
}

var animals = new[] { new Animal { Name = "Fred" } };

animals.Contains(new Animal { Name = "Fred" }); // calls Equals(object)

调用非泛型Equals,带走了实现 IEquatable 的好处。

为什么数组对and的调用Equals不同,即使这两个集合看起来都很通用struct[]class[]

数组的怪异是如此令人沮丧,以至于我正在考虑完全避免它...

注意:的通用版本Equals仅在结构实现时调用IEquatable<T>如果类型没有实现IEquatable<T>,则调用非泛型重载,Equals无论它是class还是struct

4

3 回答 3

4

看起来它实际上并不是最终被调用的 Array.IndexOf() 。查看源代码,如果是这种情况,我会期望 Equals(object) 在两种情况下都会被调用。通过查看调用 Equals 时的堆栈跟踪,可以更清楚地了解为什么您会得到您所看到的行为(值类型获取 Equals(Animal),但引用类型获取 Equals(object)。

这是值类型(struct Animal)的堆栈跟踪

at Animal.Equals(Animal other)
at System.Collections.Generic.GenericEqualityComparer`1.IndexOf(T[] array, T value, Int32 startIndex, Int32 count)
at System.Array.IndexOf[T](T[] array, T value, Int32 startIndex, Int32 count)
at System.Array.IndexOf[T](T[] array, T value)
at System.SZArrayHelper.Contains[T](T value)
at System.Linq.Enumerable.Contains[TSource](IEnumerable`1 source, TSource value) 

这是引用类型(对象动物)的堆栈跟踪

at Animal.Equals(Object obj)
at System.Collections.Generic.ObjectEqualityComparer`1.IndexOf(T[] array, T value, Int32 startIndex, Int32 count)
at System.Array.IndexOf[T](T[] array, T value, Int32 startIndex, Int32 count)
at System.Array.IndexOf[T](T[] array, T value)
at System.SZArrayHelper.Contains[T](T value)
at System.Linq.Enumerable.Contains[TSource](IEnumerable`1 source, TSource value)

从这里你可以看到被调用的不是 Array.IndexOf - 它是 Array.IndexOf[T]。该方法最终确实使用了平等比较器。在引用类型的情况下,它使用调用 Equals(object) 的 ObjectEqualityComparer。在值类型的情况下,它使用调用 Equals(Animal) 的 GenericEqualityComparer,大概是为了避免昂贵的装箱。

如果您在http://www.dotnetframework.org查看 IEnumerable 的源代码, 它的顶部有一个有趣的部分:

// Note that T[] : IList<t>, and we want to ensure that if you use
// IList<yourvaluetype>, we ensure a YourValueType[] can be used
// without jitting.  Hence the TypeDependencyAttribute on SZArrayHelper.
// This is a special hack internally though - see VM\compile.cpp.
// The same attribute is on IList<t> and ICollection<t>.
[TypeDependencyAttribute("System.SZArrayHelper")]

我不熟悉 TypeDependencyAttribute,但从评论中,我想知道是否有一些对 Array 来说特别的魔法。这可以解释如何通过 Array 的 IList.Contains 最终调用 IndexOf[T] 而不是 IndexOf。

于 2013-11-10T11:20:01.680 回答
0

我认为这是因为他们都使用自己的基本实现Equals

类继承Object.Equals实现身份相等,Structs继承ValueType.Equals实现值相等。

于 2013-11-10T09:44:47.577 回答
0

的主要目的IEquatable<T>是允许与通用结构类型进行合理有效的相等比较。它的IEquatable<T>.Equals((T)x)行为应该完全一样Equals((object)(T)x);,除了如果T是一个值类型,前者将避免后者需要的堆分配。尽管IEquatable<T>不限制T为结构类型,并且密封类在某些情况下可能会从使用它中获得轻微的性能优势,但类类型不能从该接口中获得几乎与结构类型一样多的好处。如果外部代码使用IEquatable<T>.Equals(T)而不是Equals(Object),但不应该关心使用哪种比较方法。因为IEquatable<T>与类一起使用的性能优势永远不会很大,所以知道它正在使用类类型的代码可能会决定检查该类型是否恰好实现所需的时间IEquatable<T>可能不会被接口可能提供的任何性能增益所弥补。

顺便提一下,如果 X 和 Y 是“普通”类,那么如果 X 或 Y 派生自另一个,则 X.Equals(Y) 可能合法地为真。此外,未密封类类型的变量可以合法地比较等于任何接口类型之一,无论该类是否实现该接口。通过比较,结构只能比较等于其自身类型的变量ObjectValueType、 或结构本身实现的接口。类类型实例可能与更广泛的变量类型“相等”这一事实意味着IEquatable<T>它们不像结构类型那样适用。

PS——数组之所以特殊还有另一个原因:它们支持类不能的协方差样式。给定

Dog Fido = new Dog();
Cat Felix = new Cat();
Animal[] meows = new Cat[]{Felix};

测试是完全合法的meows.Contains(Fido)。如果meows被替换为Animal[]or的实例Dog[],则新数组可能确实包含Fido; 即使不是,人们也可能合法地拥有某种未知类型的变量,Animal并想知道它是否包含在meows. 即使Cat实现IEquatable<Cat>了,尝试使用该IEquatable<Cat>.Equals(Cat)方法测试元素meows是否等于Fido也会失败,因为Fido无法转换为CatIEquatable<Cat>系统在可行和不可行时可能有一些方法可以使用Equals(Object),但这会增加很多复杂性,而且如果没有超过简单使用Equals(Object).

于 2013-11-11T19:26:40.173 回答