Wie implementieren Arrays in C # IList <T> teilweise?

99

Wie Sie vielleicht wissen, implementieren Arrays in C # IList<T>unter anderem Schnittstellen. Irgendwie tun sie dies jedoch, ohne die Count-Eigenschaft von IList<T>! Arrays haben nur eine Length-Eigenschaft.

Ist dies ein offensichtliches Beispiel dafür, dass C # /. NET seine eigenen Regeln für die Schnittstellenimplementierung verletzt, oder fehlt mir etwas?

MgSam
quelle
2
Niemand sagte, die ArrayKlasse müsse in C # geschrieben werden!
user541686
Arrayist eine "magische" Klasse, die nicht in C # oder einer anderen Sprache implementiert werden kann, die auf .net abzielt. Diese spezielle Funktion ist jedoch in C # verfügbar.
CodesInChaos

Antworten:

81

Neue Antwort im Lichte von Hans 'Antwort

Dank der Antwort von Hans können wir sehen, dass die Implementierung etwas komplizierter ist, als wir vielleicht denken. Sowohl der Compiler als auch die CLR sind sehr bemüht , den Eindruck zu erwecken, den ein Array-Typ implementiert IList<T>- aber die Array-Varianz macht dies schwieriger. Im Gegensatz zu der Antwort von Hans implementieren die Array-Typen (eindimensional, ohnehin nullbasiert) die generischen Sammlungen direkt, da der Typ eines bestimmten Arrays nicht System.Array - das ist nur der Basistyp des Arrays. Wenn Sie einen Array-Typ fragen, welche Schnittstellen er unterstützt, enthält er die generischen Typen:

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

Ausgabe:

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]

Bei eindimensionalen, auf Null basierenden Arrays wird das Array in Bezug auf die Sprache auch wirklich implementiert IList<T>. Abschnitt 12.1.2 der C # -Spezifikation sagt dies aus. Unabhängig von der zugrunde liegenden Implementierung muss sich die Sprache so verhalten, als ob die Art der T[]Implementierung IList<T>wie bei jeder anderen Schnittstelle. Aus dieser Perspektive ist die Schnittstelle wird mit einigen der Mitglieder umgesetzt explizit umgesetzt (wie Count). Das ist die beste Erklärung auf Sprachebene für das, was vor sich geht.

