Jak tablice w C # częściowo implementują IList <T>?

99

Jak więc być może wiesz, tablice w implementacji C # IList<T>, wśród innych interfejsów. Jednak w jakiś sposób robią to bez publicznego wdrażania właściwości Count IList<T>! Tablice mają tylko właściwość Length.

Czy to jest rażący przykład C # / .NET łamiącego własne zasady dotyczące implementacji interfejsu, czy czegoś mi brakuje?

MgSam
źródło
2
Nikt nie powiedział, że Arrayzajęcia muszą być napisane w C #!
user541686
Arrayto "magiczna" klasa, której nie można zaimplementować w C # ani w żadnym innym języku docelowym .net. Ale ta konkretna funkcja jest dostępna w C #.
CodesInChaos

Odpowiedzi:

81

Nowa odpowiedź w świetle odpowiedzi Hansa

Dzięki odpowiedzi udzielonej przez Hansa widzimy, że implementacja jest nieco bardziej skomplikowana, niż mogłoby się wydawać. Zarówno kompilator, jak i środowisko CLR bardzo się starają sprawiać wrażenie, że typ tablicy implementuje IList<T>- ale wariancja tablicy sprawia, że ​​jest to trudniejsze. W przeciwieństwie do odpowiedzi Hansa, typy tablic (jednowymiarowe, w każdym razie oparte na zerach) implementują kolekcje ogólne bezpośrednio, ponieważ typ konkretnej tablicy nie jest System.Array - to tylko typ podstawowy tablicy. Jeśli zapytasz typ tablicy, jakie interfejsy obsługuje, zawiera typy ogólne:

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

Wynik:

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]

W przypadku tablic jednowymiarowych, opartych na zerach, jeśli chodzi o język , tablica naprawdę również implementuje IList<T>. Sekcja 12.1.2 specyfikacji C # tak mówi. Zatem cokolwiek robi podstawowa implementacja, język musi zachowywać się tak, jakby typ T[]implementacji był taki, IList<T>jak w przypadku każdego innego interfejsu. Z tej perspektywy interfejs jest implementowany, a niektóre elementy członkowskie są jawnie implementowane (na przykład Count). To najlepsze wyjaśnienie na poziomie językowym tego, co się dzieje.

