20

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.

Community
  • 1
  • 1
nawfal
  • 70,104
  • 56
  • 326
  • 368
  • Well that's your problem right there, if you implement the `IEquatable` interface, you _must_ override the `Equals(object)` _and_ `GetHashCode()` methods. You cannot implement one and not the other and expect things to work as expected. – Jeff Mercado Nov 10 '13 at 08:29
  • @JeffMercado you're right about it, but I would love to see some 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` or set based (`Dictionary` etc) operations even if *non generic `Equals`* is not implemented. Well all that is assuming `T[]` is generic *enough*. 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. I will update my question to make it clear. – nawfal Nov 10 '13 at 08:39

3 Answers3

11

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.

Valentin Kuzub
  • 11,703
  • 7
  • 56
  • 93
  • 1
    That is `IList.Contains(object)`. I was specifically looking for `ICollection.Contains(T)`. I think you haven't answered to my question. – nawfal Nov 10 '13 at 08:41
  • see my edits. I am not sure how we can find out how runtime implementation actually looks, but we can infer it from its behavior? – Valentin Kuzub Nov 10 '13 at 09:21
  • 1
    In the case of the OP's code, Array.IndexOf() is not actually what gets called. Instead, it's Array.IndexOf[T](), which has quite different behavior. See my answer to his follow on question. http://stackoverflow.com/questions/19888123/t-contains-for-struct-and-class-behaving-differently/19889083#19889083 – hatchet - done with SOverflow Nov 10 '13 at 11:44
  • @ValentinKuzub, I dont want to be criticizing, but this answer is so wrong on many levels. **1.** Elements in `T[]` array are stored as objects? Not at all. If that was the case it would result in boxing for value types [which doesn't happen](http://stackoverflow.com/questions/8213887/does-system-array-perform-boxing-on-value-types-or-not). **2.** You say *if it's not overloaded correctly on type, which implements IEquatable, no call to `IEquatable.Equals`*, but I have rightly *implemented* `IEquatable.Equals`, and still non generic `Equals` is called. ... – nawfal Nov 11 '13 at 05:01
  • **3.** As hatchet says `Array.IndexOf` is not *necessarily* what is being called. I mean it *may be*, but there is not something we can be sure of. For structs, the right generic `Equals` is called for `T[]`s. [See this question and answer](http://stackoverflow.com/questions/19888123/t-contains-for-struct-and-class-behaving-differently). – nawfal Nov 11 '13 at 05:04
  • A better answer I can infer from your answer is: *The implementation is all at run-time, and it wasn't implemented most correctly for reference types*. If your answer has just the last two paragraphs including the quotes in between, I will accept this answer. Rest are guesses and misinformation. – nawfal Nov 11 '13 at 05:10
  • You say if it's not overloaded correctly on type, which implements IEquatable, no call to IEquatable.Equals, but I have rightly implemented IEquatable.Equals, and still non generic Equals is called. <- Correct overload will also overload usual Equals and call IEquatable.Equals from it. Thats what you did incorrectly in the first place ( I admit part of your question). – Valentin Kuzub Nov 11 '13 at 18:14
  • @ValentinKuzub I admit I made a mistake, but thats not the essence of the question. The question is why is non generic `Equals` being called. I'm asking *why is X happening*, your answer is *X happens* which itself is wrong because *X doesnt happen for structs*. The first paragraph is either wrong or so unclear. I know you're doing a guesstimate or a fair assumption, but it isnt correct. I'm tired of making the point about structs. This is the 3rd time. If you can edit or remove it, I'll accept this answer. Otherwise my downvote stands. Kindly tag me `@nawfal` so that Im notified of your reply – nawfal Nov 11 '13 at 19:50
  • @nawfal had to continue my investigation.. See the results in edited answer. – Valentin Kuzub Nov 11 '13 at 22:20
  • @ValentinKuzub - I wonder if it's failing IsAssignableFrom for the same reason the unsafe cast is needed in the hacky SzArrayHelper class (which then produces the oddness when doing the comparisons against the RuntimeType)? – hatchet - done with SOverflow Nov 11 '13 at 23:59
  • @ValentinKuzub - I was stepping through the .NET code and it was interesting. It goes from the `return collection.Contains(value);` in `Enumerable.Contains`, where T is Animal, right into `SzArrayHelper.Contains`, but there T is object, and the rest of the call chain is working with an object[]. Note that `typeof(IEquatable).IsAssignableFrom(typeof(object))` is false. If T had been Animal at that point, `typeof(IEquatable).IsAssignableFrom(typeof(Animal))` would have returned true. – hatchet - done with SOverflow Nov 12 '13 at 00:38
  • @hatchet well, **object** would explain the problem. Can we call that JitHelpers.UnsafeCast using reflection to see what it produces for our array? – Valentin Kuzub Nov 12 '13 at 00:44
  • @ValentinKuzub - I'm not sure it matters. Whatever magic code calls `SzArrayHelper.Contains` is calling the `Contains` generic method. Once that generic type is set, it effects everything after that. I don't know if that's intentional or a bug, but I think that is the key. The UnsafeCast is after that, and just dutifully casts it to object[]. Reading the comments to SzArrayHelper though, it kind of seems like a bug since it goes from `Enumerable.Contains` to `SzArrayHelper.Contains`. – hatchet - done with SOverflow Nov 12 '13 at 01:28
  • @hatchet: I suspect a lot has to do with the fact that a `Cat[]` is also considered to be an `Animal[]` and `ICollection` as well as an `Object[]` and `ICollection`. For `Cat[]`'s implementation of `ICollection` to use `IEquatable`, it would have to be different from `Cat[]`'s implementation of `ICollection` or `ICollection`. It's much easier to have every `T[]` only implement `ICollection` for the lowest level type `U` for which the interface is implemented. – supercat Nov 12 '13 at 17:34
1

Arrays does implement the generic Interfaces IList<T>, ICollection<T> and IEnumerable<T> but the implemeneation is provided at runtime and therefore are not visible to the documentation build tools (That is why you don't see ICollection<T>.Contains in the msdn documentation of Array).

I suspect that the runtime implementation just calls the non generic IList.Contains(object) that the array already has.
And therefor the non generic Equals method in your class is called.

Magnus
  • 45,362
  • 8
  • 80
  • 118
  • @ValentinKuzub Yes they do, you can see it in the remarks section in in the [Array documentation](http://msdn.microsoft.com/en-us/library/system.array(v=vs.110).aspx) But the implementation is provided at runtime as I have stated in my answer. – Magnus Nov 10 '13 at 09:42
  • @ValentinKuzub I am talking about the array in the question. And `(new[] { new Animal { Name = "Fred" } }) is ICollection` will return `true` – Magnus Nov 10 '13 at 09:48
  • @Magnus `T[]` calls the generic `Equals` in case of structs if `IEquatable` is implemented for `T`. So I doubt if it's really `IList.Contains(object)` behind the scenes. The source code indicates nothing of a type checking. See this question: http://stackoverflow.com/questions/19888123/t-contains-for-struct-and-class-behaving-differently – nawfal Nov 10 '13 at 09:58
  • 1
    @nawfal Since it is a runtime implementation we can only guess what is going on. – Magnus Nov 10 '13 at 10:05
0

Array has no method with name of contains, this is an extension method from Enumerable class.

Enumerable.Contains method, which you are using that in your array,

is using default equality comparer.

The default equality comparer, needs override of Object.Equality method.

This is because of backward compatibility.

Lists have their own specific implementations, but Enumerable should be compatible with any Enumerable, from .NET 1 to .NET 4.5

Good luck

Yaser Moradi
  • 3,267
  • 3
  • 24
  • 50
  • No other **generic** collection requires non generic `Equals`, except `T[]`. The whole purpose of implementing `IEquatable` is to avoid the cast from object, which `T[]` doesn't obey for classes. If it's to maintain backward compatibility with pre-generic era, why is it behaving well for structs that implement generic `IEquatable`? See this: http://stackoverflow.com/questions/19888123/t-contains-for-struct-and-class-behaving-differently – nawfal Nov 10 '13 at 10:08
  • @nawfal See my comment here : http://stackoverflow.com/questions/19888123/t-contains-for-struct-and-class-behaving-differently/19888269?noredirect=1#comment29586850_19888269 – Yaser Moradi Nov 10 '13 at 11:09