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

Posted

技术标签:

【中文标题】List<T>.Contains 和 T[].Contains 行为不同【英文标题】:List<T>.Contains and T[].Contains behaving differently 【发布时间】:2013-11-22 03:29:34 【问题描述】:

假设我有这门课:

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&lt;T&gt;.Contains 方法。在上述情况下,IEnumerable&lt;Animal&gt;.Contains 扩展重载被调用,而后者又调用ICollection&lt;T&gt;.Contains。以下是IEnumerable&lt;T&gt;.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);

所以我的问题是:

    为什么应该List&lt;T&gt;.ContainsT[].Contains 表现不同?换句话说,为什么前者调用 generic Equalslatter non-generic Equals 即使两个集合都是通用的 ? 有没有办法可以看到T[].Contains 的实现?

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

    如果她在实现IEquatable&lt;T&gt; 时忘记覆盖非泛型Equals,它会跳闸,在这种情况下,像T[].Contains 这样的调用会进行引用相等检查。尤其是当她希望所有泛型集合都对泛型Equals进行操作时。

    您将失去实现 IEquatable&lt;T&gt; 的所有好处(即使它对引用类型来说并不是一场灾难)。

    如 cmets 中所述,只对了解内部细节和设计选择感兴趣。没有其他通用情况我能想到 非通用 Equals 将是首选,无论是任何 List&lt;T&gt; 或基于集合(Dictionary&lt;K,V&gt; 等)的操作。更糟糕的是,had Animal been a struct, Animal[].Contains calls the generic Equals,这一切都让 T[] 实现有点奇怪,开发人员应该知道这一点。

注意:Equals 的泛型版本仅在类实现IEquatable&lt;T&gt; 时被调用。如果类没有实现IEquatable&lt;T&gt;,则非无论是由List&lt;T&gt;.Contains 还是T[].Contains 调用,都会调用Equals 的泛型重载。

【问题讨论】:

这就是你的问题,如果你实现IEquatable&lt;T&gt;接口,你必须覆盖Equals(object) GetHashCode()方法.你不能只实现一个而不是另一个,并期望事情按预期工作。 @JeffMercado 你是对的,但我希望看到一些内部细节和设计选择。没有其他 generic 情况我能想到 non generic Equals 将是首选,无论是任何 List&lt;T&gt; 或基于集合(Dictionary&lt;K,V&gt; 等)即使未实现 non generic Equals 也可以进行操作。好吧,所有这些都假设T[] 是通用的足够。更糟糕的是,如果 Animal 是一个结构,Animal[].Contains 调用 generic Equals 所有这使得 T[] 实现有点奇怪,开发人员应该知道这一点。我将更新我的问题以使其清楚。 【参考方案1】:

数组不实现IList&lt;T&gt;,因为它们可以是多维的且基于非零的。

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

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

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

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

CLR 团队不希望 System.Array 实现 IEnumerable&lt;T&gt;, 不过,ICollection&lt;T&gt;IList&lt;T&gt;,因为与 多维数组和非零基数组。定义这些 System.Array 上的接口将为所有用户启用这些接口 数组类型。相反,CLR 执行了一个小技巧:当 创建一维零下界数组类型,CLR 自动使数组类型实现IEnumerable&lt;T&gt;ICollection&lt;T&gt;IList&lt;T&gt;(其中 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&lt;T&gt;.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) 在 System.Collections.Generic.ObjectEqualityComparer`1.IndexOf(T[] 数组,T 值,Int32 startIndex,Int32 计数) 在 System.Array.IndexOf(T[] 数组,T 值) 在 System.SZArrayHelper.Contains(T 值)

现在的错误?或者这里的大问题是我们如何从实现 IEquatable&lt;T&gt; 的 DummyClass 获得 ObjectEqualityComparer?

因为下面的代码:

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

生产

System.Collections.Generic.GenericEqualityComparer1[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&lt;T&gt;).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 中的。

【讨论】:

那是IList.Contains(object)。我专门寻找ICollection&lt;T&gt;.Contains(T)。我想你还没有回答我的问题。 查看我的编辑。我不确定我们如何才能找出运行时实现的实际外观,但我们可以从它的行为中推断出来? 对于 OP 的代码,Array.IndexOf() 实际上并不是被调用的。相反,它是 Array.IndexOf[T](),它具有完全不同的行为。请参阅我对他后续问题的回答。 ***.com/questions/19888123/… @ValentinKuzub,我不想批评,但这个答案在很多层面上都是错误的。 1. T[] 数组中的元素存储为对象?一点也不。如果是这种情况,它将导致值类型 which doesn't happen 的装箱。 2. 你说如果它没有在实现 IEquatable 的类型上正确重载,则不调用IEquatable&lt;T&gt;.Equals,但我正确地实现了 i> IEquatable&lt;T&gt;.Equals,仍然是非通用的Equals 被调用。 ... 3. 正如斧头所说,Array.IndexOf 不是必然所谓的。我的意思是它可能是,但我们无法确定。对于结构,为T[]s 调用正确的泛型Equals。 See this question and answer.【参考方案2】:

Arrays 确实实现了通用接口 IList&lt;T&gt;ICollection&lt;T&gt;IEnumerable&lt;T&gt;,但实现是在运行时提供的,因此文档构建工具不可见(这就是为什么您在Array 的 msdn 文档)。 我怀疑运行时实现只是调用了数组已经拥有的非通用IList.Contains(object)。 因此,您的类中的非通用 Equals 方法被调用。

【讨论】:

@ValentinKuzub 是的,他们可以,您可以在Array documentation 的备注部分中看到它,但正如我在回答中所述,该实现是在运行时提供的。 @ValentinKuzub 我说的是问题中的数组。而(new[] new Animal Name = "Fred" ) is ICollection&lt;Animal&gt; 将返回true @Magnus T[] 调用泛型Equals 如果为T 实现IEquatable&lt;T&gt;,则在结构的情况下。所以我怀疑这是否真的是IList.Contains(object) 在幕后。源代码没有表明任何类型检查。看到这个问题:***.com/questions/19888123/… @nawfal 因为它是一个运行时实现,我们只能猜测发生了什么。【参考方案3】:

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

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

正在使用默认相等比较器

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

这是因为向后兼容。

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

祝你好运

【讨论】:

没有其他 generic 集合需要非泛型 EqualsT[] 除外。实现IEquatable&lt;T&gt; 的全部目的是避免对象的强制转换,T[] 不遵守类。如果是为了保持与前泛型时代的向后兼容性,为什么它对于实现泛型IEquatable&lt;T&gt; 的结构表现良好?看到这个:***.com/questions/19888123/… @nawfal 在这里查看我的评论:***.com/questions/19888123/…

以上是关于List<T>.Contains 和 T[].Contains 行为不同的主要内容,如果未能解决你的问题,请参考以下文章

android mono:使用 List<T> 而不是 ArrayAdapter 来使用 Contains 方法

在unity list.exist和contains的区别

List<object>.Contains 表达式树

Java List.contains(ArrayList<String> 字段值等于 x)

LINQ List.Contains() 重载以接受列名

List 与 Set 的 contains方法比较