Zauważ, że dotyczy to tylko tablic jednowymiarowych (i tablic opartych na zerach, a nie że C # jako język mówi cokolwiek o tablicach niezerowych). T[,] nie implementuje IList<T>.

Z perspektywy CLR dzieje się coś fajniejszego. Nie można uzyskać mapowania interfejsu dla ogólnych typów interfejsów. Na przykład:

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

Daje wyjątek:

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

Skąd więc ta dziwność? Uważam, że tak naprawdę jest to spowodowane kowariancją tablic, która jest brodawką w systemie typów IMO. Mimo że nieIList<T> jest kowariantna (i nie może być bezpieczna), kowariancja tablicowa pozwala na to:

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

... co sprawia, że wygląda jak typeof(string[])narzędziaIList<object> , podczas gdy tak naprawdę nie jest.

Specyfikacja interfejsu CLI (ECMA-335) partycji 1, sekcja 8.7.1, ma następującą treść:

Podpis typu T jest zgodny z podpisem typu U wtedy i tylko wtedy, gdy przynajmniej jedna z poniższych jest zachowana

...

T jest tablicą pozycji 1 od zera V[]i Ujest IList<W>, a V jest zgodna z elementem tablicy z W.

(Właściwie nie wspomina o ICollection<W>lubIEnumerable<W> które uważam, że jest to błąd w specyfikacji).

W przypadku braku wariancji specyfikacja CLI idzie w parze ze specyfikacją języka. Z sekcji 8.9.1 partycji 1:

Dodatkowo utworzony wektor z elementem typu T implementuje interfejs System.Collections.Generic.IList<U>, gdzie U: = T. (§8.7)

( Wektor to jednowymiarowa tablica z zerową podstawą).

Jeśli chodzi o szczegóły implementacji , najwyraźniej CLR wykonuje pewne funky mapowania, aby zachować tutaj zgodność przypisania: gdy string[]zostanie poproszony o implementację ICollection<object>.Count, nie może sobie z tym poradzić w całkiem normalny sposób. Czy liczy się to jako jawna implementacja interfejsu? Myślę, że rozsądne jest traktowanie tego w ten sposób, ponieważ jeśli nie poprosisz bezpośrednio o mapowanie interfejsu, zawsze się zachowuje ten sposób z perspektywy języka.

O co chodzi ICollection.Count?

Do tej pory mówiłem o interfejsach ogólnych, ale jest też nieogólny ICollectionz jego Countwłaściwością. Tym razem można uzyskać mapowanie interfejsu, aw rzeczywistości interfejs jest realizowane bezpośrednio System.Array. Dokumentacja dotycząca ICollection.Countimplementacji właściwości Arraystwierdza, że ​​została zaimplementowana z jawną implementacją interfejsu.

Jeśli ktokolwiek może wymyślić sposób, w jaki tego rodzaju jawna implementacja interfejsu różni się od „normalnej” jawnej implementacji interfejsu, z przyjemnością przyjrzę się temu dokładniej.

Stara odpowiedź dotycząca jawnej implementacji interfejsu

Pomimo powyższego, co jest bardziej skomplikowane ze względu na znajomość tablic, nadal możesz zrobić coś z tymi samymi widocznymi efektami poprzez jawną implementację interfejsu .

Oto prosty samodzielny przykład:

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
    }
}
Jon Skeet
źródło
5
Myślę, że wystąpi błąd podczas kompilacji na foo.M1 (); nie foo.M2 ();
Kevin Aenmey
Wyzwanie polega na tym, aby klasa nieogólna, taka jak tablica, zaimplementowała ogólny typ interfejsu, taki jak IList <>. Twój fragment tego nie robi.
Hans Passant
@HansPassant: Bardzo łatwo jest sprawić, by klasa nieogólna implementowała ogólny typ interfejsu. Trywialny. Nie widzę żadnej wskazówki, że o to pytał OP.
Jon Skeet
4
@JohnSaunders: Właściwie to nie sądzę, żeby cokolwiek z tego było wcześniej niedokładne. Bardzo go rozszerzyłem i wyjaśniłem, dlaczego CLR dziwnie traktuje tablice - ale uważam, że moja odpowiedź dotycząca jawnej implementacji interfejsu była wcześniej całkiem poprawna. W jaki sposób się nie zgadzasz? Ponownie przydatne byłyby szczegóły (ewentualnie w Twojej własnej odpowiedzi, jeśli to stosowne).
Jon Skeet
1
@RBT: Tak, chociaż jest różnica w tym, że użycie Countjest w porządku - ale Addzawsze będzie rzucać, ponieważ tablice mają stały rozmiar.
Jon Skeet
86

Jak więc być może wiesz, tablice w implementacji C # IList<T>, wśród innych interfejsów

Cóż, tak, erm nie, nie do końca. Oto deklaracja klasy Array w środowisku .NET 4:

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

Implementuje System.Collections.IList, a nie System.Collections.Generic.IList <>. Nie może, tablica nie jest ogólna. To samo dotyczy ogólnych interfejsów IEnumerable <> i ICollection <>.

Ale środowisko CLR tworzy konkretne typy tablic w locie, więc może technicznie utworzyć taki, który implementuje te interfejsy. Tak jednak nie jest. Wypróbuj ten kod na przykład:

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

Wywołanie GetInterfaceMap () kończy się niepowodzeniem dla konkretnego typu tablicy z komunikatem „Nie znaleziono interfejsu”. Jednak rzutowanie na IEnumerable <> działa bez problemu.

To jest pisanie jak kaczuszka. Jest to ten sam rodzaj pisania, który tworzy iluzję, że każdy typ wartości pochodzi od ValueType, który pochodzi od Object. Zarówno kompilator, jak i środowisko CLR mają specjalną wiedzę na temat typów tablic, podobnie jak w przypadku typów wartości. Kompilator widzi twoją próbę przesłania do IList <> i mówi "okej, wiem jak to zrobić!". I emituje instrukcję castclass IL. Środowisko CLR nie ma z tym żadnych problemów, wie, jak zapewnić implementację IList <>, która działa na bazowym obiekcie tablicy. Ma wbudowaną wiedzę o skądinąd ukrytej klasie System.SZArrayHelper, opakowaniu, które faktycznie implementuje te interfejsy.

