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

99

Итак, как вы, возможно, знаете, массивы в C # реализуются IList<T>среди других интерфейсов. Однако каким-то образом они делают это без публичной реализации свойства Count IList<T>! У массивов есть только свойство Length.

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

MgSam
источник
2
Никто не говорил, что этот Arrayкласс должен быть написан на C #!
user541686
Arrayэто «волшебный» класс, который не может быть реализован на C # или другом языке, ориентированном на .net. Но эта особенность доступна в C #.
CodesInChaos

Ответы:

81

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

Благодаря ответу Ханса мы видим, что реализация несколько сложнее, чем мы думали. И компилятор, и среда 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 спецификации C #. Таким образом, что бы ни делала базовая реализация, язык должен вести себя так, как если бы этот тип T[]реализован, IList<T>как и любой другой интерфейс. С этой точки зрения, интерфейс будет реализован с некоторыми из членов которые явно реализованы (например Count). Это лучшее объяснение происходящего на языковом уровне.

Обратите внимание, что это справедливо только для одномерных массивов (и массивов с нулевым отсчетом, а не то, что C # как язык говорит что-либо о ненулевых массивах). 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>, хотя на самом деле это не так.

Спецификация CLI (ECMA-335), раздел 1, раздел 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
    }
}
Джон Скит
источник
5
Я думаю, вы получите сбой во время компиляции foo.M1 (); не foo.M2 ();
Kevin Aenmey
Задача здесь состоит в том, чтобы неуниверсальный класс, например массив, реализовывал универсальный тип интерфейса, например IList <>. Ваш фрагмент этого не делает.
Hans Passant
@HansPassant: очень легко сделать неуниверсальный класс реализацией универсального типа интерфейса. Тривиально. Я не вижу никаких признаков того, что ОП спрашивал об этом.
Джон Скит,
4
@JohnSaunders: На самом деле, я не верю, что раньше что-то было неточным. Я сильно расширил его и объяснил, почему CLR странно обрабатывает массивы, но я считаю, что мой ответ о явной реализации интерфейса раньше был довольно правильным. В чем вы не согласны? Опять же, подробности были бы полезны (возможно, в вашем собственном ответе, если это необходимо).
Джон Скит
1
@RBT: Да, хотя есть разница в том, что использование Count- это нормально, но Addвсегда будет выбрасывать, поскольку массивы имеют фиксированный размер.
Джон Скит,
86

Как вы, возможно, знаете, массивы в C # реализуют 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 <>. Не может, Array не является универсальным. То же самое касается универсальных интерфейсов 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 <> работает без проблем.

Это шарлатанский набор текста. Это тот же тип ввода, который создает иллюзию, что каждый тип значения является производным от 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 (также известного как Metro). Здесь заменяется практически любой базовый тип .NET. IList <> отображается на IVector <>, например, полностью неуправляемый тип. Сам по себе подстановка, COM не поддерживает универсальные типы.

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

Ганс Пассан
источник
1
Это объявление Arrayкласса, который не является типом массива. Это базовый тип массива. Одномерный массив в C # действительно реализует IList<T>. И неуниверсальный тип, безусловно, может реализовать общий интерфейс в любом случае ... который работает, потому что существует множество разных типов - typeof(int[])! = Typeof (string []) , so typeof (int []) `реализует IList<int>и typeof(string[])реализует IList<string>.
Джон Скит
2
@HansPassant: Пожалуйста, не думайте, что я бы проголосовал против чего-то только из-за того, что это тревожит . Факт остается фактом: как ваше рассуждение Array(которое, как вы показываете, является абстрактным классом, поэтому не может быть фактическим типом объекта массива), так и вывод (который он не реализует IList<T>) неверны IMO. Способ , в котором реализуется IList<T>необычно и интересно, я согласен - но это чисто реализация деталей. Утверждать, что T[]это не реализовано IList<T>, вводит в заблуждение ИМО. Это противоречит спецификации и всему наблюдаемому поведению.
Джон Скит
6
Ну, конечно, вы думаете, что это неверно. Вы не можете смешать то, что читаете в спецификациях. Не стесняйтесь смотреть на это по-своему, но вы никогда не найдете хорошего объяснения, почему GetInterfaceMap () не работает. «Что-то напуганное» - не очень хорошее понимание. Я ношу очки для реализации: конечно, это не работает, это типизация шарлатана, конкретный тип массива на самом деле не реализует ICollection <>. Ничего особенного в этом нет. Давай оставим это здесь, мы никогда не договоримся.
Hans Passant
4
Как насчет хотя бы удаления ложной логики, которая утверждает, что массивы не могут реализовать, IList<T> потому Array что не выполняет? Эта логика - большая часть того, с чем я не согласен. Помимо этого, я думаю, нам нужно согласовать определение того, что означает для типа реализация интерфейса: на мой взгляд, типы массивов отображают все наблюдаемые особенности типов, которые реализуются IList<T>, кроме GetInterfaceMapping. Опять же, то, как это достигается, для меня менее важно, точно так же, как я могу сказать, что System.Stringэто неизменяемо, даже если детали реализации отличаются.
Джон Скит
1
А как насчет компилятора C ++ CLI? Тот явно говорит: «Я понятия не имею, как это сделать!» и выдает ошибку. Для работы требуется явное приведение IList<T>.
Тобиас Кнаусс
21

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 ниже .

dlev
источник
4
Однако меня заставляет задуматься, почему они просто не назвали свойство Count вместо Length? Массив - единственная общая коллекция, у которой есть такое свойство (если не считать string).
Тим С.
5
@TimS Хороший вопрос (и ответ на который я не знаю.) Я бы предположил, что причина в том, что «count» подразумевает некоторое количество элементов, тогда как массив имеет неизменную «длину», как только он выделяется ( независимо от того, какие элементы имеют значения.)
dlev
1
@TimS Я думаю, что это сделано, потому что ICollectionобъявляет Count, и было бы еще больше запутать, если бы тип со словом «коллекция» в нем не использовал Count:). При принятии этих решений всегда есть компромиссы.
dlev
4
@JohnSaunders: И снова ... просто отрицательный голос без полезной информации.
Джон Скит
5
@JohnSaunders: Я все еще не уверен. Ганс сослался на реализацию SSCLI, но также заявил, что типы массивов даже не реализуются IList<T>, несмотря на то, что и язык, и спецификации CLI говорят об обратном. Осмелюсь сказать, что способ, которым реализация интерфейса работает под прикрытием, может быть запутанным, но так бывает во многих ситуациях. Вы бы также проголосовали против кого-то, кто сказал, что System.Stringэто неизменяемое, только потому, что внутренняя работа изменяема? Для всех практических целей - и , конечно же , насколько C # язык обеспокоен - это является явным осущ.
Джон Скит,
2

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

Nitzmahone
источник
1

При наличии справочников:

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

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

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

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

(Акцент мой)

Источник (прокрутите вверх).

АнорЗакен
источник