Beachten Sie, dass dies nur für eindimensionale Arrays gilt (und nullbasierte Arrays, nicht dass C # als Sprache etwas über nicht nullbasierte Arrays aussagt). T[,] nicht implementiert IList<T>.

Aus CLR-Sicht ist etwas Funkigeres los. Sie können die Schnittstellenzuordnung für die generischen Schnittstellentypen nicht abrufen. Beispielsweise:

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

Gibt eine Ausnahme von:

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

Warum also die Verrücktheit? Nun, ich glaube, es liegt wirklich an der Array-Kovarianz, die eine Warze im Typsystem IMO ist. Obwohl IList<T>es nicht kovariant ist (und nicht sicher sein kann), ermöglicht die Array-Kovarianz Folgendes:

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

... was es wie Geräte aussehen lässt , wenn es nicht wirklich ist.typeof(string[])IList<object>

Die CLI-Spezifikation (ECMA-335) Partition 1, Abschnitt 8.7.1, enthält Folgendes:

Ein Signaturtyp T ist genau dann mit einem Signaturtyp U kompatibel, wenn mindestens einer der folgenden Punkte zutrifft

...

T ist ein auf Null basierendes Rang-1-Array V[]und Uist IList<W>, und V ist Array-Element-kompatibel mit W.

(Es wird nicht wirklich erwähnt ICollection<W>oder IEnumerable<W>was ich für einen Fehler in der Spezifikation halte.)

Bei Nichtvarianz geht die CLI-Spezifikation direkt mit der Sprachspezifikation einher. Aus Abschnitt 8.9.1 von Partition 1:

Zusätzlich implementiert ein erstellter Vektor mit dem Elementtyp T die Schnittstelle System.Collections.Generic.IList<U>, wobei U: = T. (§8.7)

(Ein Vektor ist ein eindimensionales Array mit einer Nullbasis.)

In Bezug auf die Implementierungsdetails führt die CLR eindeutig eine funky Zuordnung durch, um die Zuweisungskompatibilität hier zu gewährleisten: Wenn a string[]nach der Implementierung von gefragt wird ICollection<object>.Count, kann dies nicht ganz normal gehandhabt werden. Zählt dies als explizite Schnittstellenimplementierung? Ich denke, es ist vernünftig, es so zu behandeln, denn wenn Sie nicht direkt nach der Schnittstellenzuordnung fragen, verhält es sich aus sprachlicher Sicht immer so.

Was ist mit ICollection.Count?

Bisher habe ich über die generischen Schnittstellen gesprochen, aber dann gibt es die nicht generischen ICollectionmit ihrer CountEigenschaft. Dieses Mal sind wir können das Interface - Mapping erhalten, und in der Tat ist die Schnittstelle implementiert direkt durch System.Array. Die Dokumentation für die ICollection.CountEigenschaftsimplementierung in Arraybesagt, dass sie mit expliziter Schnittstellenimplementierung implementiert ist.

Wenn sich jemand vorstellen kann, wie sich diese Art der expliziten Schnittstellenimplementierung von der "normalen" expliziten Schnittstellenimplementierung unterscheidet, würde ich mich gerne weiter damit befassen.

Alte Antwort zur expliziten Schnittstellenimplementierung

Trotz des oben Gesagten, das aufgrund der Kenntnis von Arrays komplizierter ist, können Sie durch explizite Schnittstellenimplementierung immer noch etwas mit denselben sichtbaren Effekten tun .

Hier ist ein einfaches eigenständiges Beispiel:

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
quelle
5
Ich denke, Sie werden einen Kompilierungsfehler auf foo.M1 () bekommen; nicht foo.M2 ();
Kevin Aenmey
Die Herausforderung besteht darin, dass eine nicht generische Klasse wie ein Array einen generischen Schnittstellentyp wie IList <> implementiert. Ihr Snippet macht das nicht.
Hans Passant
@HansPassant: Es ist sehr einfach, eine nicht generische Klasse dazu zu bringen, einen generischen Schnittstellentyp zu implementieren. Trivial. Ich sehe keinen Hinweis darauf, dass das OP danach gefragt hat.
Jon Skeet
4
@ JohnSaunders: Eigentlich glaube ich nicht, dass irgendetwas davon vorher ungenau war. Ich habe es sehr erweitert und erklärt, warum die CLR Arrays seltsam behandelt - aber ich glaube, meine Antwort auf die explizite Schnittstellenimplementierung war vorher ziemlich richtig. Inwiefern sind Sie anderer Meinung? Auch hier wären Details hilfreich (möglicherweise in Ihrer eigenen Antwort, falls zutreffend).
Jon Skeet
1
@RBT: Ja, obwohl es einen Unterschied gibt, dass die Verwendung Countin Ordnung ist - wird aber Addimmer geworfen, da Arrays eine feste Größe haben.
Jon Skeet
86

Wie Sie vielleicht wissen, implementieren Arrays in C # IList<T>unter anderem Schnittstellen

Nun ja, ähm nein, nicht wirklich. Dies ist die Deklaration für die Array-Klasse im .NET 4-Framework:

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

Es implementiert System.Collections.IList, nicht System.Collections.Generic.IList <>. Es kann nicht, Array ist nicht generisch. Gleiches gilt für die generischen Schnittstellen IEnumerable <> und ICollection <>.

Die CLR erstellt jedoch im Handumdrehen konkrete Array-Typen, sodass sie technisch einen erstellen kann, der diese Schnittstellen implementiert. Dies ist jedoch nicht der Fall. Versuchen Sie diesen Code zum Beispiel:

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

Der Aufruf von GetInterfaceMap () schlägt für einen konkreten Array-Typ mit "Schnittstelle nicht gefunden" fehl. Eine Umwandlung in IEnumerable <> funktioniert jedoch problemlos.

Dies ist Quacksalber-wie-eine-Ente-Eingabe. Es ist dieselbe Art der Eingabe, die die Illusion erzeugt, dass jeder Werttyp von ValueType abgeleitet ist, der von Object abgeleitet ist. Sowohl der Compiler als auch die CLR verfügen über spezielle Kenntnisse über Array-Typen, ebenso wie über Werttypen. Der Compiler sieht Ihren Versuch, IList <> zu übertragen, und sagt "Okay, ich weiß, wie das geht!". Und gibt die IL-Anweisung der Castklasse aus. Die CLR hat keine Probleme damit, sie weiß, wie eine Implementierung von IList <> bereitgestellt wird, die auf dem zugrunde liegenden Array-Objekt funktioniert. Es verfügt über integrierte Kenntnisse der ansonsten verborgenen System.SZArrayHelper-Klasse, einem Wrapper, der diese Schnittstellen tatsächlich implementiert.

Die Eigenschaft Count, nach der Sie gefragt haben, sieht folgendermaßen aus:

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

Ja, diesen Kommentar kann man sicherlich als "Verstoß gegen die Regeln" bezeichnen :) Ansonsten ist er verdammt praktisch. Und sehr gut versteckt, können Sie dies in SSCLI20, der gemeinsam genutzten Quelldistribution für die CLR, überprüfen. Suchen Sie nach "IList", um zu sehen, wo die Typersetzung stattfindet. Der beste Ort, um es in Aktion zu sehen, ist die Methode clr / src / vm / array.cpp, GetActualImplementationForArrayGenericIListMethod ().

