List .Contains和T 。包含不同的行为
说我有这个class:
public class Animal : IEquatable { 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" });
它调用非genericsEquals
方法。 实际上T[]
不会暴露ICollection.Contains
方法。 在上面的例子中, IEnumerable.Contains
扩展重载被调用,后者又调用ICollection.Contains
。 以下是IEnumerable.Contains
的实现方式:
public static bool Contains(this IEnumerable source, TSource value) { ICollection collection = source as ICollection; if (collection != null) { return collection.Contains(value); //this is where it gets done for arrays } return source.Contains(value, null); }
所以我的问题是:
- 为什么要
List.Contains
和T[].Contains
行为有何不同? 换句话说,为什么前者调用genericsEquals
和后者非genericsEquals
即使两个集合都是通用的 ? - 有没有办法可以看到
T[].Contains
实现?
编辑:为什么这很重要或为什么我这样问:
-
如果在实现
IEquatable
时会忘记覆盖非genericsEquals
,IEquatable
它会IEquatable
在这种情况下调用类似T[].Contains
IEquatable
执行引用相等性检查。 特别是当她希望所有通用集合都能在通用Equals
上运行时。 -
您将失去实现
IEquatable
所有好处(即使它不是参考类型的灾难)。 -
正如评论中所述,只是对了解内部细节和设计选择感兴趣。 没有其他一般情况我可以想到非优选的
Equals
将是首选,无论是任何List
还是基于集合(Dictionary
etc)操作。 更糟糕的是, 动物是一个结构,Animal []。包含调用通用的Equals
,所有这些使T []实现有点奇怪,开发人员应该知道。
注意:仅当类实现IEquatable
时才调用Equals
的通用版本。 如果类没有实现IEquatable
,则调用Equals
非generics重载,无论它是由List.Contains
还是T[].Contains
调用T[].Contains
。
数组不实现IList
因为它们可以是多维的,非零的。
但是在运行时,具有零下限的单维数组会自动实现IList
和其他一些通用接口。 下面用2个引号详细说明了这个运行时hack的目的。
这里https://msdn.microsoft.com/en-us/library/vstudio/ms228502.aspx说:
在C#2.0及更高版本中,具有零下限的一维数组自动实现
IList
。 这使您可以创建可以使用相同代码迭代数组和其他集合类型的generics方法。 此技术主要用于读取集合中的数据。IList
接口不能用于添加或删除数组中的元素。 如果在此上下文中尝试在数组上调用IList
方法(如RemoveAt
,则会引发exception。
杰弗里里希特在他的书中说:
但是,由于与多维数组和非零数组相关的问题,CLR团队不希望
System.Array
实现IEnumerable
,ICollection
和IList
。 在System.Array上定义这些接口将为所有数组类型启用这些接口。 相反,CLR执行一个小技巧:当创建一维,零下界数组类型时,CLR自动使数组类型实现IEnumerable
,ICollection
和IList
(其中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
, // IList , etc., etc. all the way up to IList
并包含实现:
bool Contains
(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[] array, T value, int startIndex, int count) { ... return EqualityComparer .Default.IndexOf(array, value, startIndex, count); }
到现在为止还挺好。 但现在我们得到了最好奇/最多的部分。
考虑以下示例(基于您的后续问题)
public struct DummyStruct : IEquatable { 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 { 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
实现中都种植了exception抛出。
令人惊讶的是:
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" });
此代码不会抛出任何exception。 我们直接进入IEquatable Equals实现!
但是当我们尝试以下代码时:
structs.Contains(new DummyStruct {Name = "Fred"}); classes.Contains(new DummyClass { Name = "Fred" }); //<-throws exception, since it calls object.Equals method
第二行抛出exception,后面是stacktrace:
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
?
因为以下代码:
var t = EqualityComparer.Default; Console.WriteLine(t.GetType()); var t2 = EqualityComparer.Default; Console.WriteLine(t2.GetType());
产生
System.Collections.Generic.GenericEqualityComparer
1[DummyStruct] System.Collections.Generic.GenericEqualityComparer
1 [DummyClass]
两者都使用GenericEqualityComparer,它调用IEquatable方法。 事实上,默认比较器调用CreateComparer方法:
private static EqualityComparer CreateComparer() { RuntimeType c = (RuntimeType) typeof(T); if (c == typeof(byte)) { return (EqualityComparer ) new ByteEqualityComparer(); } if (typeof(IEquatable ).IsAssignableFrom(c)) { return (EqualityComparer ) RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType) typeof(GenericEqualityComparer), 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) RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType) typeof(NullableEqualityComparer), type2); } } if (c.IsEnum && (Enum.GetUnderlyingType(c) == typeof(int))) { return (EqualityComparer) RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType) typeof(EnumEqualityComparer), c); } return new ObjectEqualityComparer(); // CURIOUS PART }
好奇的部分是粗体。 很显然,对于包含Contains的DummyClass,我们得到了最后一行,并没有通过
typeof运算(IEquatable).IsAssignableFrom(c)中
校验!
为什么不? 好吧,我想它是一个bug或实现细节,由于SZArrayHelper描述类中的以下行,因结构而不同:
“T”将反映用于调用方法的接口。 实际运行时“this”将是可转换为“T []”的数组(即对于原始值和值类型,它将是>> 恰好“T []” - 对于orefs,它可能是“U []”,其中你来自T。 )
所以我们现在几乎知道一切。 剩下的唯一问题是U怎么没有传递typeof(IEquatable
检查?
PS:为了更准确,SZArrayHelper包含的实现代码来自SSCLI20。 似乎当前的实现已经改变,导致reflection器显示以下方法:
private bool Contains(T value) { return (Array.IndexOf (JitHelpers.UnsafeCast(this), value) != -1); }
JitHelpers.UnsafeCast显示以下来自dotnetframework.org的代码
static internal T UnsafeCast(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
, ICollection
和IEnumerable
但实现是在运行时提供的,因此文档构建工具不可见(这就是为什么你看不到ICollection
在Array
的msdn文档中。
我怀疑运行时实现只调用数组已有的非genericsIList.Contains(object)
。
因此,您的类中的非genericsEquals
方法被调用。
Array没有名称为contains的方法,这是Enumerable类的扩展方法。
Enumerable.Contains方法,您在数组中使用它,
正在使用默认的相等比较器 。
默认的相等比较器,需要重写Object.Equality方法。
这是因为向后兼容。
列表有自己的特定实现,但Enumerable应该与任何Enumerable兼容,从.NET 1到.NET 4.5
祝好运
上述就是C#学习教程:List .Contains和T 。包含不同的行为分享的全部内容,如果对大家有所用处且需要了解更多关于C#学习教程,希望大家多多关注---计算机技术网(www.ctvol.com)!
本文来自网络收集,不代表计算机技术网立场,如涉及侵权请联系管理员删除。
ctvol管理联系方式QQ:251552304
本文章地址:https://www.ctvol.com/cdevelopment/1010931.html