Warum sind Strukturen und Klassen in C # getrennte Konzepte?

44

Beim Programmieren in C # bin ich auf eine seltsame Entscheidung für das Sprachdesign gestoßen, die ich einfach nicht verstehe.

C # (und die CLR) haben also zwei aggregierte Datentypen: struct(Werttyp, auf dem Stapel gespeichert, keine Vererbung) und class(Referenztyp, auf dem Heap gespeichert, hat Vererbung).

Dieses Setup hört sich zunächst gut an, aber dann stoßen Sie auf eine Methode, die einen Parameter eines Aggregattyps verwendet. Um herauszufinden, ob es sich tatsächlich um einen Werttyp oder einen Referenztyp handelt, müssen Sie die Typdeklaration ermitteln. Es kann manchmal sehr verwirrend werden.

Die allgemein akzeptierte Lösung des Problems scheint darin zu bestehen, dass alle structs als "unveränderlich" deklariert werden (indem ihre Felder auf "unveränderlich" gesetzt werden readonly), um mögliche Fehler zu vermeiden und die structNützlichkeit von s einzuschränken .

C ++ verwendet beispielsweise ein viel besser verwendbares Modell: Es ermöglicht Ihnen, eine Objektinstanz entweder auf dem Stapel oder auf dem Heap zu erstellen und sie als Wert oder als Referenz (oder als Zeiger) zu übergeben. Ich höre immer wieder, dass C # von C ++ inspiriert wurde, und ich kann einfach nicht verstehen, warum es diese eine Technik nicht übernommen hat. Das Kombinieren von classund structzu einem Konstrukt mit zwei verschiedenen Zuordnungsoptionen (Heap und Stack) und deren Weitergabe als Werte oder (explizit) als Referenzen über die Schlüsselwörter refund outscheint eine nette Sache zu sein.

Die Frage ist, warum haben classund structwerden getrennte Konzepte in C # und der CLR anstelle von einem Aggregat Typ mit zwei Zuweisungsoptionen?

Mints97
quelle
34
"Die allgemein akzeptierte Lösung für das Problem scheint darin zu bestehen, alle Strukturen als" unveränderlich "zu deklarieren ... die Nützlichkeit von Strukturen einzuschränken". Viele Leute würden argumentieren, dass es nützlicher ist, wenn etwas unveränderlich ist, wenn es nicht die Ursache eines Leistungsengpasses ist. Außerdem werden structs nicht immer auf dem Stapel gespeichert. Betrachten Sie ein Objekt mit einem structFeld. Davon abgesehen, wie Mason Wheeler erwähnte, ist das Problem des Aufschneidens wahrscheinlich der größte Grund.
Doval
7
Es ist nicht wahr, dass C # von C ++ inspiriert wurde; Vielmehr wurde C # von all den (wohlgemeinten und damals gut klingenden) Fehlern im Design von C ++ und Java inspiriert.
Pieter Geerkens
18
Hinweis: Stack und Heap sind Implementierungsdetails. Nichts besagt, dass Strukturinstanzen auf dem Stack und Klasseninstanzen auf dem Heap zugeordnet werden müssen. Und es ist in der Tat nicht einmal wahr. Beispielsweise ist es sehr gut möglich, dass ein Compiler mithilfe der Escape-Analyse feststellt, dass eine Variable nicht aus dem lokalen Bereich entfernt werden kann, und sie daher auf dem Stapel zuweist, selbst wenn es sich um eine Klasseninstanz handelt. Es heißt nicht einmal, dass es überhaupt einen Stapel oder einen Haufen geben muss. Sie können Umgebungsrahmen als verknüpfte Liste auf dem Heap zuweisen und haben nicht einmal einen Stapel.
Jörg W Mittag
3
"aber dann stößt man auf eine Methode, die einen Parameter eines Aggregattyps nimmt, und um herauszufinden, ob es sich tatsächlich um einen Werttyp oder einen Referenztyp handelt, muss man die Typdeklaration finden" Umm, warum ist das genau so wichtig? ? Die Methode fordert Sie auf, einen Wert zu übergeben. Sie übergeben einen Wert. An welchem ​​Punkt müssen Sie sich darum kümmern, ob es sich um einen Referenztyp oder einen Werttyp handelt?
Luaan

