首页
学习
活动
专区
工具
TVP
发布
社区首页 >问答首页 >List<T>.Contains和T[].Contains的行为不同

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

Stack Overflow用户
提问于 2013-11-10 16:07:15
回答 3查看 1.1K关注 0票数 20

假设我有这样一个类:

代码语言:javascript
复制
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();
    }
}

这就是测试:

代码语言:javascript
复制
var animals = new[] { new Animal { Name = "Fred" } };

现在,当我这样做的时候:

代码语言:javascript
复制
animals.ToList().Contains(new Animal { Name = "Fred" }); 

它调用正确的通用Equals重载。问题出在数组类型上。假设我这样做了:

代码语言:javascript
复制
animals.Contains(new Animal { Name = "Fred" });

它调用非泛型Equals方法。实际上,T[]并不公开ICollection<T>.Contains方法。在上面的例子中,调用了IEnumerable<Animal>.Contains扩展重载,而后者又调用了ICollection<T>.Contains。下面是IEnumerable<T>.Contains的实现方式:

代码语言:javascript
复制
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);
}

所以我的问题是:

  1. 为什么List<T>.ContainsT[].Contains的行为应该不同?换句话说,即使两个集合都是generic
  2. Is的,为什么前者调用泛型Equals,后者调用非泛型Equals ,以一种我可以看到T[].Contains实现的方式?

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

  1. 它会出错,以防她在实现IEquatable<T>时忘记覆盖非泛型Equals,在这种情况下,像T[].Contains这样的调用会进行引用相等性检查。特别是当她期望所有泛型集合都在泛型Equals.
  2. You上操作时,就会失去实现IEquatable<T>的所有好处(即使这对引用类型来说并不是灾难)。
  3. 在注释中提到,他只对了解内部细节和设计选择感兴趣。我认为没有其他通用的情况下会首选非通用Equals,可以是任何基于List<T>或set (Dictionary<K,V>等)的操作。更糟糕的是,had Animal been a struct, Animal[].Contains calls the ,所有这些使得T[]实现有点奇怪,这是开发人员应该知道的。

注意:只有在类实现IEquatable<T>__时才会调用Equals的泛型版本。如果该类不实现IEquatable<T>,则无论是由List<T>.Contains还是由T[].Contains调用,都会调用Equals的非泛型重载。

EN

回答 3

Stack Overflow用户

发布于 2013-11-10 16:33:42

数组不实现IList<T>,因为它们可以是多维的,也可以不是从零开始的。

但是,在运行时,下限为零的一维数组会自动实现IList<T>和其他一些通用接口。这个运行时攻击的目的在下面的两个引号中进行了详细阐述。

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

C# 2.0及更高版本中的

,下限为零的一维数组会自动实现IList<T>。这使您能够创建泛型方法,这些方法可以使用相同的代码循环访问数组和其他集合类型。此技术主要用于读取集合中的数据。IList<T>接口不能用于在数组中添加或删除元素。如果您尝试在此上下文中的数组上调用诸如RemoveAt之类的IList<T>方法,将会抛出异常。

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

尽管如此,CLR团队不希望System.Array实现IEnumerable<T>ICollection<T>IList<T>,因为与多维数组和非零基数组相关的问题。在System.Array上定义这些接口将为所有数组类型启用这些接口。相反,CLR执行一个小技巧:当创建一维的零下限数组类型时,CLR会自动使数组类型实现IEnumerable<T>ICollection<T>IList<T> (其中T是数组的元素类型),并为所有数组类型的基类型实现这三个接口(只要它们是引用类型)。

深入挖掘,SZArrayHelper是为基于零的一维数组提供这种“老套”IList实现的类。

下面是类的描述:

//---------------------------------------------------------------------------------------- //!在学习本课程之前,请先阅读本文。/此类上的方法必须非常小心地编写,以避免引入安全漏洞。//这是因为它们是用特殊的"this“调用的!所有这些方法的"this“对象//都不是SZArrayHelper对象。相反,它们的类型是U[] //,其中U[]是可转换为T[]的。不会实例化任何实际的SZArrayHelper对象。因此,您将//看到许多转换为"this“"T[]”的表达式。/此类是允许T[]类型的SZ数组公开IList、// IList等一直到IList所必需的。当//进行以下调用时:/ ((IList) (new Un)).SomeIListMethod() /接口存根调度器将此视为特殊情况,加载SZArrayHelper,//找到相应的泛型方法(仅通过方法名匹配),实例化//它的类型并执行它。/ "T“将反映用于调用该方法的接口。实际运行时"this“将是//可转换为"T[]”的数组(即,对于原语和值类型,它将恰好是// "T[]“-对于oref,它可以是U从T派生的"U[]“。)//--------------------------------------

并包含实现:

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; }

所以我们调用下面的方法

代码语言:javascript
复制
public static int IndexOf<T>(T[] array, T value, int startIndex, int count) {
    ...
    return EqualityComparer<T>.Default.IndexOf(array, value, startIndex, count);
}

到目前一切尚好。但现在我们到了最令人好奇/ But的部分。

考虑以下示例(基于您的后续问题)

代码语言:javascript
复制
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()实现中都植入了异常抛出。

令人惊讶的是:

代码语言:javascript
复制
    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实现!

但是当我们尝试以下代码时:

代码语言:javascript
复制
    structs.Contains(new DummyStruct {Name = "Fred"});
    classes.Contains(new DummyClass { Name = "Fred" }); //<-throws exception, since it calls object.Equals method

第二行抛出异常,堆栈跟踪如下:

System.Collections.Generic.ObjectEqualityComparer`1.IndexOf(T[]的

数组(Object DummyClass.Equals),T的值,System.Array.IndexOf(T[]的数组,T的值)System.SZArrayHelper.Contains(T的值)的Int32开始索引,Int32的计数)

现在是bug了吗?这里最大的问题是,我们是如何从实现了IEquatable<T>的DummyClass中获得ObjectEqualityComparer的

因为下面的代码:

代码语言:javascript
复制
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.GenericEqualityComparer1DummyClass

两者都使用调用IEquatable方法的GenericEqualityComparer。事实上,默认的比较器调用CreateComparer方法:

代码语言:javascript
复制
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派生)

所以我们现在知道了几乎所有的事情。剩下的唯一问题是,为什么你没有通过typeof(IEquatable<T>).IsAssignableFrom(c)检查?

PS:更准确地说,SZArrayHelper包含来自SSCLI20的实现代码。目前的实现似乎已经改变,原因反射器显示了这个方法的以下内容:

代码语言:javascript
复制
private bool Contains<T>(T value)
{
    return (Array.IndexOf<T>(JitHelpers.UnsafeCast<T[]>(this), value) != -1);
}

JitHelpers.UnsafeCast显示了来自dotnetframework.org的以下代码

代码语言:javascript
复制
   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中发生的。

票数 11
EN

Stack Overflow用户

发布于 2013-11-10 16:45:54

数组确实实现了通用接口IList<T>ICollection<T>IEnumerable<T>,但实现是在运行时提供的,因此对于文档构建工具是不可见的(这就是为什么您在Array的msdn文档中看不到ICollection<T>.Contains )。

我怀疑运行时实现只是调用数组已有的非泛型IList.Contains(object)

因此,类中的非泛型Equals方法被调用。

票数 1
EN

Stack Overflow用户

发布于 2013-11-10 16:37:59

数组没有名为contains的方法,这是可枚举类的扩展方法。

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

正在使用默认相等比较器

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

这是因为向后兼容。

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

祝好运

票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/19887562

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档