List <T> .Contains和T [].包含不同的行为

naw*_*fal 19 c# arrays contains list equals

说我有这个班:

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

这是测试:

var animals = new[] { new Animal { Name = "Fred" } };
Run Code Online (Sandbox Code Playgroud)

现在,当我这样做时:

animals.ToList().Contains(new Animal { Name = "Fred" }); 
Run Code Online (Sandbox Code Playgroud)

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

animals.Contains(new Animal { Name = "Fred" });
Run Code Online (Sandbox Code Playgroud)

它调用非泛型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);
}
Run Code Online (Sandbox Code Playgroud)

所以我的问题是:

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

编辑:为什么这很重要或为什么我这样问:

  1. 如果她在执行时忘记覆盖非泛型EqualsIEquatable<T>,那么它会跳起来,在这种情况下调用就像T[].Contains引用相等性检查一样.特别是当她希望所有通用集合都使用泛型时Equals.

  2. 您将失去实施的所有好处IEquatable<T>(即使它不是参考类型的灾难).

  3. 正如评论中所述,只是对了解内部细节和设计选择感兴趣.没有其他一般情况我可以想到非泛型Equals将优先选择,无论是任何List<T>基于(或Dictionary<K,V>等)的操作.更糟糕的是,如果动物是一个结构,Animal [].包含调用泛型Equals,所有这些使T []实现有点奇怪,开发人员应该知道.

注意:Equals只有在类实现IEquatable<T>时才调用泛型版本.如果类没有实现IEquatable<T>,非一般的过载Equals被称为不论它是由被称为List<T>.ContainsT[].Contains.

Val*_*zub 9

数组不实现,IList<T>因为它们可以是多维的,非零的.

但是在运行时,具有零下限的单维数组会自动实现,IList<T>并且会有一些其他通用接口.这个运行时hack的目的在下面的2个引号中详细说明.

这里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.)
//----------------------------------------------------------------------------------------
Run Code Online (Sandbox Code Playgroud)

并包含实现:

    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;
    }
Run Code Online (Sandbox Code Playgroud)

所以我们称之为以下方法

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

到现在为止还挺好.但现在我们到了最好奇/越野车的部分.

请考虑以下示例(根据您的跟进问题)

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

我在两个非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" });
Run Code Online (Sandbox Code Playgroud)

此代码不会抛出任何异常.我们直接进入IEquatable Equals实现!

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

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

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

System.Array.IndexOf(T []数组,T值)处的System.Collections.Generic.ObjectEqualityComparer`1.IndexOf(T []数组,T值,Int32 startIndex,Int32计数)中的DummyClass.Equals(Object obj)at System.SZArrayHelper.Contains(T值)

现在的错误?或者大问题是我们如何从我们实现的DummyClass到ObjectEqualityComparer IEquatable<T>

因为以下代码:

var t = EqualityComparer<DummyStruct>.Default;
            Console.WriteLine(t.GetType());
            var t2 = EqualityComparer<DummyClass>.Default;
            Console.WriteLine(t2.GetType());
Run Code Online (Sandbox Code Playgroud)

产生

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

两者都使用GenericEqualityComparer,它调用IEquatable方法.事实上,默认比较器调用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
}
Run Code Online (Sandbox Code Playgroud)

好奇的部分是粗体.很显然,对于包含Contains的DummyClass,我们得到了最后一行,并没有通过

typeof运算(IEquatable).IsAssignableFrom(c)中

校验!

为什么不?好吧,我猜它是一个bug或实现细节,由于SZArrayHelper描述类中的以下行,因结构而不同:

"T"将反映用于调用方法的接口.实际的运行时"this"将是可转换为"T []"的数组(即对于原始值和值类型,它将是>> 恰好"T []" - 对于orefs,它可能是"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);
}
Run Code Online (Sandbox Code Playgroud)

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;
    }
Run Code Online (Sandbox Code Playgroud)

现在我想知道三个惊叹号以及它是如何在那个神秘中发生的getILIntrinsicImplementation.