Warum unterstützen Strukturen keine Vererbung?

128

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?

Julia
quelle
6
Ich bin nicht begeistert von dieser Funktionalität, aber ich kann mir einige Fälle vorstellen, in denen die Vererbung von Strukturen nützlich wäre: Sie möchten möglicherweise eine Point2D-Struktur auf eine Point3D-Struktur mit Vererbung erweitern, Sie möchten möglicherweise von Int32 erben, um ihre Werte einzuschränken zwischen 1 und 100 möchten Sie möglicherweise eine Typdefinition erstellen, die über mehrere Dateien hinweg sichtbar ist (der Trick "Typ A = Typ B verwenden" hat nur den Dateibereich) usw.
Julia
4
Vielleicht möchten Sie stackoverflow.com/questions/1082311/… lesen , in dem etwas mehr über Strukturen erklärt wird und warum sie auf eine bestimmte Größe beschränkt werden sollten. Wenn Sie die Vererbung in einer Struktur verwenden möchten, sollten Sie wahrscheinlich eine Klasse verwenden.
Justin
1
Vielleicht möchten Sie stackoverflow.com/questions/1222935/… lesen, um zu erfahren, warum dies auf der dotNet-Plattform einfach nicht möglich ist. Sie haben es kalt gemacht, mit den gleichen Problemen, die für eine verwaltete Plattform katastrophal sein können.
Dykam
@ Justin-Klassen haben Leistungskosten, die Strukturen vermeiden können. Und bei der Spieleentwicklung ist das wirklich wichtig. In einigen Fällen sollten Sie keine Klasse verwenden, wenn Sie helfen können.
Gavin Williams
@ Dykam Ich denke, es kann in C # gemacht werden. Katastrophal ist eine Übertreibung. Ich kann heute katastrophalen Code in C # schreiben, wenn ich mit einer Technik nicht vertraut bin. Das ist also kein wirkliches Problem. Wenn die Strukturvererbung unter bestimmten Szenarien einige Probleme lösen und eine bessere Leistung erzielen kann, bin ich dafür.
Gavin Williams

Antworten:

121

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 FooTypeein Referenztyp ist, werden 11 Objekte auf dem verwalteten Heap (eine für die Anordnung, und 10 für jede Instanz) angelegt werden. Wenn FooTypees 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 Baseund DerivedWerttypen 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*=2wäre eigentlich sein modifizierende values[0].B!

Versuchen Sie, DAS zu debuggen !

jonp
quelle
11
Die sinnvolle Lösung für dieses Problem wäre, die Besetzung von Base [] zu Detived [] zu verbieten. Genau wie das Casting von short [] nach int [] verboten ist, obwohl das Casting von short nach int möglich ist.
Niki
3
+ Antwort: Das Problem mit der Vererbung hat mich erst angesprochen, als Sie es in Form von Arrays ausgedrückt haben. Ein anderer Benutzer gab an, dass dieses Problem durch "Schneiden" von Strukturen auf die entsprechende Größe gemildert werden könnte, aber ich sehe das Schneiden als Ursache für mehr Probleme als es löst.
Julia
4
Ja, aber das "macht Sinn", da Array-Konvertierungen für implizite Konvertierungen und nicht für explizite Konvertierungen gelten. short to int ist möglich, erfordert jedoch eine Umwandlung. Daher ist es sinnvoll, dass short [] nicht in int [] konvertiert werden kann (kurz vor dem Konvertierungscode wie 'a.Select (x => (int) x) .ToArray ( ) '). Wenn die Laufzeit die Umwandlung von Basis zu Abgeleitet nicht zulässt, handelt es sich um eine "Warze", da dies für Referenztypen zulässig ist. Wir haben also zwei verschiedene "Warzen" möglich - die Vererbung von Strukturen verbieten oder die Umwandlung von Arrays von abgeleiteten in Arrays von Base verbieten.
Jonp
3
Zumindest durch die Verhinderung der Strukturvererbung haben wir ein separates Schlüsselwort und können leichter sagen, dass "Strukturen etwas Besonderes sind", im Gegensatz zu einer "zufälligen" Einschränkung in etwas, das für eine Menge von Dingen (Klassen) funktioniert, aber nicht für eine andere (Strukturen). . Ich stelle mir vor, dass die Strukturbeschränkung viel einfacher zu erklären ist ("sie sind anders!").
Jonp
2
müssen den Namen der Funktion von 'Quadrat' in 'Doppel' ändern
John
69

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?
Kenan EK
quelle
5
C ++ antwortete darauf mit der Einführung des Konzepts des "Slicing". Das ist also ein lösbares Problem. Warum sollte die Strukturvererbung nicht unterstützt werden?
Jonp
1
Betrachten Sie Arrays vererbbarer Strukturen und denken Sie daran, dass C # eine (speicher-) verwaltete Sprache ist. Das Schneiden oder eine ähnliche Option würde die Grundlagen der CLR zerstören.
Kenan EK
4
@ Jonp: Lösbar, ja. Wünschenswert? Hier ist ein Gedankenexperiment: Stellen Sie sich vor, Sie haben eine Basisklasse Vector2D (x, y) und eine abgeleitete Klasse Vector3D (x, y, z). Beide Klassen haben eine Magnitude-Eigenschaft, die sqrt (x ^ 2 + y ^ 2) bzw. sqrt (x ^ 2 + y ^ 2 + z ^ 2) berechnet. Wenn Sie schreiben 'Vector3D a = Vector3D (5, 10, 15); Vector2D b = a; ', was soll' a.Magnitude == b.Magnitude 'zurückgeben? Wenn wir dann 'a = (Vector3D) b' schreiben, hat a.Magnitude vor der Zuweisung den gleichen Wert wie danach? Die .NET-Designer sagten sich wahrscheinlich: "Nein, wir werden nichts davon haben."
Julia
1
Nur weil ein Problem gelöst werden kann, heißt das nicht, dass es gelöst werden sollte. Manchmal ist es einfach am besten, Situationen zu vermeiden, in denen das Problem auftritt.
Dan Diplo
@ kek444: struct Mit Fooerben Barsoll nicht zulassen , dass Fooauf 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 Barals erstes Element in Foound hat Fooumfasst Mitgliedsnamen, die diesen Mitgliedern einen Alias ​​geben Bar, sodass Code, der früher Barangepasst wurde, Foostattdessen verwendet werden kann, ohne dass alle Verweise auf thing.BarMemberdurch ersetzt werden müssen thing.theBar.BarMember, und die Fähigkeit erhalten bleibt, alle BarFelder als Gruppe zu lesen und zu schreiben ; ...
Supercat
14

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.

