Как массивы на С# частично реализуют IList <T>?

Итак, как вы знаете, массивы в С# реализуют IList<T>, среди других интерфейсов. Так или иначе, они делают это, не публично реализуя свойство Count IList<T>! Массивы имеют свойство Length.

Является ли это вопиющим примером С#/. NET, нарушающим собственные правила реализации интерфейса или я чего-то не хватает?

Ответ 1

Новый ответ в свете ответа Ханса

Благодаря ответу Ганса, мы видим, что реализация несколько сложнее, чем мы могли бы подумать. И компилятор, и CLR очень стараются создать впечатление, что тип массива реализует IList<T>, но дисперсия массива делает это сложнее. Вопреки ответам Ганса, типы массивов (одномерные, нулевые) в любом случае реализуют общие коллекции напрямую, потому что тип любого конкретного массива не является System.Array - это только базовый тип массива. Если вы спросите тип массива, какие интерфейсы он поддерживает, он включает в себя общие типы:

foreach (var type in typeof(int[]).GetInterfaces())
{
    Console.WriteLine(type);
}

Вывод:

System.ICloneable
System.Collections.IList
System.Collections.ICollection
System.Collections.IEnumerable
System.Collections.IStructuralComparable
System.Collections.IStructuralEquatable
System.Collections.Generic.IList`1[System.Int32]
System.Collections.Generic.ICollection`1[System.Int32]
System.Collections.Generic.IEnumerable`1[System.Int32]

Для одномерных массивов с нулевым основанием, насколько это касается языка, массив действительно реализует IList<T> тоже. В разделе 12.1.2 спецификации С# сказано так. Итак, независимо от того, что делает базовая реализация, язык должен вести себя так, как будто тип T[] реализует IList<T>, как и любой другой интерфейс. С этой точки зрения интерфейс реализован с некоторыми явными реализациями (например, Count). Это лучшее объяснение на уровне языка для того, что происходит.

Обратите внимание, что это справедливо только для одномерных массивов (и массивов с нулевым основанием, а не для С#, поскольку язык говорит что-либо о ненулевых массивах). T[,] не реализует IList<T>.

С точки зрения CLR происходит что-то более смелое. Вы не можете получить сопоставление интерфейса для общих типов интерфейсов. Например:

typeof(int[]).GetInterfaceMap(typeof(ICollection<int>))

Выдает исключение:

Unhandled Exception: System.ArgumentException: Interface maps for generic
interfaces on arrays cannot be retrived.

Так почему же странность? Я считаю, что это действительно из-за ковариации массива, которая является бородавкой в ​​системе типов, ИМО. Хотя IList<T> не является ковариантным (и не может быть безопасным), ковариация массива позволяет это работать:

string[] strings = { "a", "b", "c" };
IList<object> objects = strings;

... что делает его похожим на typeof(string[]) реализует IList<object>, если это действительно не так.

Раздел 1 спецификации CLI (ECMA-335), раздел 8.7.1, имеет следующее:

Тип подписи T совместим с типом сигнатуры U тогда и только тогда, когда выполняется хотя бы одно из следующих значений:

...

T представляет собой массив ранга-1, основанный на нулевом значении V[], а U - IList<W>, а V - совместимый с массивом-элемент с W.

(На самом деле он не упоминает ICollection<W> или IEnumerable<W>, который, я считаю, является ошибкой в ​​спецификации.)

Для невариантности спецификация CLI напрямую связана с спецификацией языка. Из раздела 8.9.1 раздела 1:

Кроме того, созданный вектор с типом элемента T реализует интерфейс System.Collections.Generic.IList<U>, где U: = T. (§8.7)

(Вектор - это одномерный массив с нулевой базой.)

Теперь с точки зрения деталей реализации, CLR делает какое-то фанковое отображение, чтобы поддерживать совместимость здесь: когда a string[] запрашивается для реализации ICollection<object>.Count, он не может справиться с этим в довольно обычный способ. Означает ли это, что это явная реализация интерфейса? Я думаю, что разумно относиться к этому так, как если бы вы не запрашивали прямое отображение интерфейса, он всегда ведет себя таким образом с точки зрения языка.

Как насчет ICollection.Count?

До сих пор я говорил об общих интерфейсах, но тогда есть не общий ICollection со свойством Count. На этот раз мы можем получить сопоставление интерфейса, и на самом деле интерфейс реализован непосредственно System.Array. Документация для реализации свойства ICollection.Count в Array гласит, что она реализована с явной реализацией интерфейса.

Если кто-нибудь может подумать о том, как эта явная реализация интерфейса отличается от "нормальной" реализации явного интерфейса, я был бы рад изучить ее дальше.

Старый ответ о явной реализации интерфейса

Несмотря на вышеизложенное, более сложное из-за знания массивов, вы все равно можете сделать что-то с теми же видимыми эффектами через явная реализация интерфейса.

Вот простой пример:

public interface IFoo
{
    void M1();
    void M2();
}

public class Foo : IFoo
{
    // Explicit interface implementation
    void IFoo.M1() {}

    // Implicit interface implementation
    public void M2() {}
}

class Test    
{
    static void Main()
    {
        Foo foo = new Foo();

        foo.M1(); // Compile-time failure
        foo.M2(); // Fine

        IFoo ifoo = foo;
        ifoo.M1(); // Fine
        ifoo.M2(); // Fine
    }
}

Ответ 2

Итак, как вы знаете, массивы в реализации С# IList<T>, среди других интерфейсов

Хорошо, да, нет, не совсем. Это объявление для класса Array в платформе .NET 4:

[Serializable, ComVisible(true)]
public abstract class Array : ICloneable, IList, ICollection, IEnumerable, 
                              IStructuralComparable, IStructuralEquatable
{
    // etc..
}

Он реализует System.Collections.IList, а не System.Collections.Generic.IList < > . Он не может, массив не является общим. То же самое относится к общим интерфейсам IEnumerable < > и ICollection < > .

Но CLR создает конкретные типы массивов "на лету", поэтому он может технически создать тот, который реализует эти интерфейсы. Однако это не так. Попробуйте этот код, например:

using System;
using System.Collections.Generic;

class Program {
    static void Main(string[] args) {
        var goodmap = typeof(Derived).GetInterfaceMap(typeof(IEnumerable<int>));
        var badmap = typeof(int[]).GetInterfaceMap(typeof(IEnumerable<int>));  // Kaboom
    }
}
abstract class Base { }
class Derived : Base, IEnumerable<int> {
    public IEnumerator<int> GetEnumerator() { return null; }
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); }
}

Вызов GetInterfaceMap() завершается с ошибкой для конкретного типа массива с "Интерфейс не найден". Однако приведение в IEnumerable < > работает без проблем.

Это типичная для quacks-like-a-duck. Это тот же тип ввода, который создает иллюзию, что каждый тип значения происходит из ValueType, который происходит от Object. И компилятор, и CLR имеют специальные знания типов массивов, как и типы значений. Компилятор видит вашу попытку кастинга в IList < > и говорит "хорошо, я знаю, как это сделать!". И испускает инструкцию castclass IL. У CLR нет проблем с этим, он знает, как обеспечить реализацию IList < > , которая работает с базовым объектом массива. Он имеет встроенные знания о скрытом в скрытом классе System.SZArrayHelper, оболочке, которая фактически реализует эти интерфейсы.

Что он не делает явно, как все утверждают, свойство Count, о котором вы спрашивали, выглядит следующим образом:

    internal int get_Count<T>() {
        //! Warning: "this" is an array, not an SZArrayHelper. See comments above
        //! or you may introduce a security hole!
        T[] _this = JitHelpers.UnsafeCast<T[]>(this);
        return _this.Length;
    }

Да, вы можете, конечно, назвать этот комментарий "нарушением правил":) Это иначе проклято. И очень хорошо скрытый, вы можете проверить это в SSCLI20, распределении общего источника для CLR. Найдите "IList", чтобы увидеть, где происходит замещение типа. Лучшим местом для его просмотра является метод clr ​​/src/vm/array.cpp, GetActualImplementationForArrayGenericIListMethod().

Этот вид замещения в CLR довольно мягкий по сравнению с тем, что происходит при проектировании языка в среде CLR, что позволяет писать управляемый код для WinRT (aka Metro). Почти любой тип .NET типа подставляется там. IList < > отображает на IVector < > , например, полностью неуправляемый тип. Сама подстановка, COM не поддерживает общие типы.

Хорошо, это был взгляд на то, что происходит за занавеской. Это могут быть очень неудобные, странные и незнакомые моря с драконами, живущими в конце карты. Может быть очень полезно сделать Землю плоской и моделировать другое изображение того, что действительно происходит в управляемом коде. Таким образом, сопоставление его с каждым любимым ответом удобно. Что не так хорошо работает для типов значений (не мутируйте структуру!), Но эта очень хорошо скрыта. Ошибка метода GetInterfaceMap() - единственная утечка в абстракции, о которой я могу думать.

Ответ 3

IList<T>.Count реализовано явно:

int[] intArray = new int[10];
IList<int> intArrayAsList = (IList<int>)intArray;
Debug.Assert(intArrayAsList.Count == 10);

Это делается так, что, когда у вас есть простая переменная массива, вы не имеете сразу Count и Length.

В общем, явная реализация интерфейса используется, когда вы хотите убедиться, что тип может использоваться определенным образом, не заставляя всех потребителей такого типа думать об этом таким образом.

Изменить: Упс, плохо вспомните. ICollection.Count выполняется явно. Общий IList<T> обрабатывается как Hans descibes ниже.

Ответ 5

Это не отличается от явной реализации интерфейса IList. Просто потому, что вы реализуете интерфейс, не означает, что его члены должны появляться как члены класса. Он реализует свойство Count, он просто не раскрывает его на X [].

Ответ 6

Доступны справочные источники:

//----------------------------------------------------------------------------------------
// ! 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.)
//----------------------------------------------------------------------------------------
sealed class SZArrayHelper {
    // It is never legal to instantiate this class.
    private SZArrayHelper() {
        Contract.Assert(false, "Hey! How'd I get here?");
    }

    /* ... snip ... */
}

В частности, эта часть:

диспетчер заглушки интерфейса рассматривает это как особый случай, загружает SZArrayHelper, находит соответствующий общий метод (просто сопоставляется по имени метода), создает экземпляр для типа и выполняет его.

(Акцент мой)

Source (прокрутка вверх).