T []。包含struct和class的行为方式不同

时间:2013-11-10 09:23:08

标签: c# arrays generics struct equals

这是对此的后续问题: List<T>.Contains and T[].Contains behaving differently

T[].Contains是类和结构时,

T的行为有所不同。假设我有 struct

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)

在这里, generic 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的好处。

为什么Equalsstruct[]的数组调用class[]不同,即使这两个集合看起来都是通用的

阵列怪异是如此令人沮丧,以至于我想完全避免它...

注意:仅当struct 实现Equals时才调用IEquatable<T>的通用版本。如果类型未实现{{ 1}},调用IEquatable<T>的非泛型重载,无论它是Equals还是class

3 个答案:

答案 0 :(得分:4)

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

以下是值类型(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]。该方法确实最终使用Equality比较器。在引用类型的情况下,它使用调用Equals(object)的ObjectEqualityComparer。在值类型的情况下,它使用GenericEqualityComparer调用Equals(Animal),可能是为了避免昂贵的装箱。

如果你在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。这可以解释IndexOf [T]如何通过Array的IList.Contains调用而不是IndexOf。

答案 1 :(得分:0)

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

类继承实现身份相等的Object.EqualsStructs继承实现值相等的ValueType.Equals

答案 2 :(得分: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>与结构类型不同。{/ p>

PS - 阵列特殊的另一个原因是:它们支持一种类不可能的协方差风格。给定

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

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