List.Contains and T[].Contains behaving differently

前端 未结 3 959
情歌与酒
情歌与酒 2021-02-12 14:25

Say I have this class:

public class Animal : IEquatable
{
    public string Name { get; set; }

    public bool Equals(Animal other)
    {
                 


        
3条回答
  •  长情又很酷
    2021-02-12 15:10

    Arrays do not implement IList 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 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. 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 interface cannot be used to add or remove elements from an array. An exception will be thrown if you try to call an IList 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, ICollection, and IList, 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, ICollection, and IList (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,
    // IList, etc., etc. all the way up to IList. When the following call is
    // made:
    //
    //   ((IList) (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  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 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[] array, T value, int startIndex, int count) {
        ...
        return EqualityComparer.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
    {
        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();
        }
    }
    

    I have planted exception throws in both non IEquatable.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?

    Because the following code:

    var t = EqualityComparer.Default;
                Console.WriteLine(t.GetType());
                var t2 = EqualityComparer.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 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
    }
    

    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).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 value)
    {
        return (Array.IndexOf(JitHelpers.UnsafeCast(this), value) != -1);
    }
    

    JitHelpers.UnsafeCast shows following code from 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;
        }
    

    Now I wonder about three exclamation marks and how exactly it happens in that mysterious getILIntrinsicImplementation.

    提交回复
    热议问题