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

20

假设我有这个类:

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

所以我的问题是:
  1. 为什么List<T>.ContainsT[].Contains的行为应该不同?换句话说,为什么前者调用泛型Equals而后者调用非泛型Equals,尽管两个集合都是泛型的?
  2. 我有办法看到T[].Contains的实现吗?
编辑:为什么这很重要或我为什么在问这个问题:
  1. 如果她忘记覆盖非泛型Equals,那么在实现IEquatable<T>时会让人感到困惑,因为像T[].Contains这样的调用会进行引用相等性检查。特别是当她期望所有泛型集合都使用泛型Equals时。

  2. 您将失去实现IEquatable<T>的所有好处(即使对于引用类型也不是灾难性的)。

  3. 如评论中所述,只是想知道内部细节和设计选择。我想不出其他任何一种泛型情况,其中非泛型Equals将被优先考虑,无论是任何List<T>还是基于集合的(Dictionary<K,V>等)操作。更糟糕的是,如果Animal是一个结构,则Animal[].Contains调用泛型Equals,这使得T[]实现有点奇怪,开发人员应该知道。

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

那就是你的问题所在,如果你实现了 IEquatable<T> 接口,你必须重写 Equals(object)GetHashCode() 方法。你不能只实现其中一个并期望一切都能正常工作。 - Jeff Mercado
@JeffMercado 你对此事说得没错,但我希望能看到一些内部细节和设计选择。在我能想到的其他通用情况中,没有其他情况会优先选择*非泛型 Equals,无论是任何List<T>还是基于集合的(Dictionary<K,V>等)操作,即使没有实现非泛型 Equals。嗯,这一切都是以T[]足够泛型为前提的。更糟糕的是,如果Animal是一个结构体,Animal[].Contains会调用通用 Equals*,这使得T[]的实现有点奇怪,开发人员应该了解这一点。我会更新我的问题,让它更清楚明确。 - nawfal
3个回答

11
数组没有实现 IList<T>,因为它们可以是多维和非零基础的。
然而,在运行时,具有下限为零的单维数组自动实现了 IList<T> 和一些其他通用接口。这种运行时技巧的目的在下面的两个引号中详细说明。
在这里 http://msdn.microsoft.com/en-us/library/vstudio/ms228502.aspx 上说:
“在 C# 2.0 及更高版本中,具有下限为零的单维数组自动实现 IList<T>。这使您可以创建通用方法,使用相同的代码来迭代数组和其他集合类型。该技术主要用于读取集合中的数据。不能使用 IList<T> 接口向数组添加或删除元素。如果您尝试在此上下文中调用像 RemoveAt 这样的 IList<T> 方法,则会引发异常。”
Jeffrey Richter 在他的书中说:
“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) 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)

现在的问题是我们如何从实现了 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.GenericEqualityComparer1[DummyStruct] System.Collections.Generic.GenericEqualityComparer1[DummyClass]

两者都使用GenericEqualityComparer,它调用IEquatable方法。实际上,Default comparer会调用以下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[]"-对于oref,它可以是"U[]",其中U派生自T)。

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

PS:为了更准确,SZArrayHelper Contains实现代码来自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中是如何发生的。


1
那是 IList.Contains(object)。我特别在寻找的是 ICollection<T>.Contains(T)。我认为你没有回答我的问题。 - nawfal
1
在 OP 的代码中,实际上并不会调用 Array.IndexOf()。相反,它调用的是 Array.IndexOfT,其行为完全不同。请参见我对他后续问题的回答。https://dev59.com/0mIj5IYBdhLWcg3wx34x#19889083 - hatchet - done with SOverflow
  1. 正如hatchet所说,Array.IndexOf不一定是被调用的方法。我的意思是,它可能会被调用,但我们不能确定。对于结构体,T[]的正确泛型Equals方法将被调用。请参见此问题和答案
- nawfal
我从你的回答中可以推断出更好的答案是:实现全部在运行时完成,并且对于引用类型的实现并不是最正确的。 如果你的回答只包括最后两段,包括其中的引用,我会接受这个答案。其余部分都是猜测和错误信息。 - nawfal
@ValentinKuzub 我承认我犯了一个错误,但这不是问题的本质。问题是为什么会调用非泛型的 Equals。我问的是 为什么会发生 X,而你的回答是 X 会发生,这本身就是错误的,因为 对于结构体来说,X 不会发生。第一段要么是错误的,要么就是不够清楚。我知道你在猜测或做出公正的假设,但它并不正确。我已经厌倦了关于结构体的争论。这是第三次了。如果你可以编辑或删除它,我会接受这个答案。否则,我的反对票还是存在的。请标记我 @nawfal,这样我就能收到你的回复通知。 - nawfal
显示剩余9条评论

1
数组实现了通用接口 IList、ICollection 和 IEnumerable,但是实现是在运行时提供的,因此对于文档生成工具来说是不可见的(这就是为什么在 Array 的 msdn 文档中看不到 ICollection.Contains 的原因)。我怀疑运行时实现只是调用数组已经拥有的非泛型 IList.Contains(object) 方法。因此,你的类中的非泛型 Equals 方法被调用。

@ValentinKuzub 是的,你可以在Array文档的备注部分看到它。但是实现是在运行时提供的,正如我在答案中所述。 - Magnus
@ValentinKuzub 我在谈论问题中的数组。而且(new[] { new Animal { Name = "Fred" } }) is ICollection<Animal>将返回true - Magnus
@Magnus 如果 T 实现了 IEquatable<T>,则在结构体的情况下,T[] 调用通用的 Equals。因此,我怀疑背后是否真的是 IList.Contains(object)。源代码没有任何类型检查的迹象。请参见此问题:https://dev59.com/0mIj5IYBdhLWcg3wx34x - nawfal
1
@nawfal 由于这是一个运行时实现,我们只能猜测发生了什么。 - Magnus

0

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

您正在使用Enumerable.Contains方法在数组中进行操作,

它使用默认相等比较器

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

这是由于向后兼容性。

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

祝你好运


除了 T[] 之外,没有其他 通用 集合需要非泛型的 Equals。实现 IEquatable<T> 的整个目的是避免从对象进行转换,而对于类来说,T[] 不遵守这一点。如果它是为了保持与泛型之前的兼容性,那么为什么对于实现泛型 IEquatable<T> 的结构体表现良好呢?请参见:https://dev59.com/0mIj5IYBdhLWcg3wx34x - nawfal
@nawfal 请看我在这里的评论:https://dev59.com/0mIj5IYBdhLWcg3wx34x#19888269?noredirect=1#comment29586850_19888269 - Yaser Moradi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接