Список <T>.Contains и T []. Содержит поведение иначе

Скажем, у меня есть этот класс:

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

Итак, мои вопросы:

  • Почему List<T>.Contains и T[].Contains ведут себя по-другому? Другими словами, почему бывшее имя вызывало общий Equals, а последний не общий Equals , хотя обе коллекции являются общими?
  • Есть ли способ, которым я вижу реализацию T[].Contains?

Изменить: Почему это имеет значение или почему я спрашиваю об этом:

  • Он запускается один раз, если она забывает переопределять не общие Equals при реализации IEquatable<T>, и в этом случае вызовы типа T[].Contains выполняют проверку ссылочного равенства. Особенно, когда она ожидает, что все родовые коллекции будут работать с общим Equals.

  • Вы теряете все преимущества внедрения IEquatable<T> (даже если это не катастрофа для ссылочных типов).

  • Как отмечено в комментариях, просто интересно знать внутренние детали и варианты дизайна. Нет другой общей ситуации, я могу подумать о том, где будет использоваться не общий Equals, будь то любые операции List<T> или set based (Dictionary<K,V> и т.д.). Хуже того, имел Animal - это структура, Animal []. Содержит вызовы generic Equals, все, что делает реализацию T [] странной, что разработчики должны знать,

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

Ответы

Ответ 1

Массивы не реализуют IList<T>, потому что они могут быть многомерными и ненулевыми.

Однако во время выполнения одномерные массивы с нижней границей нуля автоматически реализуют IList<T> и некоторые другие общие интерфейсы. Цель этого взлома во время выполнения приведена ниже в двух кавычках.

Здесь http://msdn.microsoft.com/en-us/library/vstudio/ms228502.aspx говорится:

В С# 2.0 и более поздних одномерных массивах, имеющих нижнюю границу нуля автоматически реализует IList<T>. Это позволяет создавать общие методы, которые могут использовать один и тот же код для итерации по массивам и другие типы коллекций. Этот метод в первую очередь полезен для чтение данных в коллекциях. Интерфейс IList<T> не может использоваться для добавлять или удалять элементы из массива. Исключение будет выбрано, если вы пытаетесь вызвать метод IList<T>, такой как RemoveAt в массиве в этот контекст.

В своей книге Джеффри Рихтер говорит:

Команда CLR не захотела System.Array реализовать IEnumerable<T>, ICollection<T> и IList<T>, однако, из-за проблем, связанных с многомерные массивы и ненулевые массивы. Определение этих интерфейсы на System.Array включили бы эти интерфейсы для всех типы массивов. Вместо этого CLR выполняет небольшой трюк: когда создается одномерный тип массива с нулевым нижним пределом, CLR автоматически реализует тип массива IEnumerable<T>, ICollection<T> и IList<T> (где T - тип элемента массивов) и также реализует три интерфейса для всех базовых типов массивов если они являются ссылочными типами.

Копаем глубже, SZArrayHelper - это класс, который предоставляет эту "взломанную" реализацию 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 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

Вторая строка исключает исключение, со следующей командой stacktrace:

DummyClass.Equals(Object obj) в System.Collections.Generic.ObjectEqualityComparer`1.IndexOf(Т [] массива, значение Т, Int32 StartIndex, счетчик Int32) в System.Array.IndexOf(массив T [], значение T) в System.SZArrayHelper.Contains(значение Т)

Теперь ошибка? или Большой вопрос, вот как мы попали в ObjectEqualityComparer из нашего DummyClass, который реализует IEquatable<T>?

Потому что следующий код:

var t = EqualityComparer<DummyStruct>.Default;
            Console.WriteLine(t.GetType());
            var t2 = EqualityComparer<DummyClass>.Default;
            Console.WriteLine(t2.GetType());

Производит

System.Collections.Generic.GenericEqualityComparer 1[DummyStruct] System.Collections.Generic.GenericEqualityComparer 1 [DummyClass]

Оба используют GenericEqualityComparer, который вызывает метод IEquatable. Фактически Компаратор по умолчанию вызывает следующий метод 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
}

Любопытные части выделены полужирным шрифтом. Очевидно, для DummyClass with Contains мы получили последнюю строку и не пропустили

TypeOf (IEquatable).IsAssignableFrom(с)

проверить!

Почему бы и нет? я предполагаю, что это либо ошибка, либо деталь реализации, которая отличается для структур из-за следующей строки в классе описания SZArrayHelper:

"T" будет отображать интерфейс, используемый для вызова метода. Фактическое время выполнения "this" будет массивом, который можно использовать для "T []" (т.е. Для примитивов и типов значений, он будет точно "T []" - для орфов, это может быть a "U []", где U происходит от T.)

Итак, теперь мы знаем почти все. Единственный вопрос, который остается, заключается в следующем: U не проходит typeof(IEquatable<T>).IsAssignableFrom(c) check?

PS: чтобы быть более точным, SZArrayHelper Содержит код реализации из 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.

Ответ 2

Массивы реализуют общие интерфейсы IList<T>, ICollection<T> и IEnumerable<T>, но реализация выполняется во время выполнения и поэтому не видна инструментам сборки документации (поэтому вы не видите ICollection<T>.Contains в msdn-документация Array).

Я подозреваю, что реализация выполнения просто вызывает не общий IList.Contains(object), который уже имеет массив.
И поэтому вызывается не общий метод Equals в вашем классе.

Ответ 3

Array не имеет метода с именем contains, это метод расширения из класса Enumerable.

Метод Enumerable.Contains, который вы используете в своем массиве,

использует сопоставление равенства по умолчанию.

По умолчанию для сравнения равенств, необходимо переопределить метод Object.Equality.

Это связано с обратной совместимостью.

Списки имеют свои собственные конкретные реализации, но Enumerable должен быть совместим с любым Enumerable, от .NET 1 до .NET 4.5

Удачи.