20

说我有这门课:

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

    public bool Equals(Animal other)
    {
        return Name.Equals(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.ToList().Contains(new Animal { Name = "Fred" }); 

它调用了正确的泛型Equals重载。问题在于数组类型。假设我这样做:

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

它调用非泛型Equals方法。实际上T[]没有暴露ICollection<T>.Contains方法。在上述情况下,IEnumerable<Animal>.Contains扩展重载被调用,而扩展重载又调用ICollection<T>.Contains. 下面是如何IEnumerable<T>.Contains实现的:

public static bool Contains<TSource>(this IEnumerable<TSource> source, TSource value)
{
    ICollection<TSource> collection = source as ICollection<TSource>;
    if (collection != null)
    {
        return collection.Contains(value); //this is where it gets done for arrays
    }
    return source.Contains(value, null);
}

所以我的问题是:

  1. 为什么应该 List<T>.ContainsT[].Contains行为不同?换句话说,为什么前者调用泛型Equals后者调用非泛型Equals ,即使这两个集合都是泛型的
  2. 有没有办法我可以看到T[].Contains实施?

编辑:为什么重要或者我为什么要问这个:

  1. 如果她在实现时忘记覆盖非泛型EqualsIEquatable<T>,它就会跳闸,在这种情况下,调用就像T[].Contains引用相等检查一样。尤其是当她希望所有泛型集合都对genericEquals进行操作时。

  2. 你失去了实现的所有好处IEquatable<T>(即使它对引用类型来说并不是一场灾难)。

  3. 如评论中所述,只是对了解内部细节和设计选择感兴趣。没有其他通用情况我能想到非通用Equals的首选,无论是任何List<T>或基于集合(Dictionary<K,V>等)的操作。更糟糕的是,如果 Animal 是一个结构体, Animal[].Contains 调用了genericEquals,这一切使得 T[] 实现有点奇怪,开发人员应该知道的。

注意:的泛型版本Equals仅在类实现时调用IEquatable<T>如果类没有实现IEquatable<T>,则调用非泛型重载,Equals无论它是由List<T>.Contains还是调用T[].Contains

4

3 回答 3

11

数组没有实现IList<T>,因为它们可以是多维的和基于非零的。

然而,在运行时,下限为零的一维数组会自动实现IList<T>和其他一些通用接口。下面用 2 个引号详细说明了此运行时 hack 的目的。

这里http://msdn.microsoft.com/en-us/library/vstudio/ms228502.aspx它说:

在 C# 2.0 及更高版本中,下限为零的一维数组会自动实现IList<T>. 这使您能够创建可以使用相同代码遍历数组和其他集合类型的泛型方法。此技术主要用于读取集合中的数据。该IList<T>接口不能用于在数组中添加或删除元素。IList<T>如果您尝试在此上下文中调用诸如RemoveAt数组上的方法,则会引发异常。

杰弗里·里希特在他的书中说:

由于与多维数组和非零基数组相关的问题,CLR 团队不想System.Array实现IEnumerable<T>ICollection<T>和。IList<T>在 System.Array 上定义这些接口将为所有数组类型启用这些接口。相反,CLR 执行了一个小技巧:当创建一维、零下界数组类型时,CLR 自动使数组类型实现IEnumerable<T>ICollection<T>、 和IList<T>(其中T是数组的元素类型),并且还实现了以下三个接口:数组类型的所有基类型,只要它们是引用类型。

深入挖掘,SZArrayHelper是为基于零的单维数组提供这种“hacky”IList 实现的类。

这是类描述:

//----------------------------------------------------------------------------------------
// ! READ THIS BEFORE YOU WORK ON THIS CLASS.
// 
// The methods on this class must be written VERY carefully to avoid introducing security holes.
// That's because they are invoked with special "this"! The "this" object
// for all of these methods are not SZArrayHelper objects. Rather, they are of type U[]
// where U[] is castable to T[]. No actual SZArrayHelper object is ever instantiated. Thus, you will
// see a lot of expressions that cast "this" "T[]". 
//
// This class is needed to allow an SZ array of type T[] to expose IList<T>,
// IList<T.BaseType>, etc., etc. all the way up to IList<Object>. When the following call is
// made:
//
//   ((IList<T>) (new U[n])).SomeIListMethod()
//
// the interface stub dispatcher treats this as a special case, loads up SZArrayHelper,
// finds the corresponding generic method (matched simply by method name), instantiates
// it for type <T> and executes it. 
//
// The "T" will reflect the interface used to invoke the method. The actual runtime "this" will be
// array that is castable to "T[]" (i.e. for primitivs and valuetypes, it will be exactly
// "T[]" - for orefs, it may be a "U[]" where U derives from T.)
//----------------------------------------------------------------------------------------

并包含实现:

    bool Contains<T>(T value) {
        //! Warning: "this" is an array, not an SZArrayHelper. See comments above
        //! or you may introduce a security hole!
        T[] _this = this as T[];
        BCLDebug.Assert(_this!= null, "this should be a T[]");
        return Array.IndexOf(_this, value) != -1;
    }

所以我们调用以下方法

public static int IndexOf<T>(T[] array, T value, int startIndex, int count) {
    ...
    return EqualityComparer<T>.Default.IndexOf(array, value, startIndex, count);
}

到目前为止,一切都很好。但现在我们到了最好奇/最有问题的部分。

考虑以下示例(基于您的后续问题)

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

    public bool Equals(DummyStruct other) //<- he is the man
    {
        return Name == other.Name;
    }
    public override bool Equals(object obj)
    {
        throw new InvalidOperationException("Shouldn't be called, since we use Generic Equality Comparer");
    }
    public override int GetHashCode()
    {
        return Name == null ? 0 : Name.GetHashCode();
    }
}

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

    public bool Equals(DummyClass other)
    {
        return Name == other.Name;
    }
    public override bool Equals(object obj) 
    {
        throw new InvalidOperationException("Shouldn't be called, since we use Generic Equality Comparer");
    }
    public override int GetHashCode()
    {
        return Name == null ? 0 : Name.GetHashCode();
    }
}