Co nie jest jednoznaczne z twierdzeniami wszystkich, właściwość Count, o którą pytałeś, wygląda następująco:

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

Tak, z pewnością możesz nazwać ten komentarz „łamaniem zasad” :) Poza tym jest cholernie przydatny. I wyjątkowo dobrze ukryty, możesz to sprawdzić w SSCLI20, współdzielonej dystrybucji źródeł CLR. Wyszukaj „IList”, aby zobaczyć, gdzie ma miejsce podstawianie typów. Najlepszym miejscem do zobaczenia tego w akcji jest metoda clr / src / vm / array.cpp, GetActualImplementationForArrayGenericIListMethod ().

Ten rodzaj substytucji w środowisku CLR jest dość łagodny w porównaniu z tym, co dzieje się w projekcji języka w środowisku CLR, który umożliwia pisanie kodu zarządzanego dla WinRT (aka Metro). Prawie każdy podstawowy typ platformy .NET zostanie tam zastąpiony. IList <> mapuje na przykład IVector <>, całkowicie niezarządzany typ. Sam w sobie podstawienie, COM nie obsługuje typów ogólnych.

Cóż, to było spojrzenie na to, co dzieje się za zasłoną. Mogą to być bardzo niewygodne, dziwne i nieznane morza ze smokami żyjącymi na końcu mapy. Bardzo przydatne może być spłaszczenie Ziemi i modelowanie innego obrazu tego, co naprawdę dzieje się w kodzie zarządzanym. Odwzorowanie go na ulubioną odpowiedź wszystkich jest w ten sposób wygodne. Co nie działa tak dobrze w przypadku typów wartości (nie modyfikuj struktury!), Ale ta jest bardzo dobrze ukryta. Błąd metody GetInterfaceMap () to jedyny przeciek w abstrakcji, o jakim przychodzi mi do głowy.

Hans Passant
źródło
1
To jest deklaracja Arrayklasy, która nie jest typem tablicy. Jest to podstawowy typ tablicy. Pojedyncza wymiarowe tablicy C # sposób realizacji IList<T>. A typ nieogólny z pewnością może i tak zaimplementować interfejs ogólny ... który działa, ponieważ istnieje wiele różnych typów - typeof(int[])! = Typeof (string []) , so typeof (int []) `implementuje IList<int>i typeof(string[])implementuje IList<string>.
Jon Skeet
2
@HansPassant: Proszę nie zakładać, że przegłosowałbym coś tylko dlatego, że jest to niepokojące . Faktem jest, że zarówno twoje rozumowanie przez Array(które, jak pokazujesz, jest klasą abstrakcyjną, więc prawdopodobnie nie może być rzeczywistym typem obiektu tablicy), jak i wniosek (że nie implementuje IList<T>) są nieprawidłowe IMO. Sposób , w którym realizuje IList<T>się niezwykła i ciekawa, będę zgodzić - ale to czysto realizacja szczegół. Twierdzenie, że T[]to nie implementuje, IList<T>jest mylące dla IMO. Jest to sprzeczne ze specyfikacją i wszelkim obserwowanym zachowaniem.
Jon Skeet
6
Cóż, na pewno uważasz, że to nieprawda. Nie możesz zmusić go do zabawy z tym, co przeczytasz w specyfikacjach. Nie krępuj się, zobacz to na swój sposób, ale nigdy nie znajdziesz dobrego wyjaśnienia, dlaczego GetInterfaceMap () zawodzi. „Coś dziwnego” nie jest zbytnim wglądem. Noszę okulary implementacyjne: oczywiście się nie udaje, jest to pisanie typu quack-like-a-duck, konkretny typ tablicy w rzeczywistości nie implementuje ICollection <>. Nie ma w tym nic dziwnego. Zatrzymajmy to tutaj, nigdy się nie zgodzimy.
Hans Passant
4
A co z przynajmniej usunięciem fałszywej logiki, której tablice nie mogą zaimplementować, IList<T> ponieważ Array tak nie jest? Ta logika jest dużą częścią tego, z czym się nie zgadzam. Poza tym myślę, że musielibyśmy uzgodnić definicję tego, co oznacza implementacja interfejsu przez typ: moim zdaniem typy tablicowe wyświetlają wszystkie obserwowalne cechy typów, które implementują IList<T>, inne niż GetInterfaceMapping. Ponownie, sposób, w jaki to zostanie osiągnięte, ma dla mnie mniejsze znaczenie, tak jak mogę powiedzieć, że System.Stringjest to niezmienne, mimo że szczegóły implementacji są różne.
Jon Skeet,
1
A co z kompilatorem C ++ CLI? Ten oczywiście mówi: „Nie mam pojęcia, jak to zrobić!” i wyświetla błąd. Potrzebuje wyraźnej obsady IList<T>, aby działała.
Tobias Knauss
21

