Як масиви в C # частково реалізують IList <T>?


99

Отже, як вам відомо, масиви в C # реалізуються IList<T>серед інших інтерфейсів. Хоч якось це роблять, не публічно реалізуючи власність Count IList<T>! Масиви мають лише властивість Length.

Це нахабний приклад того, що C # /. NET порушує власні правила щодо реалізації інтерфейсу чи я щось пропускаю?


2
Ніхто не сказав, що Arrayклас потрібно писати на C #!
користувач541686

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.

То чому дивацтво? Ну, я вважаю, що це дійсно пов'язано з коваріацією масиву, яка є бородавкою в системі типів, IMO. Незважаючи на те, що IList<T>він не є коваріантним (і не може бути безпечним), коваріація масиву дозволяє це працювати:

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

... що робить його виглядати як typeof(string[])знаряддя IList<object>, коли це не на самому ділі.

Розділ 8.7.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 робить певне відображення, щоб зберегти сумісність призначення тут: коли 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 ();
Кевін Айнмей

Завдання тут полягає в тому, щоб негенеричний клас, як масив, реалізував загальний тип інтерфейсу, як IList <>. Ваш фрагмент цього не робить.
Ганс Пасант

@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 <>. Це не може, масив не є загальним. Те саме стосується загальних інтерфейсів 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 <> і каже "добре, я знаю, як це зробити!". І випускає інструкцію ІЛ з каскаду. З 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 .dt замінюється там. Наприклад, IList <> відображає на IVector <>, цілком некерований тип. Сам замінник COM не підтримує загальних типів.

Ну, це був погляд на те, що відбувається за завісою. Це можуть бути дуже незручні, дивні та незнайомі моря з драконами, що живуть у кінці карти. Це може бути дуже корисно зробити Землю плоскою та моделювати інше зображення того, що насправді відбувається в керованому коді. Таке зручне відображення улюбленої відповіді усім. Що не працює так добре для типів значень (не мутуйте структуру!), Але це дуже добре приховано. Невдача методу GetInterfaceMap () - це єдиний витік абстракції, про який я можу придумати.


1
Це декларація для Arrayкласу, який не є типом масиву. Це базовий тип масиву. Один одновимірний масив в C # це здійснити IList<T>. І без універсального типу , безумовно , може реалізувати загальний інтерфейс в будь-якому випадку ... , який працює , тому що є багато різних типів - typeof(int[])! = TypeOf (рядок []) , so TypeOf (ІНТ []) `реалізує IList<int>і typeof(string[])знарядь IList<string>.
Джон Скіт

2
@HansPassant: Будь ласка, не припускайте, що я б порушив щось саме через те, що це непокоїть . Факт залишається фактом, що і ваші міркування через Array(який, як ви показуєте, абстрактний клас, тому не може бути фактичним типом об’єкта масиву), і висновок (який він не реалізує IList<T>) є неправильним ІМО. Спосіб , в якому реалізується IList<T>незвично і цікаво, я згоден - але це чисто реалізація деталей. Стверджувати, що T[]не застосовується IList<T>, вводить в оману IMO. Це суперечить специфіці та всій спостережуваній поведінці.
Джон Скіт

6
Ну, впевнений, ви вважаєте, що це неправильно. Ви не можете змусити її з тим, що читаєте у специфікаціях. Будь ласка, не соромтеся бачити це по-своєму, але ви ніколи не придумаєте хорошого пояснення, чому GetInterfaceMap () не вдається. "Щось фанкі" - це не багато прозріння. Я ношу окуляри для реалізації: звичайно, це не вдається, він набирає шарлатан-як-качка, тип конкретного масиву насправді не реалізує ICollection <>. Нічого забавного в цьому немає. Давайте збережемо його тут, ми ніколи не погодимось.
Ганс Пасант

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>обробляється так, як внизу описується Ганс .


4
Мене змушує замислитися, чому вони не просто називали властивість Count замість довжини? Масив - єдина поширена колекція, яка має таке властивість (якщо не рахувати string).
Тім С.

5
@TimS Хороший запитання (і той, відповідь якого я не знаю.) Я б припустив, що причина полягає в тому, що "count" передбачає деяку кількість елементів, тоді як масив має незмінну "довжину", як тільки він виділяється ( незалежно від того, які елементи мають значення).
dlev

1
@TimS Я думаю, що це робиться тому, що ICollectionдекларує Count, і було б ще більше заплутано, якщо тип із словом "колекція" в ньому не використовувався Count:). Завжди є компроміси у прийнятті цих рішень.
день

4
@JohnSaunders: І знову ж таки ... просто голосовий запис без корисної інформації.
Джон Скіт

5
@JohnSaunders: Я досі не переконаний. Ханс посилався на реалізацію SSCLI, але також стверджував, що типи масивів навіть не реалізуються IList<T>, незважаючи на те, що мови та специфікації CLI виглядають протилежними. Смію сказати, що реалізація інтерфейсу під обкладинками може бути хитромудрою, але це так у багатьох ситуаціях. Ви також хотіли б сказати, що хтось сказав, що System.Stringце незмінне, лише тому, що внутрішня робота є змінною? Для всіх практичних цілей - і , звичайно ж , наскільки C # мова стурбований - це є явним здійснення.
Джон Скіт


2

Це не відрізняється від явної реалізації інтерфейсу IList. Тільки тому, що ви реалізуєте інтерфейс, це не означає, що його члени повинні відображатися як члени класу. Він робить реалізувати властивість Count, він просто не виставляє його на X [].


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, знаходить відповідний загальний метод (відповідний просто за назвою методу) , інстанціює його для типу та виконує його.

(Наголос мій)

Джерело (прокручування вгору).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.