清单&LT; T&GT;。载和T []包含行为不同。 [英] List<T>.Contains and T[].Contains behaving differently
问题描述
说我有这个类:
公共类动物:IEquatable&LT;动物&GT;
{
公共字符串名称{;组; } 公共布尔等于(动物等)
{
返回Name.Equals(other.Name);
}
公众覆盖布尔等于(obj对象)
{
回报等于((动物)OBJ);
}
公共覆盖INT GetHash code()
{
返回名称== NULL? 0:Name.GetHash code();
}
}
这是测试:
VAR动物=新[] {新的动物{名称=弗雷德}};
现在,当我这样做:
animals.ToList()包含(新动物{名称=弗雷德});
它调用合适的通用等于
的过载。问题是与数组类型。假设我做的:
animals.Contains(新动物{名称=弗雷德});
它调用的非普通等于
的方法。其实 T []
不公开的ICollection&LT; T&GT;。载
方法。在上述情况下的IEnumerable&LT;动物&GT;。载
扩展超载被称为这反过来又调用的ICollection&LT; T&GT;。载
。下面是如何的IEnumerable&LT; T&GT;。载
实施
公共静态布尔包含&LT; TSource&GT;(这个IEnumerable的&LT; TSource&GT;源,TSource值)
{
ICollection的&LT; TSource&GT;集合=源作为的ICollection&LT; TSource取代;
如果(集合!= NULL)
{
返回collection.Contains(值); //这是它得到了数组做
}
返回source.Contains(值null);
}
所以我的问题是:
- 为什么的应的
列表&LT; T&GT;。载
和T []包含
不同的表现?换句话说,为什么前者调用的通用等于
的和的后者非通用等于
的即使两个集合是通用? - 有没有一种方法,我可以看到
T []包含
的实施?
编辑:为何重要或者为什么我问这个:
-
据车次一起来的情况下,她忘了覆盖的非普通
等于
实施时的IEquatable&LT; T&GT ;
在这种情况下调用,例如T []包含
做了参考平等检查。尤其是当她预计的所有泛型集合的所操作的通用等于
的 -
您失去实施的所有优点
IEquatable&LT; T&GT;
(即使它不是引用类型灾难) -
正如评论指出的那样,只是想知道的内部细节和设计选择。有没有其他一般的情况下,我能想到的,其中的非普通
等于
的将是preferred,无论是任何列表&LT; T&GT;
或一套基于(词典&LT; K,V&GT;
等)操作。更糟的是,<一个href=\"http://stackoverflow.com/questions/19888123/t-contains-for-struct-and-class-behaving-differently\">had动物是一个结构,动物[]。包含调用的通用等于
的,所有这些使得T []实现还挺奇怪的,事情开发人员应该知道。
注意: 通用版等于
只调用当类的工具 IEquatable&LT; T&GT;
的如果类没有实现 IEquatable&LT; T&GT;
,非泛型重载等于
被称为不管它被称为由列表&LT; T&GT;。载
或 T []包含
。
数组不执行的IList&LT; T&GT;
,因为它们可以是多维的和非基于零
然而,在具有下限为零自动实施运行一维数组的IList&LT; T&GT;
和其他一些通用接口。此运行黑客的目的是在2报价详述如下。
下面 http://msdn.microsoft.com/en-us /library/vstudio/ms228502.aspx 它说:
在C#2.0及更高版本,一维数组有一个下界
零自动实施的IList&LT; T&GT;
。这使您可以创建
可以使用相同的code到数组迭代泛型方法
和其他集合类型。这种技术是对主要有用
在集合中读取数据。在的IList&LT; T&GT;
接口不能用
添加或删除数组中的元素。一个会抛出异常,如果
您尝试调用一个的IList&LT; T&GT;
的方法,如RemoveAt移除
中的阵列上
这样的背景下。
块引用>杰弗里里希特在他的书中说:
在CLR开发团队不想
的System.Array
实施的IEnumerable&LT; T&GT;
,
的ICollection&LT; T&GT; T&GT;
,不过,因为涉及到的问题,的IList&LT
多维数组和基于非零阵列。这些定义
上的System.Array接口将启用这些接口为所有
数组类型。相反,CLR执行一个小窍门:当
一维,零下限数组类型被创建时,CLR
自动进行阵列式实施的IEnumerable&LT; T&GT;
,
的ICollection&LT; T&GT;
和的IList&LT; T&GT;
(其中T
是数组的元素类型)和
也实现了三个接口对于所有的阵列型的碱
类型,只要它们是引用类型。
块引用>挖掘更深, SZArrayHelper 是用于提供这种哈克的IList实现基于单渔政船零数组类。
下面是类描述:
<$p$p><$c$c>//----------------------------------------------------------------------------------------
//!阅读此之前你工作这个类。
//
//这个类中的方法必须非常小心地写到避免引入安全漏洞。
//这是因为他们被调用的特殊本! 此对象
//对于所有这些方法都没有SZArrayHelper对象。相反,它们是U型的[]
//其中U []是强制转换为T []。没有实际SZArrayHelper对象已经被实例化。因此,你会
//看到很多前pressions那投这个的T []。
//
//需要这个类,允许类型的数组SZ T []揭露的IList&LT; T&GT ;,
//的IList&LT; T.BaseType&gt;等,等一路攀升到IList&LT;对象&gt ;.当下面的调用是
//做:
//
//((IList的&LT; T&GT;)(新U [N]))。SomeIListMethod()
//
//接口存根调度员将此视为一个特殊的情况下,加载了SZArrayHelper,
//找到相应的通用法(方法的名称简单匹配),实例化
//它的类型&lt; T&GT;并执行。
//
//将T将反映用来调用该方法的接口。实际运行本将是
//数组,它是可浇注为T [](即,用于primitivs和值类型,这将是准确
//T [] - 为orefs,它可能是一个U [],其中,U从T中派生)
// ------------------------------------------------ ----------------------------------------
块引用>和包含实现:
布尔包含&LT; T&GT;(T值){
//!警告:这是一个数组,而不是一个SZArrayHelper。见上述评论
//!或者您可能会引入安全漏洞!
T [] = _this以此为T [];
BCLDebug.Assert(!_this = NULL,这应该是一个T []);
返回Array.IndexOf(_this,值)!= -1;
}
块引用>所以我们称之为下面的方法
公共静态INT的IndexOf&LT; T&GT;(T []数组,T价值,诠释了startIndex,诠释计数){
...
返回EqualityComparer&LT; T&GT; .Default.IndexOf(数组值,则startIndex,计数);
}到目前为止好。但是,现在我们得到的最好奇/车的部分。
考虑下面的例子(根据你的跟进问题)
公共结构DummyStruct:IEquatable&LT; DummyStruct&GT;
{
公共字符串名称{;组; } 公共布尔等于(DummyStruct等)//&LT; - 他是男人
{
返回名称== other.Name;
}
公众覆盖布尔等于(obj对象)
{
抛出新的InvalidOperationException异常(不应该叫,因为我们使用通用的相等比较);
}
公共覆盖INT GetHash code()
{
返回名称== NULL? 0:Name.GetHash code();
}
}公共类DummyClass:IEquatable&LT; DummyClass&GT;
{
公共字符串名称{;组; } 公共布尔等于(其他DummyClass)
{
返回名称== other.Name;
}
公众覆盖布尔等于(obj对象)
{
抛出新的InvalidOperationException异常(不应该叫,因为我们使用通用的相等比较);
}
公共覆盖INT GetHash code()
{
返回名称== NULL? 0:Name.GetHash code();
}
}我栽种例外抛出两个非
IEquatable&LT; T&GT; .Equals()
实施令人惊讶的是:
DummyStruct [] =结构新的[] {新DummyStruct {名称=弗雷德}};
DummyClass [] =类新的[] {新DummyClass {名称=弗雷德}}; Array.IndexOf(结构,新DummyStruct {名称=弗雷德});
Array.IndexOf(类,新DummyClass {名称=弗雷德});这code不抛出任何异常。我们直接获取到IEquatable等于执行!
但是,当我们尝试以下code:
structs.Contains(新DummyStruct {名称=弗雷德});
classes.Contains(新DummyClass {名称=弗雷德}); //&LT; -throws例外,因为它调用的Object.Equals方法第二行抛出异常,具有以下堆栈跟踪:
DummyClass.Equals(obj对象)在
System.Collections.Generic.ObjectEqualityComparer`1.IndexOf(T []数组,T值,的Int32的startIndex,的Int32计数)
在System.Array.IndexOf(T []数组,T值)
System.SZArrayHelper.Contains(T值)
块引用>现在的错误?这里还是最大的问题是我们如何得ObjectEqualityComparer从我们DummyClass它的确实现了
IEquatable&LT; T&GT;
由于以下code:
VAR T = EqualityComparer&LT; DummyStruct&GT; .DEFAULT;
Console.WriteLine(t.GetType());
VAR T2 = EqualityComparer&LT; DummyClass&GT; .DEFAULT;
Console.WriteLine(t2.GetType());生成
System.Collections.Generic.GenericEqualityComparer
1 [DummyStruct]
1 [DummyClass]
System.Collections.Generic.GenericEqualityComparer
块引用>都使用GenericEqualityComparer,要求IEquatable方法。
在下面的方法CreateComparer事实上默认的比较呼叫:私有静态EqualityComparer&LT; T&GT; CreateComparer()
{
RuntimeType C =(RuntimeType)的typeof(T);
如果(C == typeof运算(字节))
{
返回(EqualityComparer&LT; T&GT;)新ByteEqualityComparer();
}
如果(typeof运算(IEquatable&LT; T&GT;)IsAssignableFrom(C))
{
回报(EqualityComparer&LT; T&GT;)RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof运算(GenericEqualityComparer&LT; INT&GT;),C);
} //相关部分
如果(c.IsGenericType&放大器;及(c.GetGenericTypeDefinition()== typeof运算(可为空&所述;&GT)))
{
RuntimeType TYPE2 =(RuntimeType)c.GetGenericArguments()[0];
如果(typeof运算(IEquatable&LT;方式&gt;)MakeGenericType(新类型[] {}类型2)IsAssignableFrom(2型))
{
回报(EqualityComparer&LT; T&GT;)RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof运算(NullableEqualityComparer&LT; INT&GT;),TYPE2);
}
}
如果(c.IsEnum&放大器;及(Enum.GetUnderlyingType(三)== typeof运算(INT)))
{
回报(EqualityComparer&LT; T&GT;)RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof运算(EnumEqualityComparer&LT; INT&GT;),C);
}
返回新ObjectEqualityComparer&LT; T&GT;(); //好奇PART
}好奇的部分加粗。显然对于DummyClass与包含我们到了最后一行,并没有通过
typeof运算(IEquatable).IsAssignableFrom(C)
块引用>检查!
为什么不呢?嗯,我想它不是一个bug或实施细节,这为不同由于SZArrayHelper描述类以下行的结构:
在T将反映用来调用该方法的接口。实际运行本将是数组,它是强制转换为T [](即用于primitivs和值类型,这将是>> 正是T [] - 为orefs,它可能是一个U []其中U在T派生)
块引用>
块引用>因此,我们知道现在几乎一切。唯一的问题,这是剩下的就是怎么弄ü没有通过
?的typeof(IEquatable&LT; T&GT;)IsAssignableFrom(C)
检查PS:更准确,SZArrayHelper包含执行code是SSCLI20。如此看来,目前实现已更改,导致反射显示了该方法如下:
私人布尔包含&LT; T&GT;(T值)
{
返回(Array.IndexOf&LT; T&GT;(JitHelpers.UnsafeCast&LT; T []&GT;!(本),价值)= -1);
}以下从dotnetframework.org
code JitHelpers.UnsafeCast节目静态内部温度T UnsafeCast&LT; T&GT;(对象o)其中T:类
{
//这个函数的主体将由EE代替不安全code,它只是返回Ò!
//见getILIntrinsicImplementation对于如何发生的。
回复O为T;
}现在我想大约三个感叹号和究竟怎么了发生在那个神秘的
getILIntrinsicImplementation
。Say I have this class:
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(); } }
This is the test:
var animals = new[] { new Animal { Name = "Fred" } };
Now, when I do:
animals.ToList().Contains(new Animal { Name = "Fred" });
it calls the right generic
Equals
overload. The problem is with array types. Suppose I do:animals.Contains(new Animal { Name = "Fred" });
it calls non generic
Equals
method. ActuallyT[]
doesn't exposeICollection<T>.Contains
method. In the above caseIEnumerable<Animal>.Contains
extension overload is called which in turn calls theICollection<T>.Contains
. Here is howIEnumerable<T>.Contains
is implemented: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); }
So my questions are:
- Why should
List<T>.Contains
andT[].Contains
behave differently? In other words, why is former calling the genericEquals
and the latter non-genericEquals
even though both the collections are generic?- Is there a way I can see
T[].Contains
implementation?Edit: Why does it matter or why am I asking this:
It trips one up in case she forgets to override non generic
Equals
when implementingIEquatable<T>
in which case calls likeT[].Contains
does a referential equality check. Especially when she expects all generic collections to operate on genericEquals
.You lose all the benefits of implementing
IEquatable<T>
(even though it isn't a disaster for reference types).As noted in comments, just interested in knowing the internal details and design choices. There is no other generic situation I can think of where the non generic
Equals
will be preferred, be it anyList<T>
or set based (Dictionary<K,V>
etc) operations. Even worse, had Animal been a struct, Animal[].Contains calls the genericEquals
, all which makes T[] implementation kinda strange, something developers ought to know.Note: The generic version of
Equals
is called only when the class implementsIEquatable<T>
. If the class doesn't implementIEquatable<T>
, non-generic overload ofEquals
is called irrespective of whether it is called byList<T>.Contains
orT[].Contains
.解决方案Arrays do not implement
IList<T>
because they can be multidimensional and non-zero based.However at runtime single-dimensional arrays that have a lower bound of zero automatically implement
IList<T>
and some other generic interfaces. The purpose of this runtime hack is elaborated below in 2 quotes.Here http://msdn.microsoft.com/en-us/library/vstudio/ms228502.aspx it says:
In C# 2.0 and later, single-dimensional arrays that have a lower bound of zero automatically implement
IList<T>
. This enables you to create generic methods that can use the same code to iterate through arrays and other collection types. This technique is primarily useful for reading data in collections. TheIList<T>
interface cannot be used to add or remove elements from an array. An exception will be thrown if you try to call anIList<T>
method such asRemoveAt
on an array in this context.Jeffrey Richter in his book says:
The CLR team didn’t want
System.Array
to implementIEnumerable<T>
,ICollection<T>
, andIList<T>
, though, because of issues related to multi-dimensional arrays and non-zero–based arrays. Defining these interfaces on System.Array would have enabled these interfaces for all array types. Instead, the CLR performs a little trick: when a single-dimensional, zero–lower bound array type is created, the CLR automatically makes the array type implementIEnumerable<T>
,ICollection<T>
, andIList<T>
(whereT
is the array’s element type) and also implements the three interfaces for all of the array type’s base types as long as they are reference types.Digging deeper, SZArrayHelper is the class that provides this "hacky" IList implementations for Single dimention Zero based arrays.
Here is the Class description:
//---------------------------------------------------------------------------------------- // ! 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.) //----------------------------------------------------------------------------------------
And Contains implementation:
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; }
So we call following method
public static int IndexOf<T>(T[] array, T value, int startIndex, int count) { ... return EqualityComparer<T>.Default.IndexOf(array, value, startIndex, count); }
So far so good. But now we get to the most curious/buggy part.
Consider following example (based on your follow up question)
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(); } }
I have planted exception throws in both non
IEquatable<T>.Equals()
implementations.The surprise is:
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" });
This code doesn't throw any exceptions. We get directly to the IEquatable Equals implementation!
But when we try the following code:
structs.Contains(new DummyStruct {Name = "Fred"}); classes.Contains(new DummyClass { Name = "Fred" }); //<-throws exception, since it calls object.Equals method
Second line throws exception, with following stacktrace:
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 value)
Now the bug? or Big Question here is how we got to ObjectEqualityComparer from our DummyClass which does implement
IEquatable<T>
?Because the following code:
var t = EqualityComparer<DummyStruct>.Default; Console.WriteLine(t.GetType()); var t2 = EqualityComparer<DummyClass>.Default; Console.WriteLine(t2.GetType());
Produces
System.Collections.Generic.GenericEqualityComparer
1[DummyStruct] System.Collections.Generic.GenericEqualityComparer
1[DummyClass]Both use GenericEqualityComparer, which calls IEquatable method. In fact Default comparer calls following CreateComparer method:
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 }
The curious parts are bolded. Evidently for DummyClass with Contains we got to last line, and didn't pass
typeof(IEquatable).IsAssignableFrom(c)
check!
Why not? well I guess its either a bug or implementation detail, which differs for structs because of the following line in SZArrayHelper description class:
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.)
So we know almost everything now. The only question, which is left, is how comes U doesn't pass
typeof(IEquatable<T>).IsAssignableFrom(c)
check?PS: to be more accurate, SZArrayHelper Contains implementation code is from SSCLI20. It seems that currently implementation has changed, cause reflector shows the following for this method:
private bool Contains<T>(T value) { return (Array.IndexOf<T>(JitHelpers.UnsafeCast<T[]>(this), value) != -1); }
JitHelpers.UnsafeCast shows following code from 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; }
Now I wonder about three exclamation marks and how exactly it happens in that mysterious
getILIntrinsicImplementation
.这篇关于清单&LT; T&GT;。载和T []包含行为不同。的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!