IEquatable<T>.Equals()我在两个非实现中都设置了异常抛出。

惊喜是:

    DummyStruct[] structs = new[] { new DummyStruct { Name = "Fred" } };
    DummyClass[] classes = new[] { new DummyClass { Name = "Fred" } };

    Array.IndexOf(structs, new DummyStruct { Name = "Fred" });
    Array.IndexOf(classes, new DummyClass { Name = "Fred" });

此代码不会引发任何异常。我们直接进入 IEquatable Equals 实现!

但是当我们尝试以下代码时:

    structs.Contains(new DummyStruct {Name = "Fred"});
    classes.Contains(new DummyClass { Name = "Fred" }); //<-throws exception, since it calls object.Equals method

第二行抛出异常,堆栈跟踪如下:

DummyClass.Equals(Object obj) at System.Collections.Generic.ObjectEqualityComparer`1.IndexOf(T[] array, T value, Int32 startIndex, Int32 count) at System.Array.IndexOf(T[] array, T value) at System.SZArrayHelper.Contains(T 值)

现在的错误?或者这里的大问题是我们如何从我们的 DummyClass 得到 ObjectEqualityComparerIEquatable<T>呢?

因为下面的代码:

var t = EqualityComparer<DummyStruct>.Default;
            Console.WriteLine(t.GetType());
            var t2 = EqualityComparer<DummyClass>.Default;
            Console.WriteLine(t2.GetType());

生产

System.Collections.Generic.GenericEqualityComparer 1[DummyStruct] System.Collections.Generic.GenericEqualityComparer1[DummyClass]

两者都使用调用 IEquatable 方法的 GenericEqualityComparer。事实上,默认比较器调用以下 CreateComparer 方法:

private static EqualityComparer<T> CreateComparer()
{
    RuntimeType c = (RuntimeType) typeof(T);
    if (c == typeof(byte))
    {
        return (EqualityComparer<T>) new ByteEqualityComparer();
    }
    if (typeof(IEquatable<T>).IsAssignableFrom(c))
    {
        return (EqualityComparer<T>) RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType) typeof(GenericEqualityComparer<int>), c);
    } // RELEVANT PART
    if (c.IsGenericType && (c.GetGenericTypeDefinition() == typeof(Nullable<>)))
    {
        RuntimeType type2 = (RuntimeType) c.GetGenericArguments()[0];
        if (typeof(IEquatable<>).MakeGenericType(new Type[] { type2 }).IsAssignableFrom(type2))
        {
            return (EqualityComparer<T>) RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType) typeof(NullableEqualityComparer<int>), type2);
        }
    }
    if (c.IsEnum && (Enum.GetUnderlyingType(c) == typeof(int)))
    {
        return (EqualityComparer<T>) RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType) typeof(EnumEqualityComparer<int>), c);
    }
    return new ObjectEqualityComparer<T>(); // CURIOUS PART
}

奇怪的部分是粗体的。显然,对于带有 Contains 的 DummyClass,我们到了最后一行,但没有通过

typeof(IEquatable).IsAssignableFrom(c)

查看!

为什么不?好吧,我猜它要么是错误,要么是实现细节,由于 SZArrayHelper 描述类中的以下行,因此结构不同:

“T”将反映用于调用该方法的接口。实际运行时 "this" 将是可转换为 "T[]" 的数组(即对于原始类型和值类型,它将是 >>完全是 "T[]" - 对于 orefs,它可能是"U[]" 其中U 派生自 T。)

所以我们现在几乎什么都知道了。剩下的唯一问题是,U 怎么没有通过typeof(IEquatable<T>).IsAssignableFrom(c)检查?

PS:更准确地说, SZArrayHelper 包含的实现代码来自 SSCLI20。似乎当前的实现已经改变,因为反射器为此方法显示以下内容:

private bool Contains<T>(T value)
{
    return (Array.IndexOf<T>(JitHelpers.UnsafeCast<T[]>(this), value) != -1);
}

JitHelpers.UnsafeCast 显示来自 dotnetframework.org 的以下代码

   static internal T UnsafeCast<t>(Object o) where T : class
    {
        // The body of this function will be replaced by the EE with unsafe code that just returns o!!!
        // See getILIntrinsicImplementation for how this happens.
        return o as T;
    }

现在我想知道三个感叹号以及它究竟是如何发生在那个神秘的getILIntrinsicImplementation

于 2013-11-10T08:33:42.457 回答
1

Arrays 确实实现了通用接口IList<T>,但是实现是在运行时提供的ICollection<T>IEnumerable<T>因此文档构建工具不可见(这就是为什么您ICollection<T>.Contains在 的 msdn 文档中看不到的原因Array)。

我怀疑运行时实现只是调用IList.Contains(object)了数组已经拥有的非泛型。
因此Equals,您的类中的非泛型方法被调用。

于 2013-11-10T08:45:53.577 回答
0

Array 没有名称为 contains 的方法,这是 Enumerable 类的扩展方法。

Enumerable.Contains 方法,您在数组中使用该方法,

正在使用默认相等比较器

默认的相等比较器,需要重写 Object.Equality 方法。

这是因为向后兼容。

列表有自己特定的实现,但 Enumerable 应该与任何 Enumerable 兼容,从 .NET 1 到 .NET 4.5

祝你好运

于 2013-11-10T08:37:59.423 回答