清单< T>。载和T []包含行为不同。 [英] List<T>.Contains and T[].Contains behaving differently

查看:158
本文介绍了清单< T>。载和T []包含行为不同。的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

说我有这个类:

 公共类动物:IEquatable<动物>
{
    公共字符串名称{;组; }    公共布尔等于(动物等)
    {
        返回Name.Equals(other.Name);
    }
    公众覆盖布尔等于(obj对象)
    {
        回报等于((动物)OBJ);
    }
    公共覆盖INT GetHash code()
    {
        返回名称== NULL? 0:Name.GetHash code();
    }
}

这是测试:

  VAR动物=新[] {新的动物{名称=弗雷德}};

现在,当我这样做:

  animals.ToList()包含(新动物{名称=弗雷德});

它调用合适的通用等于 的过载。问题是与数组类型。假设我做的:

  animals.Contains(新动物{名称=弗雷德});

它调用的非普通等于 的方法。其实 T [] 不公开的ICollection< T>。载方法。在上述情况下的IEnumerable<动物>。载扩展超载被称为这反过来又调用的ICollection< T>。载。下面是如何的IEnumerable< T>。载实施

 公共静态布尔包含< TSource>(这个IEnumerable的< TSource>源,TSource值)
{
    ICollection的< TSource>集合=源作为的ICollection< TSource取代;
    如果(集合!= NULL)
    {
        返回collection.Contains(值); //这是它得到了数组做
    }
    返回source.Contains(值null);
}

所以我的问题是:


  1. 为什么的列表< T>。载 T []包含不同的表现?换句话说,为什么前者调用的通用等于 的和的后者非通用等于 即使两个集合是通用

  2. 有没有一种方法,我可以看到 T []包含的实施?

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


  1. 据车次一起来的情况下,她忘了覆盖的非普通等于实施时 IEquatable< T&GT ; 在这种情况下调用,例如 T []包含做了参考平等检查。尤其是当她预计的所有泛型集合的所操作的通用等于


  2. 您失去实施的所有优点 IEquatable< T> (即使它不是引用类型灾难)


  3. 正如评论指出的那样,只是想知道的内部细节和设计选择。有没有其他一般的情况下,我能想到的,其中的非普通等于 的将是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; D​​ummyStruct&GT;
{
    公共字符串名称{;组; }    公共布尔等于(DummyStruct等)//&LT; - 他是男人
    {
        返回名称== other.Name;
    }
    公众覆盖布尔等于(obj对象)
    {
        抛出新的InvalidOperationException异常(不应该叫,因为我们使用通用的相等比较);
    }
    公共覆盖INT GetHash code()
    {
        返回名称== NULL? 0:Name.GetHash code();
    }
}公共类DummyClass:IEquatable&LT; D​​ummyClass&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; D​​ummyStruct&GT; .DEFAULT;
            Console.WriteLine(t.GetType());
            VAR T2 = EqualityComparer&LT; D​​ummyClass&GT; .DEFAULT;
            Console.WriteLine(t2.GetType());

生成


  

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


都使用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. Actually T[] doesn't expose ICollection<T>.Contains method. In the above case IEnumerable<Animal>.Contains extension overload is called which in turn calls the ICollection<T>.Contains. Here is how IEnumerable<T>.Containsis 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:

  1. Why should List<T>.Contains and T[].Contains behave differently? In other words, why is former calling the generic Equals and the latter non-generic Equals even though both the collections are generic?
  2. Is there a way I can see T[].Contains implementation?

Edit: Why does it matter or why am I asking this:

  1. It trips one up in case she forgets to override non generic Equals when implementing IEquatable<T> in which case calls like T[].Contains does a referential equality check. Especially when she expects all generic collections to operate on generic Equals.

  2. You lose all the benefits of implementing IEquatable<T> (even though it isn't a disaster for reference types).

  3. 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 any List<T> or set based (Dictionary<K,V> etc) operations. Even worse, had Animal been a struct, Animal[].Contains calls the generic Equals, all which makes T[] implementation kinda strange, something developers ought to know.

Note: The generic version of Equals is called only when the class implements IEquatable<T>. If the class doesn't implement IEquatable<T>, non-generic overload of Equals is called irrespective of whether it is called by List<T>.Contains or T[].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. The IList<T> interface cannot be used to add or remove elements from an array. An exception will be thrown if you try to call an IList<T> method such as RemoveAt on an array in this context.

Jeffrey Richter in his book says:

The CLR team didn’t want System.Array to implement IEnumerable<T>, ICollection<T>, and IList<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 implement IEnumerable<T>, ICollection<T>, and IList<T> (where T 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.GenericEqualityComparer1[DummyStruct] System.Collections.Generic.GenericEqualityComparer1[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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