T []. Содержит для структуры и класса поведение по-разному

Это следующий вопрос: Список <T> .Contains и T []. Содержит поведение по-разному

T[].Contains ведет себя по-другому, когда T является классом и структурой. Предположим, что у меня есть эта структура:

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)

Здесь общий Equals по праву называется, как я ожидал.

Но в случае класса:

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)

Вызывается не общий Equals, отнимающий преимущество реализации `IEquatable.

Почему массив, вызывающий Equals по-разному для struct[] и class[], , хотя обе коллекции, похоже, выглядят родовыми?

Странность массива настолько расстраивает, что я думаю об этом полностью избегать...

Примечание. Общая версия Equals вызывается только тогда, когда struct реализует IEquatable<T>. Если тип не реализует IEquatable<T>, то негенерическая перегрузка Equals вызывается независимо от того, является ли она class или struct.

Ответ 1

Похоже, что это не фактически Array.IndexOf(), который заканчивается тем, что вызывается. Глядя на источник этого, я бы ожидал, что Equals (object) будет вызван в обоих случаях, если это так. Посмотрев на трассировку стека в точке, где вызывается Equals, становится более понятным, почему вы получаете поведение, которое вы видите (тип значения получает Equals (Animal), но ссылочный тип получает Equals (object).

Вот трассировка стека для типа значения (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) 

Вот трассировка стека для ссылочного типа (объект 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)

Из этого вы можете видеть, что это не Array.IndexOf, что вызывается - это Array.IndexOf [T]. Этот метод делает в конечном итоге с использованием Equality comparers. В случае ссылочного типа он использует ObjectEqualityComparer, который вызывает Equals (object). В случае типа значения он использует GenericEqualityComparer, который вызывает Equals (Animal), предположительно, чтобы избежать дорогого бокса.

Если вы посмотрите на исходный код для IEnumerable в http://www.dotnetframework.org он имеет этот интересный бит вверху:

// 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")]

Я не знаком с TypeDependencyAttribute, но из комментария мне интересно, есть ли какая-то магия для этой особой для Array. Это может объяснить, как IndexOf [T] получает вызов вместо IndexOf через Array IList.Contains.

Ответ 2

Я думаю, что это потому, что они используют собственную базовую реализацию Equals

Классы наследуют Object.Equals, который реализует равенство идентичности, Structs inherit ValueType.Equals, который реализует равенство ценности.

Ответ 3

Основной целью IEquatable<T> является обеспечение разумно эффективного сопоставления сравнений с типами общей структуры. Предполагается, что IEquatable<T>.Equals((T)x) должен вести себя точно так же, как Equals((object)(T)x);, за исключением того, что если T - тип значения, первый будет избегать выделения кучи, которое потребуется для последнего. Хотя IEquatable<T> не ограничивает T как тип структуры, а закрытые классы могут в некоторых случаях получать небольшую выгоду от использования, типы классов не могут получить почти столько же выгоды от этого интерфейса, сколько и типы структуры. Правильно написанный класс может выполняться несколько быстрее, если внешний код использует IEquatable<T>.Equals(T) вместо Equals(Object), но в противном случае не заботится о том, какой метод сравнения используется. Поскольку преимущество использования IEquatable<T> с классами никогда не бывает очень большим, код, который знает его с использованием типа класса, может решить, что время, необходимое для проверки того, будет ли тип реализовывать IEquatable<T>, скорее всего, не будет окупиться с помощью какого-либо увеличения производительности интерфейс может быть правдоподобным.

Кстати, стоит отметить, что если X и Y являются "нормальными" классами, X.Equals(Y) может быть законным, если X или Y происходит от другого. Кроме того, переменная типа незапечатанного класса может законно сравниваться с одним из типов интерфейса, независимо от того, реализует ли этот класс этот класс. Для сравнения, структура может сравниваться только с переменной своего типа, Object, ValueType или интерфейсом, который сама структура реализует. Тот факт, что экземпляры типа класса могут быть "равны" гораздо более широкому диапазону типов переменных, означает, что IEquatable<T> не применим к ним, как и к типам структуры.

PS - Есть еще одна причина, по которой массивы являются особыми: они поддерживают стиль ковариации, которые классы не могут. Учитывая,

Dog Fido = new Dog();
Cat Felix = new Cat();
Animal[] meows = new Cat[]{Felix};

вполне законно тестировать meows.Contains(Fido). Если meows были заменены экземпляром Animal[] или Dog[], новый массив действительно может содержать Fido; даже если бы это было не так, можно было бы законно иметь переменную какого-либо неизвестного типа Animal и хотите знать, содержится ли она в meows. Даже если Cat реализует IEquatable<Cat>, пытаясь использовать метод IEquatable<Cat>.Equals(Cat) для проверки того, равен ли элемент meows Fido, потому что Fido не может быть преобразован в Cat. Могут быть способы использования системой IEquatable<Cat>, когда она работоспособна, и Equals(Object), когда это не так, но это добавит много сложности, и было бы трудно обойтись без затрат на производительность, которые были бы выше, чем у просто используя Equals(Object).