Antworten:

58

Der Grund, warum C # (und Java und im Wesentlichen jede andere OO-Sprache, die nach C ++ entwickelt wurde) das C ++ - Modell in diesem Aspekt nicht kopierten, ist, dass die Art und Weise, wie C ++ dies tut, ein schreckliches Durcheinander ist.

Sie haben oben die relevanten Punkte korrekt identifiziert struct:: Werttyp, keine Vererbung. class: Referenztyp, hat Vererbung. Vererbungs- und Werttypen (oder genauer gesagt Polymorphismus und Pass-by-Wert) passen nicht zusammen. Wenn Sie ein Objekt vom Typ Derivedan ein Methodenargument vom Typ übergeben Baseund darauf eine virtuelle Methode aufrufen, können Sie nur sicherstellen, dass es sich bei dem übergebenen Objekt um eine Referenz handelt.

Dazwischen und all den anderen Problemen, denen Sie in C ++ begegnen, wenn Sie vererbbare Objekte als Werttypen haben (Kopierkonstruktoren und Objektschnitte fallen Ihnen ein !), Ist die beste Lösung, einfach Nein zu sagen.

Gutes Sprachdesign implementiert nicht nur Funktionen, sondern weiß auch, welche Funktionen nicht implementiert werden sollen. Eine der besten Möglichkeiten, dies zu tun, besteht darin, aus den Fehlern derjenigen zu lernen, die vor Ihnen aufgetreten sind.

Mason Wheeler
quelle
29
Dies ist nur ein weiteres sinnloses subjektives Ranting in C ++. Ich kann nicht abstimmen, würde es aber tun, wenn ich könnte.
Bartek Banachewicz
17
@MasonWheeler: "Ist eine schreckliche Sauerei " klingt subjektiv genug. Dies wurde bereits in einem langen Kommentarthread zu einer anderen Ihrer Antworten besprochen . Der Thread wurde geknackt (leider, weil er nützliche Kommentare enthielt, obwohl in Flame War Sauce). Ich denke nicht, dass es sich lohnt, das Ganze hier zu wiederholen, aber "C # hat es richtig gemacht und C ++ hat es falsch gemacht" (was die Botschaft zu sein scheint, die Sie vermitteln wollen) ist in der Tat eine subjektive Aussage.
Andy Prowl
15
@MasonWheeler: Ich habe in dem Thread, der geknackt wurde, auch einige andere Leute getroffen - deswegen finde ich es bedauerlich, dass er gelöscht wurde. Ich halte es nicht für eine gute Idee, diesen Thread hier zu replizieren, aber die Kurzversion lautet: In C ++ kann der Benutzer des Typs, nicht sein Designer , entscheiden, mit welcher Semantik ein Typ verwendet werden soll (Referenzsemantik oder Wert) Semantik). Dies hat Vor- und Nachteile: Sie toben gegen die Nachteile, ohne die Vorteile zu berücksichtigen (oder zu kennen?). Deshalb ist die Analyse subjektiv.
Andy Prowl
7
seufz Ich glaube, die Diskussion über LSP-Verstöße hat bereits stattgefunden. Und ich glaube ein bisschen, dass die Mehrheit der Leute zustimmte, dass die LSP-Erwähnung ziemlich seltsam und nicht verwandt ist, aber ich kann es nicht überprüfen, weil ein Mod den Kommentarthread kaputt gemacht hat .
Bartek Banachewicz
9
Wenn Sie Ihren letzten Absatz nach oben verschieben und den aktuellen ersten Absatz löschen, haben Sie meines Erachtens das perfekte Argument. Der derzeitige erste Absatz ist jedoch nur subjektiv.
Martin York
19

