Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

T[].Contains for struct and class behaving differently

This is a followup question to this: List<T>.Contains and T[].Contains behaving differently

T[].Contains is behaving differently when T is class and struct. Suppose I have this struct:

public struct Animal : IEquatable<Animal>
{
    public string Name { get; set; }

    public bool Equals(Animal other) //<- he is the man
    {
        return Name == 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.Contains(new Animal { Name = "Fred" }); // calls Equals(Animal)

Here, generic Equals is rightly called as I expected.

But in case of a class:

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

    public bool Equals(Animal other)
    {
        return Name == other.Name;
    }
    public override bool Equals(object obj) //<- he is the man
    {
        return Equals((Animal)obj);
    }
    public override int GetHashCode()
    {
        return Name == null ? 0 : Name.GetHashCode();
    }
}

var animals = new[] { new Animal { Name = "Fred" } };

animals.Contains(new Animal { Name = "Fred" }); // calls Equals(object)

The non generic Equals is called, taking away the benefit of implementing `IEquatable.

Why is array calling Equals differently for struct[] and class[], even though both the collections seem to look generic?

The array weirdness is so frustrating that I'm thinking of avoiding it totally...

Note: The generic version of Equals is called only when the struct implements IEquatable<T>. If the type doesn't implement IEquatable<T>, non-generic overload of Equals is called irrespective of whether it is class or struct.

like image 402
nawfal Avatar asked Nov 10 '13 09:11

nawfal


People also ask

What is the difference between a struct and a class?

A Structure is not secure and cannot hide its implementation details from the end user while a class is secure and can hide its programming and designing details. Following are the points that expound on this difference: 1) Members of a class are private by default and members of a struct are public by default.

What is the difference between structure variable and structure class?

Instance of 'structure' is called 'structure variable'. Instance of a 'class' is called 'object'. Supports polymorphism and a class can also be inherited. A structure is a collection of variables of dissimilar data types, all referenced by one name. A structure declaration forms a template that is used to create an instance of the structure.

What are the similarities between structure and class in C++?

Similarities 1 In C++, both the structure and class are syntactically equivalent. 2 Both structure and class can declare some of their members private. 3 The name of a structure or class can be used as a stand-alone type. 4 Both structure and class support the mechanism of inheritance.

How to initiate a member variable of a structure directly?

Member variables of a structure cannot be initiated directly. 1) Members of a class are private by default and members of a structure are public by default. For example, program 1 fails in compilation but program 2 works fine, 2) Class is declared using the class keyword, and structure is declared using the struct keyword.


1 Answers

It appears that it's not actually Array.IndexOf() that ends up getting called. Looking at the source for that, I would have expected the Equals(object) to get called in both cases if that were the case. By looking at the stack trace at the point where the Equals gets called, it makes it more clear why you're getting the behavior you're seeing (value type gets Equals(Animal), but reference type gets Equals(object).

Here is the stack trace for the value type (struct Animal)

at Animal.Equals(Animal other)
at System.Collections.Generic.GenericEqualityComparer`1.IndexOf(T[] array, T value, Int32 startIndex, Int32 count)
at System.Array.IndexOf[T](T[] array, T value, Int32 startIndex, Int32 count)
at System.Array.IndexOf[T](T[] array, T value)
at System.SZArrayHelper.Contains[T](T value)
at System.Linq.Enumerable.Contains[TSource](IEnumerable`1 source, TSource value) 

Here is the stack trace for the reference type (object Animal)

at Animal.Equals(Object obj)
at System.Collections.Generic.ObjectEqualityComparer`1.IndexOf(T[] array, T value, Int32 startIndex, Int32 count)
at System.Array.IndexOf[T](T[] array, T value, Int32 startIndex, Int32 count)
at System.Array.IndexOf[T](T[] array, T value)
at System.SZArrayHelper.Contains[T](T value)
at System.Linq.Enumerable.Contains[TSource](IEnumerable`1 source, TSource value)

From this you can see that it's not Array.IndexOf that's getting called - it's Array.IndexOf[T]. That method does end up using Equality comparers. In the case of the reference type, it uses ObjectEqualityComparer which call Equals(object). In the case of the value type, it uses GenericEqualityComparer which calls Equals(Animal), presumably to avoid an expensive boxing.

If you look at the source code for IEnumerable at http://www.dotnetframework.org it has this interesting bit at the top:

// Note that T[] : IList<t>, and we want to ensure that if you use
// IList<yourvaluetype>, we ensure a YourValueType[] can be used
// without jitting.  Hence the TypeDependencyAttribute on SZArrayHelper.
// This is a special hack internally though - see VM\compile.cpp.
// The same attribute is on IList<t> and ICollection<t>.
[TypeDependencyAttribute("System.SZArrayHelper")]

I'm not familiar with TypeDependencyAttribute, but from the comment, I'm wondering if there is some magic going on that's special for Array. This may explain how IndexOf[T] ends up getting called instead of IndexOf via Array's IList.Contains.

like image 157
hatchet - done with SOverflow Avatar answered Nov 06 '22 14:11

hatchet - done with SOverflow