IList<T>.Countjest implementowana jawnie :

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

Odbywa się to w taki sposób, że gdy masz prostą zmienną tablicową, nie masz obu Counti Lengthbezpośrednio dostępnych.

Ogólnie rzecz biorąc, jawna implementacja interfejsu jest używana, gdy chcesz mieć pewność, że typ może być używany w określony sposób, bez zmuszania wszystkich konsumentów tego typu do myślenia o tym w ten sposób.

Edycja : Ups, źle pamiętam. ICollection.Countjest implementowana jawnie. OgólnyIList<T> jest obsługiwane tak, jak Hans opisuje poniżej .

dlev
źródło
4
Zastanawiam się jednak, dlaczego nie nazwali właściwości Count zamiast Length? Array to jedyna wspólna kolekcja, która ma taką właściwość (chyba że liczysz string).
Tim S.
5
@TimS Dobre pytanie (i takie, którego odpowiedzi nie znam). Spekuluję, że powodem jest to, że „count” oznacza pewną liczbę elementów, podczas gdy tablica ma niezmienną „długość”, gdy tylko zostanie przydzielona ( niezależnie od tego, które elementy mają wartości.)
dlev
1
@TimS Myślę, że zostało to zrobione, ponieważ ICollectiondeklaruje Count, a byłoby jeszcze bardziej zagmatwane, gdyby typ ze słowem „kolekcja” nie był używany Count:). Podejmowanie takich decyzji zawsze wymaga kompromisów.
dlev
4
@JohnSaunders: I znowu ... tylko głos przeciw, bez przydatnych informacji.
Jon Skeet
5
@JohnSaunders: Nadal nie jestem przekonany. Hans odniósł się do implementacji SSCLI, ale twierdził również, że typy tablicowe nawet nie implementują IList<T>, mimo że specyfikacje języka i interfejsu CLI wydają się być przeciwne. Ośmielę się powiedzieć, że sposób, w jaki implementacja interfejsu działa pod osłonami, może być skomplikowana, ale tak jest w wielu sytuacjach. Czy zgodziłbyś się również na kogoś, kto mówi, że System.Stringjest to niezmienne, tylko dlatego, że wewnętrzne działanie jest zmienne? Ze wszystkich praktycznych powodów - iz pewnością w przypadku języka C # - jest to jawne impl.
Jon Skeet
10

Jawna implementacja interfejsu . Krótko mówiąc, deklarujesz to jako void IControl.Paint() { }lub int IList<T>.Count { get { return 0; } }.

Tim S.
źródło
2

Nie różni się od jawnej implementacji interfejsu IList. Tylko dlatego, że implementujesz interfejs, nie oznacza, że ​​jego członkowie muszą pojawiać się jako członkowie klasy. To robi wdrożyć właściwości Count, to po prostu nie narażać go na X [].

nitzmahone
źródło
1

Przy dostępnych źródłach referencyjnych:

//----------------------------------------------------------------------------------------
// ! 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 ... */
}

W szczególności ta część:

dyspozytor odgałęzień interfejsu traktuje to jako przypadek specjalny , ładuje SZArrayHelper, znajduje odpowiednią metodę generyczną (dopasowaną po prostu przez nazwę metody) , tworzy jej instancję dla typu i wykonuje ją.

(Podkreślenie moje)

Źródło (przewiń w górę).

AnorZaken
źródło