Ich weiß, dass Strukturen in .NET keine Vererbung unterstützen, aber es ist nicht genau klar, warum sie auf diese Weise eingeschränkt sind.
Welcher technische Grund verhindert, dass Strukturen von anderen Strukturen erben?
Ich weiß, dass Strukturen in .NET keine Vererbung unterstützen, aber es ist nicht genau klar, warum sie auf diese Weise eingeschränkt sind.
Welcher technische Grund verhindert, dass Strukturen von anderen Strukturen erben?
Antworten:
Der Grund, warum Werttypen die Vererbung nicht unterstützen können, liegt an Arrays.
Das Problem ist, dass aus Leistungs- und GC-Gründen Arrays von Werttypen "inline" gespeichert werden. Beispielsweise gegeben new FooType[10] {...}
, wenn FooType
ein Referenztyp ist, werden 11 Objekte auf dem verwalteten Heap (eine für die Anordnung, und 10 für jede Instanz) angelegt werden. Wenn FooType
es sich stattdessen um einen Werttyp handelt, wird nur eine Instanz auf dem verwalteten Heap erstellt - für das Array selbst (da jeder Array-Wert "inline" mit dem Array gespeichert wird).
Nehmen wir nun an, wir hätten Vererbung mit Werttypen. In Kombination mit dem oben beschriebenen "Inline-Speicher" -Verhalten von Arrays passieren schlechte Dinge, wie in C ++ zu sehen ist .
Betrachten Sie diesen Pseudo-C # -Code:
struct Base
{
public int A;
}
struct Derived : Base
{
public int B;
}
void Square(Base[] values)
{
for (int i = 0; i < values.Length; ++i)
values [i].A *= 2;
}
Derived[] v = new Derived[2];
Square (v);
Nach normalen Konvertierungsregeln kann a Derived[]
in a konvertiert werden Base[]
(zum Guten oder Schlechten). Wenn Sie also für das obige Beispiel s / struct / class / g verwenden, wird es ohne Probleme kompiliert und wie erwartet ausgeführt. Aber wenn Base
und Derived
Werttypen sind und Arrays Werte inline speichern, haben wir ein Problem.
Wir haben ein Problem, weil wir Square()
nichts darüber wissen Derived
. Es wird nur Zeigerarithmetik verwendet, um auf jedes Element des Arrays zuzugreifen, und um einen konstanten Betrag erhöht ( sizeof(A)
). Die Versammlung wäre vage wie folgt:
for (int i = 0; i < values.Length; ++i)
{
A* value = (A*) (((char*) values) + i * sizeof(A));
value->A *= 2;
}
(Ja, das ist eine abscheuliche Assembly, aber der Punkt ist, dass wir das Array mit bekannten Konstanten zur Kompilierungszeit inkrementieren, ohne zu wissen, dass ein abgeleiteter Typ verwendet wird.)
Wenn dies tatsächlich passieren würde, hätten wir Probleme mit der Speicherbeschädigung. Insbesondere innerhalb Square()
, values[1].A*=2
wäre eigentlich sein modifizierende values[0].B
!
Versuchen Sie, DAS zu debuggen !
Stellen Sie sich vor, Strukturen unterstützen die Vererbung. Dann erklären:
BaseStruct a;
InheritedStruct b; //inherits from BaseStruct, added fields, etc.
a = b; //?? expand size during assignment?
würde bedeuten, dass Strukturvariablen keine feste Größe haben, und deshalb haben wir Referenztypen.
Noch besser, bedenken Sie Folgendes:
BaseStruct[] baseArray = new BaseStruct[1000];
baseArray[500] = new InheritedStruct(); //?? morph/resize the array?
Foo
erben Bar
soll nicht zulassen , dass Foo
auf eine zugeordnet werden Bar
, sondern eine Struktur erklärt , dass Art und Weise ein paar nützlichen Effekte ermöglichen: (1) ein vom Typ erstellen speziell benannte Mitglied Bar
als erstes Element in Foo
und hat Foo
umfasst Mitgliedsnamen, die diesen Mitgliedern einen Alias geben Bar
, sodass Code, der früher Bar
angepasst wurde, Foo
stattdessen verwendet werden kann, ohne dass alle Verweise auf thing.BarMember
durch ersetzt werden müssen thing.theBar.BarMember
, und die Fähigkeit erhalten bleibt, alle Bar
Felder als Gruppe zu lesen und zu schreiben ; ...
Strukturen verwenden keine Referenzen (es sei denn, sie sind eingerahmt, aber Sie sollten versuchen, dies zu vermeiden), daher ist Polymorphismus nicht sinnvoll, da keine Indirektion über einen Referenzzeiger erfolgt. Objekte befinden sich normalerweise auf dem Heap und werden über Referenzzeiger referenziert. Strukturen werden jedoch auf dem Stapel zugewiesen (sofern sie nicht eingerahmt sind) oder "innerhalb" des Speichers zugewiesen, der von einem Referenztyp auf dem Heap belegt wird.
Foo
dass ein Bereich des Strukturtypen hat in Bar
der Lage zu betrachten Bar
‚s Mitglieder als seine eigenen, so dass Eine Point3d
Klasse könnte z. B. ein kapseln Point2d xy
, sich aber auf das X
dieses Feldes entweder xy.X
oder beziehen X
.
Hier ist , was die docs sagen:
Strukturen sind besonders nützlich für kleine Datenstrukturen mit Wertsemantik. Komplexe Zahlen, Punkte in einem Koordinatensystem oder Schlüssel-Wert-Paare in einem Wörterbuch sind gute Beispiele für Strukturen. Der Schlüssel zu diesen Datenstrukturen besteht darin, dass sie nur wenige Datenelemente haben, dass keine Vererbung oder referenzielle Identität erforderlich ist und dass sie bequem mithilfe der Wertesemantik implementiert werden können, bei der die Zuweisung den Wert anstelle der Referenz kopiert.
Grundsätzlich sollen sie einfache Daten enthalten und haben daher keine "zusätzlichen Funktionen" wie Vererbung. Es wäre wahrscheinlich technisch möglich, dass sie eine begrenzte Art von Vererbung unterstützen (kein Polymorphismus, da sie sich auf dem Stapel befinden), aber ich glaube, es ist auch eine Entwurfsentscheidung, Vererbung nicht zu unterstützen (wie viele andere Dinge in .NET Sprachen sind.)
Auf der anderen Seite stimme ich den Vorteilen der Vererbung zu, und ich denke, wir haben alle den Punkt erreicht, an dem wir wollen struct
, dass wir von einem anderen erben, und erkennen, dass dies nicht möglich ist. Aber zu diesem Zeitpunkt ist die Datenstruktur wahrscheinlich so weit fortgeschritten, dass es sowieso eine Klasse sein sollte.
Point3D
aus einer zu erstellen Point2D
; Sie wären nicht in der Lage um a Point3D
anstelle von a zu verwenden Point2D
, aber Sie müssten das nicht Point3D
komplett neu implementieren .) So habe ich es sowieso interpretiert ...
class
über , struct
wenn angemessen.
Eine klassenähnliche Vererbung ist nicht möglich, da eine Struktur direkt auf den Stapel gelegt wird. Eine erbende Struktur wäre größer als die übergeordnete, aber die JIT weiß es nicht und versucht, zu viel auf zu wenig Speicherplatz zu setzen. Klingt etwas unklar, schreiben wir ein Beispiel:
struct A {
int property;
} // sizeof A == sizeof int
struct B : A {
int childproperty;
} // sizeof B == sizeof int * 2
Wenn dies möglich wäre, würde es auf dem folgenden Snippet abstürzen:
void DoSomething(A arg){};
...
B b;
DoSomething(b);
Platz wird für die Größe von A zugewiesen, nicht für die Größe von B.
Es gibt einen Punkt, den ich korrigieren möchte. Obwohl der Grund, warum Strukturen nicht vererbt werden können, darin besteht, dass sie auf dem Stapel leben, ist dies die richtige Erklärung. Structs, wie jedes andere Werttyp kann in dem Stapel leben. Da es davon abhängt, wo die Variable deklariert ist, befinden sie sich entweder im Stapel oder im Heap . Dies ist der Fall, wenn es sich um lokale Variablen bzw. Instanzfelder handelt.
Mit diesen Worten hat Cecil einen Namen richtig getroffen.
Ich möchte dies betonen, Werttypen können auf dem Stapel leben. Das bedeutet nicht, dass sie es immer tun. Lokale Variablen, einschließlich Methodenparameter, werden. Alle anderen werden nicht. Trotzdem bleibt es der Grund, warum sie nicht vererbt werden können. :-)
Strukturen werden auf dem Stapel zugeordnet. Dies bedeutet, dass die Wertesemantik ziemlich kostenlos ist und der Zugriff auf Strukturelemente sehr billig ist. Dies verhindert keinen Polymorphismus.
Sie können jede Struktur mit einem Zeiger auf ihre virtuelle Funktionstabelle beginnen lassen. Dies wäre ein Leistungsproblem (jede Struktur hätte mindestens die Größe eines Zeigers), aber es ist machbar. Dies würde virtuelle Funktionen ermöglichen.
Was ist mit dem Hinzufügen von Feldern?
Wenn Sie eine Struktur auf dem Stapel zuweisen, weisen Sie eine bestimmte Menge an Speicherplatz zu. Der erforderliche Speicherplatz wird zur Kompilierungszeit festgelegt (ob vorzeitig oder beim JITting). Wenn Sie Felder hinzufügen und dann einem Basistyp zuweisen:
struct A
{
public int Integer1;
}
struct B : A
{
public int Integer2;
}
A a = new B();
Dadurch wird ein unbekannter Teil des Stapels überschrieben.
Die Alternative besteht darin, dass die Laufzeit dies verhindert, indem nur die Größe von (A) Bytes in eine A-Variable geschrieben wird.
Was passiert, wenn B eine Methode in A überschreibt und auf das Feld Integer2 verweist? Entweder löst die Laufzeit eine MemberAccessException aus, oder die Methode greift stattdessen auf zufällige Daten auf dem Stapel zu. Beides ist nicht zulässig.
Es ist absolut sicher, eine Strukturvererbung zu haben, solange Sie Strukturen nicht polymorph verwenden oder wenn Sie beim Erben keine Felder hinzufügen. Aber diese sind nicht besonders nützlich.
Dies scheint eine sehr häufige Frage zu sein. Ich möchte hinzufügen, dass Werttypen "an Ort und Stelle" gespeichert werden, an dem Sie die Variable deklarieren. Abgesehen von Implementierungsdetails bedeutet dies, dass es keinen Objektheader gibt, der etwas über das Objekt aussagt. Nur die Variable weiß, welche Art von Daten sich dort befinden.
Strukturen unterstützen Schnittstellen, sodass Sie auf diese Weise einige polymorphe Dinge tun können.
IL ist eine stapelbasierte Sprache. Das Aufrufen einer Methode mit einem Argument sieht also ungefähr so aus:
Wenn die Methode ausgeführt wird, werden einige Bytes vom Stapel entfernt, um das Argument abzurufen. Es weiß genau, wie viele Bytes entfernt werden müssen, da das Argument entweder ein Referenztypzeiger ist (immer 4 Bytes bei 32-Bit) oder ein Werttyp, für den die Größe immer genau bekannt ist.
Wenn es sich um einen Referenztypzeiger handelt, sucht die Methode das Objekt im Heap und erhält das Typhandle, das auf eine Methodentabelle verweist, die diese bestimmte Methode für diesen genauen Typ behandelt. Wenn es sich um einen Wertetyp handelt, ist keine Suche in einer Methodentabelle erforderlich, da Werttypen die Vererbung nicht unterstützen. Daher gibt es nur eine mögliche Kombination aus Methode und Typ.
Wenn Werttypen die Vererbung unterstützen würden, würde dies einen zusätzlichen Aufwand bedeuten, da der bestimmte Typ der Struktur sowie ihr Wert auf dem Stapel platziert werden müssten, was eine Art Methodentabellensuche für die bestimmte konkrete Instanz des Typs bedeuten würde. Dies würde die Geschwindigkeits- und Effizienzvorteile von Werttypen beseitigen.