Diese Art der Ersetzung in der CLR ist im Vergleich zu der Sprachprojektion in der CLR, mit der verwalteter Code für WinRT (auch bekannt als Metro) geschrieben werden kann, recht mild. Nahezu jeder .NET-Kerntyp wird dort ersetzt. IList <> wird beispielsweise IVector <> zugeordnet, einem völlig nicht verwalteten Typ. COM ist selbst eine Substitution und unterstützt keine generischen Typen.

Nun, das war ein Blick darauf, was hinter dem Vorhang passiert. Es kann sehr unangenehm sein, seltsame und unbekannte Meere mit Drachen, die am Ende der Karte leben. Es kann sehr nützlich sein, die Erde flach zu machen und ein anderes Bild davon zu modellieren, was wirklich in verwaltetem Code vor sich geht. Auf diese Weise ist es bequem, es jeder Lieblingsantwort zuzuordnen. Was für Werttypen nicht so gut funktioniert (mutiere keine Struktur!), Aber diese ist sehr gut versteckt. Der GetInterfaceMap () -Methodenfehler ist das einzige Leck in der Abstraktion, an das ich denken kann.

Hans Passant
quelle
1
Das ist die Deklaration für die ArrayKlasse, die nicht der Typ eines Arrays ist. Es ist der Basistyp für ein Array. Ein eindimensionales Array in C # wird implementiert IList<T>. Und ein nicht generischer Typ kann sicherlich sowieso eine generische Schnittstelle implementieren ... was funktioniert, weil es viele verschiedene Typen gibt - typeof(int[])! = Typeof (string []) , so typeof (int []) `implementiert IList<int>und typeof(string[])implementiert IList<string>.
Jon Skeet
2
@ HansPassant: Bitte nehmen Sie nicht an, dass ich etwas ablehnen würde, nur weil es beunruhigend ist . Die Tatsache bleibt, dass sowohl Ihre Argumentation über Array(die, wie Sie zeigen, eine abstrakte Klasse ist, also möglicherweise nicht der tatsächliche Typ eines Array-Objekts sein kann) als auch die Schlussfolgerung (die sie nicht implementiert IList<T>) eine falsche IMO sind. Die ArtIList<T> und Weise, wie es implementiert wird, ist ungewöhnlich und interessant, da stimme ich zu - aber das ist nur ein Implementierungsdetail . Zu behaupten, dass T[]dies nicht implementiert wird, IList<T>ist eine irreführende IMO. Es widerspricht der Spezifikation und allen beobachteten Verhaltensweisen.
Jon Skeet
6
Nun, sicher denkst du, dass es falsch ist. Sie können es nicht mit dem, was Sie in den Spezifikationen gelesen haben, zum Leben erwecken. Bitte sehen Sie es sich so an, aber Sie werden nie eine gute Erklärung dafür finden, warum GetInterfaceMap () fehlschlägt. "Something funky" ist keine große Einsicht. Ich trage eine Implementierungsbrille: Natürlich schlägt dies fehl, es ist eine quacksalberartige Eingabe, ein konkreter Array-Typ implementiert ICollection <> nicht wirklich. Nichts Ungewöhnliches. Lassen Sie es uns hier behalten, wir werden nie zustimmen.
Hans Passant
4
Was ist mit dem Entfernen der falschen Logik, die behauptet, Arrays könnten nicht implementiert werden, IList<T> weil Array dies nicht der Fall ist? Diese Logik ist ein großer Teil dessen, womit ich nicht einverstanden bin. Darüber hinaus müssten wir uns auf eine Definition einigen, was es für einen Typ bedeutet, eine Schnittstelle zu implementieren: Meiner Meinung nach zeigen Array-Typen alle beobachtbaren Merkmale von Typen an, die IList<T>außer implementieren GetInterfaceMapping. Auch hier ist es für mich weniger wichtig, wie dies erreicht wird, genauso wie ich sagen kann, dass dies System.Stringunveränderlich ist, obwohl die Implementierungsdetails unterschiedlich sind.
Jon Skeet
1
Was ist mit dem C ++ CLI-Compiler? Dieser sagt offensichtlich "Ich habe keine Ahnung, wie das geht!" und gibt einen Fehler aus. Es braucht eine explizite Besetzung, IList<T>um zu arbeiten.
Tobias Knauss
21

