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<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);
所以我的问题是:
-
为什么应该
List<T>.Contains
和T[].Contains
表现不同?换句话说,为什么前者调用 generic Equals
而 latter non-generic Equals
即使两个集合都是通用的 ?
有没有办法可以看到T[].Contains
的实现?
编辑:为什么重要或我为什么要问这个:
如果她在实现IEquatable<T>
时忘记覆盖非泛型Equals
,它会跳闸,在这种情况下,像T[].Contains
这样的调用会进行引用相等检查。尤其是当她希望所有泛型集合都对泛型Equals
进行操作时。
您将失去实现 IEquatable<T>
的所有好处(即使它对引用类型来说并不是一场灾难)。
如 cmets 中所述,只对了解内部细节和设计选择感兴趣。没有其他通用情况我能想到 非通用 Equals
将是首选,无论是任何 List<T>
或基于集合(Dictionary<K,V>
等)的操作。更糟糕的是,had Animal been a struct, Animal[].Contains calls the generic Equals
,这一切都让 T[] 实现有点奇怪,开发人员应该知道这一点。
注意:Equals
的泛型版本仅在类实现IEquatable<T>
时被调用。如果类没有实现IEquatable<T>
,则非无论是由List<T>.Contains
还是T[].Contains
调用,都会调用Equals
的泛型重载。
【问题讨论】:
这就是你的问题,如果你实现IEquatable<T>
接口,你必须覆盖Equals(object)
和 GetHashCode()
方法.你不能只实现一个而不是另一个,并期望事情按预期工作。
@JeffMercado 你是对的,但我希望看到一些内部细节和设计选择。没有其他 generic 情况我能想到 non generic Equals
将是首选,无论是任何 List<T>
或基于集合(Dictionary<K,V>
等)即使未实现 non generic Equals
也可以进行操作。好吧,所有这些都假设T[]
是通用的足够。更糟糕的是,如果 Animal
是一个结构,Animal[].Contains
调用 generic Equals
所有这使得 T[]
实现有点奇怪,开发人员应该知道这一点。我将更新我的问题以使其清楚。
【参考方案1】:
数组不实现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) 在 System.Collections.Generic.ObjectEqualityComparer`1.IndexOf(T[] 数组,T 值,Int32 startIndex,Int32 计数) 在 System.Array.IndexOf(T[] 数组,T 值) 在 System.SZArrayHelper.Contains(T 值)
现在的错误?或者这里的大问题是我们如何从实现 IEquatable<T>
的 DummyClass 获得 ObjectEqualityComparer?
因为下面的代码:
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.GenericEqualityComparer
1[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
中的。
【讨论】:
那是IList.Contains(object)
。我专门寻找ICollection<T>.Contains(T)
。我想你还没有回答我的问题。
查看我的编辑。我不确定我们如何才能找出运行时实现的实际外观,但我们可以从它的行为中推断出来?
对于 OP 的代码,Array.IndexOf() 实际上并不是被调用的。相反,它是 Array.IndexOf[T](),它具有完全不同的行为。请参阅我对他后续问题的回答。 ***.com/questions/19888123/…
@ValentinKuzub,我不想批评,但这个答案在很多层面上都是错误的。 1. T[]
数组中的元素存储为对象?一点也不。如果是这种情况,它将导致值类型 which doesn't happen 的装箱。 2. 你说如果它没有在实现 IEquatableIEquatable<T>.Equals
IEquatable<T>.Equals
,仍然是非通用的Equals
被调用。 ...
3. 正如斧头所说,Array.IndexOf
不是必然所谓的。我的意思是它可能是,但我们无法确定。对于结构,为T[]
s 调用正确的泛型Equals
。 See this question and answer.【参考方案2】:
Arrays 确实实现了通用接口 IList<T>
、ICollection<T>
和 IEnumerable<T>
,但实现是在运行时提供的,因此文档构建工具不可见(这就是为什么您在Array
的 msdn 文档)。
我怀疑运行时实现只是调用了数组已经拥有的非通用IList.Contains(object)
。
因此,您的类中的非通用 Equals
方法被调用。
【讨论】:
@ValentinKuzub 是的,他们可以,您可以在Array documentation 的备注部分中看到它,但正如我在回答中所述,该实现是在运行时提供的。 @ValentinKuzub 我说的是问题中的数组。而(new[] new Animal Name = "Fred" ) is ICollection<Animal>
将返回true
@Magnus T[]
调用泛型Equals
如果为T
实现IEquatable<T>
,则在结构的情况下。所以我怀疑这是否真的是IList.Contains(object)
在幕后。源代码没有表明任何类型检查。看到这个问题:***.com/questions/19888123/…
@nawfal 因为它是一个运行时实现,我们只能猜测发生了什么。【参考方案3】:
Array 没有名称为 contains 的方法,这是 Enumerable 类的扩展方法。
Enumerable.Contains 方法,您在数组中使用该方法,
正在使用默认相等比较器。
默认的相等比较器,需要重写 Object.Equality 方法。
这是因为向后兼容。
列表有自己特定的实现,但 Enumerable 应该与任何 Enumerable 兼容,从 .NET 1 到 .NET 4.5
祝你好运
【讨论】:
没有其他 generic 集合需要非泛型Equals
,T[]
除外。实现IEquatable<T>
的全部目的是避免对象的强制转换,T[]
不遵守类。如果是为了保持与前泛型时代的向后兼容性,为什么它对于实现泛型IEquatable<T>
的结构表现良好?看到这个:***.com/questions/19888123/…
@nawfal 在这里查看我的评论:***.com/questions/19888123/…以上是关于List<T>.Contains 和 T[].Contains 行为不同的主要内容,如果未能解决你的问题,请参考以下文章
android mono:使用 List<T> 而不是 ArrayAdapter 来使用 Contains 方法