Analog ist C # im Grunde genommen wie ein Satz von Mechanikerwerkzeugen, bei denen jemand gelesen hat, dass Sie Zangen und verstellbare Schraubenschlüssel generell vermeiden sollten, so dass verstellbare Schraubenschlüssel überhaupt nicht enthalten sind und die Zangen in einer speziellen Schublade mit der Kennzeichnung "unsicher" verriegelt sind. , und können nur mit Zustimmung eines Vorgesetzten verwendet werden, nachdem ein Haftungsausschluss unterzeichnet wurde, der Ihren Arbeitgeber von jeglicher Verantwortung für Ihre Gesundheit entbindet.

Im Vergleich dazu enthält C ++ nicht nur verstellbare Schraubenschlüssel und Zangen, sondern auch einige Sonderwerkzeuge, deren Zweck nicht sofort ersichtlich ist. Wenn Sie nicht wissen, wie Sie sie richtig halten können, können sie Ihnen leicht die Hand nehmen Daumen (aber wenn Sie erst einmal wissen, wie man sie benutzt, können Sie mit den grundlegenden Werkzeugen in der C # -Toolbox Dinge tun, die im Grunde unmöglich sind). Darüber hinaus gibt es eine Drehmaschine, Fräsmaschine, Flachschleifmaschine, Metallbandsäge usw., mit denen Sie jederzeit ganz neue Werkzeuge entwerfen und herstellen können, wenn Sie dies für erforderlich halten (aber ja, die Werkzeuge dieser Maschinisten können und werden dies bewirken Schwere Verletzungen, wenn Sie nicht wissen, was Sie mit ihnen machen - oder wenn Sie einfach nur nachlässig werden.

Dies spiegelt den grundlegenden Unterschied in der Philosophie wider: C ++ versucht, Ihnen alle Werkzeuge zur Verfügung zu stellen, die Sie für praktisch jedes gewünschte Design benötigen. Es wird fast kein Versuch unternommen, zu steuern, wie Sie diese Werkzeuge verwenden. Daher ist es auch einfach, mit ihnen Entwürfe zu erstellen, die nur in seltenen Situationen gut funktionieren, und Entwürfe, die wahrscheinlich nur eine schlechte Idee sind und von denen niemand weiß, in welcher Situation Sie werden wahrscheinlich überhaupt gut funktionieren. Insbesondere wird ein Großteil davon durch die Entkopplung von Entwurfsentscheidungen erzielt - auch wenn diese in der Praxis fast immer gekoppelt sind. Infolgedessen gibt es einen großen Unterschied zwischen dem einfachen Schreiben von C ++ und dem guten Schreiben von C ++. Um C ++ gut zu schreiben, müssen Sie eine Menge Redewendungen und Faustregeln kennen (einschließlich Faustregeln, wie ernst es ist, sie zu überdenken, bevor Sie andere Faustregeln brechen). Als Ergebnis, C ++ orientiert sich viel mehr an der Benutzerfreundlichkeit (von Experten) als an der Leichtigkeit des Lernens. Es gibt auch (allzu viele) Umstände, unter denen es auch nicht wirklich schrecklich einfach ist, es zu benutzen.

C # unternimmt noch viel mehr, um zu erzwingen (oder zumindest äußerst deutlich vorzuschlagen), was die Sprachdesigner als gute Entwurfspraktiken erachteten. Nicht wenige Dinge, die in C ++ entkoppelt sind (aber in der Praxis normalerweise zusammenpassen), sind in C # direkt gekoppelt. Es erlaubt "unsicherem" Code, die Grenzen ein wenig zu verschieben, aber ehrlich gesagt nicht viel.

Das Ergebnis ist, dass es auf der einen Seite einige Designs gibt, die ziemlich direkt in C ++ ausgedrückt werden können und in C # wesentlich ungeschickter auszudrücken sind. Auf der anderen Seite ist es eine ganze Menge einfacher C #, und die Chancen zur Herstellung ein wirklich schreckliches Designs , die Arbeit für Ihre Situation nicht lernen (oder wahrscheinlich andere) werden drastisch reduziert. In vielen (wahrscheinlich sogar den meisten) Fällen können Sie ein solides, funktionsfähiges Design erhalten, indem Sie sozusagen "im Fluss" bleiben. Oder, als einer meiner Freunde (zumindest ich sehe ihn gerne als Freund - nicht sicher, ob er wirklich einverstanden ist), macht es C # leicht, in die Grube des Erfolgs zu fallen.

Wenn Sie sich also genauer mit der Frage befassen, wie classund structwie sie sich in den beiden Sprachen befinden: Objekte, die in einer Vererbungshierarchie erstellt wurden, in der Sie möglicherweise ein Objekt einer abgeleiteten Klasse als Basisklasse / Schnittstelle verwenden, sind Sie ziemlich fertig stecken geblieben mit der Tatsache, dass Sie dies normalerweise über einen Zeiger oder eine Referenz tun müssen - auf einer konkreten Ebene passiert, dass das Objekt der abgeleiteten Klasse etwas Gedächtnis enthält, das als eine Instanz der Basisklasse behandelt werden kann / Schnittstelle, und das abgeleitete Objekt wird über die Adresse dieses Teils des Speichers manipuliert.

In C ++ ist es Sache des Programmierers, dies korrekt zu tun. Wenn er die Vererbung verwendet, muss er sicherstellen, dass (zum Beispiel) eine Funktion, die mit polymorphen Klassen in einer Hierarchie arbeitet, dies über einen Zeiger oder einen Verweis auf die Basis tut Klasse.

In C # ist die grundsätzlich gleiche Trennung zwischen den Typen viel eindeutiger und wird von der Sprache selbst erzwungen. Der Programmierer muss keine Schritte ausführen, um eine Instanz einer Klasse als Referenz zu übergeben, da dies standardmäßig geschieht.

Jerry Sarg
quelle
2
Als C ++ - Fan denke ich, dass dies eine hervorragende Zusammenfassung der Unterschiede zwischen C # und der Swiss Army Chainsaw ist.
David Thornley
1
@ DavidThornley: Ich habe zumindest versucht zu schreiben, was ich für einen etwas ausgeglichenen Vergleich hielt. Nicht mit den Fingern zeigen, aber etwas von dem, was ich sah, als ich das schrieb, fand ich ... etwas ungenau (um es schön auszudrücken).
Jerry Coffin
7

Dies ist aus "C #: Warum brauchen wir eine andere Sprache?" - Gunnerson, Eric:

Einfachheit war ein wichtiges Entwurfsziel für C #.

Es ist möglich, auf Einfachheit und Sprachreinheit zu verzichten, aber Reinheit um der Reinheit willen nützt dem professionellen Programmierer wenig. Wir haben daher versucht, unseren Wunsch nach einer einfachen und prägnanten Sprache in Einklang zu bringen, um die realen Probleme der Programmierer zu lösen.

[...]

Wertetypen , Überladung von Operatoren und benutzerdefinierte Konvertierungen erhöhen die Komplexität der Sprache , ermöglichen jedoch eine enorme Vereinfachung eines wichtigen Benutzerszenarios.

Die Referenzsemantik für Objekte ist eine Möglichkeit, viele Probleme zu vermeiden (natürlich und nicht nur das Aufteilen von Objekten), aber bei Problemen in der realen Welt können manchmal Objekte mit einer Wertsemantik erforderlich sein (z. B. ein Blick auf Klänge, die ich niemals als Referenzsemantik verwenden sollte, oder? aus einem anderen Blickwinkel).

Gibt es einen besseren Ansatz, als diese schmutzigen, hässlichen und schlechten Objekte mit Wertsemantik unter dem Markennamen zu trennen struct?

Manlio
quelle
1
Ich weiß nicht, vielleicht nicht diese schmutzigen, hässlichen und schlechten Objekte mit Referenzsemantik?
Bartek Banachewicz
Vielleicht ... bin ich eine verlorene Sache.
Manlio
2
Einer der größten Mängel im Design von Java ist das Fehlen jeglicher Mittel, um zu deklarieren, ob eine Variable zur Verkapselung von Identität oder Besitz verwendet wird , und einer der größten Mängel in C # ist das Fehlen eines Mittels, um Operationen zu unterscheiden eine Variable aus Operationen an einem Objekt, auf das eine Variable eine Referenz enthält. Selbst wenn sich die Runtime nicht um solche Unterscheidungen kümmerte, würde die Möglichkeit, in einer Sprache anzugeben, ob eine Variable vom Typ int[]gemeinsam genutzt oder geändert werden kann (Arrays können eine von beiden sein, aber im Allgemeinen nicht beide), dazu beitragen, dass falscher Code falsch aussieht.
Supercat
4

Anstatt an ObjectWerttypen zu denken , die von diesen abgeleitet sind, wäre es sinnvoller, sich Speicherorttypen vorzustellen, die in einem völlig anderen Universum als Klasseninstanztypen existieren, aber für jeden Werttyp muss ein entsprechender Heap-Objekttyp vorhanden sein. Ein Speicherort des Strukturtyps enthält einfach eine Verkettung der öffentlichen und privaten Felder des Typs, und der Heap-Typ wird nach einem Muster wie dem folgenden automatisch generiert:

// Defined structure
struct Point : IEquatable<Point>
{
  public int X,Y;
  public Point(int x, int y) { X=x; Y=y; }
  public bool Equals(Point other) { return X==other.X && y==other.Y; }
  public bool Equals(Object other)
  { return other != null && other.GetType()==typeof(this) && Equals(Point(other)); }
  public bool ToString() { return String.Format("[{0},{1}", x, y); }
  public bool GetHashCode() { return unchecked(x+y*65531); }
}        
// Auto-generated class
class boxed_Point: IEquatable<Point>
{
  public Point value; // Fake name; C++/CLI, though not C#, allow full access
  public boxed_Point(Point v) { value=v; }
  // Members chain to each member of the original
  public bool Equals(Point other) { return value.Equals(other); }
  public bool Equals(Object other) { return value.Equals(other); }
  public String ToString() { return value.ToString(); }
  public Int32 GetHashCode() { return value.GetHashCode(); }
}

und für eine Anweisung wie: Console.WriteLine ("Der Wert ist {0}", somePoint);

zu übersetzen als: boxed_Point box1 = new boxed_Point (somePoint); Console.WriteLine ("Der Wert ist {0}", box1);

In der Praxis ist es nicht erforderlich, die Heap-Instanztypen wie z. B. aufzurufen, da Speicherorttypen und Heap-Instanztypen in separaten Universen vorhanden sind boxed_Int32. da das System wissen würde, welche Kontexte die Heap-Objekt-Instanz benötigen und welche einen Speicherort benötigen.

Einige Leute denken, dass alle Werttypen, die sich nicht wie Objekte verhalten, als "böse" angesehen werden sollten. Ich bin der gegenteiligen Ansicht: Da Speicherorte von Werttypen weder Objekte noch Verweise auf Objekte sind, sollte die Erwartung, dass sie sich wie Objekte verhalten sollten, als nicht hilfreich angesehen werden. In Fällen, in denen sich eine Struktur sinnvoll wie ein Objekt verhalten kann, ist es nichts Falsches, wenn sie dies tut, aber jedes structist in seinem Herzen nichts anderes als eine Ansammlung von öffentlichen und privaten Feldern, die mit Klebeband zusammengeklebt sind.

Superkatze
quelle