IList<T>.Countwird explizit implementiert :

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

Dies geschieht so, dass Sie bei einer einfachen Array-Variablen nicht beide Countund habenLength direkt verfügbar haben.

Im Allgemeinen wird die explizite Schnittstellenimplementierung verwendet, wenn Sie sicherstellen möchten, dass ein Typ auf eine bestimmte Art und Weise verwendet werden kann, ohne dass alle Benutzer des Typs gezwungen sind, auf diese Weise darüber nachzudenken.

Edit : Hoppla, schlechter Rückruf da. ICollection.Countwird explizit implementiert. Das Generikum IList<T>wird wie unten beschrieben behandelt .

dlev
quelle
4
Ich frage mich jedoch, warum sie nicht einfach die Eigenschaft Count anstelle von Length aufgerufen haben. Array ist die einzige gemeinsame Sammlung mit einer solchen Eigenschaft (sofern Sie nicht zählen string).
Tim S.
5
@TimS Eine gute Frage (und eine, deren Antwort ich nicht kenne). Ich würde spekulieren, dass der Grund darin liegt, dass "count" eine bestimmte Anzahl von Elementen impliziert, während ein Array eine unveränderliche "Länge" hat, sobald es zugewiesen wird ( unabhängig davon, welche Elemente Werte haben.)
dlev
1
@TimS denke ich das getan , weil ICollectionerklärt Count, und es wäre auch seine mehr verwirrend , wenn ein Typ mit dem Wort „Sammlung“ in es nicht verwenden Count:). Es gibt immer Kompromisse bei diesen Entscheidungen.
dlev
4
@ JohnSaunders: Und nochmal ... nur eine Abwertung ohne nützliche Informationen.
Jon Skeet
5
@ JohnSaunders: Ich bin immer noch nicht überzeugt. Hans hat auf die SSCLI-Implementierung verwiesen, aber auch behauptet, dass Array-Typen nicht einmal implementiert werden IList<T>, obwohl sowohl Sprach- als auch CLI-Spezifikationen im Gegenteil zu sein scheinen. Ich wage zu sagen, dass die Art und Weise, wie die Schnittstellenimplementierung unter dem Deckmantel funktioniert, kompliziert sein kann, aber das ist in vielen Situationen der Fall. Würden Sie auch jemanden ablehnen, der sagt, dass dies System.Stringunveränderlich ist, nur weil die internen Abläufe veränderlich sind? Für alle praktischen Zwecke - und sicherlich für die C # -Sprache - ist dies explizit implizit.
Jon Skeet
2

Es ist nicht anders als eine explizite Schnittstellenimplementierung von IList. Nur weil Sie die Schnittstelle implementieren, müssen ihre Mitglieder nicht als Klassenmitglieder angezeigt werden. Es tut der Count - Eigenschaft implementieren, es aussetzt einfach nicht auf X [].

Nitzmahone
quelle
1

Mit verfügbaren Referenzquellen:

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

Speziell dieser Teil:

Der Interface-Stub-Dispatcher behandelt dies als Sonderfall , lädt SZArrayHelper, findet die entsprechende generische Methode (einfach nach Methodenname abgeglichen ) , instanziiert sie für den Typ und führt sie aus.

(Hervorhebung von mir)

Quelle (nach oben scrollen).

AnorZaken
quelle