Martin Liversage
quelle
8
man muss keinen Polymorphismus verwenden, um die Vererbung
auszunutzen
Sie hätten also wie viele verschiedene Arten der Vererbung in .NET?
John Saunders
Polymorphismus existiert in Strukturen. Berücksichtigen Sie nur den Unterschied zwischen dem Aufruf von ToString (), wenn Sie ihn in einer benutzerdefinierten Struktur implementieren, oder wenn eine benutzerdefinierte Implementierung von ToString () nicht vorhanden ist.
Kenan EK
Das liegt daran, dass sie alle von System.Object stammen. Es ist mehr der Polymorphismus des System.Object-Typs als von Strukturen.
John Saunders
Polymorphismus könnte bei Strukturen, die als generische Typparameter verwendet werden, von Bedeutung sein. Polymorphismus funktioniert mit Strukturen, die Schnittstellen implementieren. Das größte Problem bei Schnittstellen besteht darin, dass sie byrefs nicht für Strukturfelder verfügbar machen können. Andernfalls wäre das Größte , was ich denke , hilfreich sein , so weit wie structs „vererben“ wäre ein Mittel , einen Typ (Struktur oder Klasse) mit , Foodass ein Bereich des Strukturtypen hat in Barder Lage zu betrachten Bar‚s Mitglieder als seine eigenen, so dass Eine Point3dKlasse könnte z. B. ein kapseln Point2d xy, sich aber auf das Xdieses Feldes entweder xy.Xoder beziehen X.
Supercat
8

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.

Blixt
quelle
4
Das ist nicht der Grund, warum es keine Vererbung gibt.
Dykam
Ich glaube, dass die Vererbung, über die hier gesprochen wird, nicht in der Lage ist, zwei Strukturen zu verwenden, bei denen eine austauschbar von der anderen erbt, sondern die Implementierung einer Struktur zu einer anderen wiederzuverwenden und zu ergänzen (dh eine Point3Daus einer zu erstellen Point2D; Sie wären nicht in der Lage um a Point3Danstelle von a zu verwenden Point2D, aber Sie müssten das nicht Point3Dkomplett neu implementieren .) So habe ich es sowieso interpretiert ...
Blixt
Kurz gesagt: Es könnte die Vererbung ohne Polymorphismus unterstützen. Das tut es nicht. Ich glaube , es ist eine Design - Wahl eine Person zu helfen , wählen classüber , structwenn angemessen.
Blixt
3

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.

Dykam
quelle
2
C ++ behandelt diesen Fall ganz gut, IIRC. Die Instanz von B wird so geschnitten, dass sie in die Größe eines A passt. Wenn es sich wie bei .NET-Strukturen um einen reinen Datentyp handelt, passiert nichts Schlimmes. Bei einer Methode, die ein A zurückgibt, tritt ein Problem auf, und Sie speichern diesen Rückgabewert in einem B, dies sollte jedoch nicht zulässig sein. Kurz gesagt, die .NET - Designer könnte haben mit dieser behandelt , wenn sie wollten, aber sie tat es nicht aus irgendeinem Grund.
Rmeador
1
Für Ihr DoSomething () gibt es wahrscheinlich kein Problem, da (unter der Annahme einer C ++ - Semantik) 'b' zum Erstellen einer A-Instanz "in Scheiben geschnitten" wird. Das Problem liegt bei <i> Arrays </ i>. Betrachten Sie Ihre vorhandenen A- und B-Strukturen und eine <c> DoSomething (A [] arg) {arg [1] .property = 1;} </ c> -Methode. Da Arrays von Werttypen die Werte "inline" speichern, bewirkt DoSomething (actual = new B [2] {}), dass die tatsächliche [0] .childproperty festgelegt wird, nicht die tatsächliche [1] .property. Das ist schlecht.
Jonp
2
@ John: Ich habe nicht behauptet, dass es so ist, und ich glaube auch nicht, dass @jonp es war. Wir haben lediglich erwähnt, dass dieses Problem alt ist und gelöst wurde. Daher haben die .NET-Designer beschlossen, es aus einem anderen Grund als der technischen Unmöglichkeit nicht zu unterstützen.
Rmeador
Es ist zu beachten, dass das Problem "Arrays abgeleiteter Typen" in C ++ nicht neu ist. siehe parashift.com/c++-faq-lite/proper-inheritance.html#faq-21.4 (Arrays in C ++ sind böse! ;-)
jonp
@ John: Die Lösung für das Problem "Arrays von abgeleiteten Typen und Basistypen mischen sich nicht" ist wie üblich "Don't Do That". Aus diesem Grund sind Arrays in C ++ böse (erlaubt leichter eine Speicherbeschädigung) und warum .NET die Vererbung mit Werttypen nicht unterstützt (Compiler und JIT stellen sicher, dass dies nicht möglich ist).
Jonp
3

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. :-)

Rui Craveiro
quelle
3

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.

user38001
quelle
1
Fast. Niemand sonst erwähnte das Slicing-Problem in Bezug auf den Stapel, nur in Bezug auf Arrays. Und niemand sonst erwähnte die verfügbaren Lösungen.
user38001
1
Alle Werttypen in .net werden bei der Erstellung mit Nullen gefüllt, unabhängig von ihrem Typ oder den darin enthaltenen Feldern. Das Hinzufügen eines vtable-Zeigers zu einer Struktur würde ein Mittel zum Initialisieren von Typen mit Standardwerten ungleich Null erfordern. Eine solche Funktion kann für eine Vielzahl von Zwecken nützlich sein, und die Implementierung einer solchen Funktion ist in den meisten Fällen nicht allzu schwierig, aber in .net gibt es nichts Nahes.
Superkatze
@ user38001 "Strukturen werden auf dem Stapel zugewiesen" - es sei denn, es handelt sich um Instanzfelder. In diesem Fall werden sie auf dem Heap zugewiesen.
David Klempfner
2

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.

Cecil hat einen Namen
quelle
Der Compiler weiß, was da ist. Unter Bezugnahme auf C ++ kann dies nicht die Antwort sein.
Henk Holterman
Woher haben Sie C ++ abgeleitet? Ich würde sagen, an Ort und Stelle zu sagen, weil dies am besten zum Verhalten passt. Der Stack ist ein Implementierungsdetail, um einen MSDN-Blogartikel zu zitieren.
Cecil hat einen Namen
Ja, C ++ zu erwähnen war schlecht, nur mein Gedankengang. Aber abgesehen von der Frage, ob Laufzeitinformationen benötigt werden, warum sollten Strukturen keinen 'Objekt-Header' haben? Der Compiler kann sie beliebig mischen. Es könnte sogar einen Header in einer [Structlayout] -Struktur ausblenden.
Henk Holterman
Da Strukturen Werttypen sind, ist dies bei einem Objektheader nicht erforderlich, da die Laufzeit den Inhalt immer wie bei anderen Werttypen kopiert (eine Einschränkung). Mit einem Header würde es keinen Sinn machen, denn dafür sind Referenztypklassen gedacht: P
Cecil hat einen Namen
1

Strukturen unterstützen Schnittstellen, sodass Sie auf diese Weise einige polymorphe Dinge tun können.

Wyatt Barnett
quelle
0

IL ist eine stapelbasierte Sprache. Das Aufrufen einer Methode mit einem Argument sieht also ungefähr so ​​aus:

  1. Schieben Sie das Argument auf den Stapel
  2. Rufen Sie die Methode auf.

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.

Matt Howells
quelle
1
C ++ hat das gelöst, lesen Sie diese Antwort für das eigentliche Problem: stackoverflow.com/questions/1222935/…
Dykam