9 Stimmen

Sollte ich struct oder class verwenden?

Ich befinde mich in einem klassischen Design-Dilemma. Ich schreibe eine C#-Datenstruktur für einen Wert und Maßeinheit Tupel (z.B. 7,0 Millimeter) und ich frage mich, ob ich einen Referenztyp oder einen Werttyp verwenden sollte.

Die Vorteile einer Struktur sollte es weniger Heap-Aktionen geben, was zu einer besseren Leistung bei Ausdrücken und einer geringeren Belastung des Garbage Collectors führt. Dies wäre normalerweise meine Wahl für einen einfachen Typ wie diesen, aber in diesem konkreten Fall gibt es Nachteile.

Das Tupel ist Teil eines recht allgemeinen Rahmens für Analyseergebnisse, bei dem die Ergebnisse in einer WPF-Anwendung je nach Typ des Ergebniswerts auf unterschiedliche Weise dargestellt werden. Diese Art der schwachen Typisierung wird von WPF mit all seinen Datenvorlagen, Wertkonvertierungen und Vorlagenselektoren außergewöhnlich gut gehandhabt. Die Implikation ist, dass der Wert eine Menge von Boxing / Unboxing durchlaufen wird, wenn mein Tupel als Struktur dargestellt wird. Tatsächlich wird die Verwendung des Tupels in Ausdrücken geringer sein als die Verwendung in Boxing-Szenarien. Um das ganze Boxing zu vermeiden, überlege ich, meinen Typ als Klasse zu deklarieren. Eine weitere Sorge über eine Struktur ist, dass es Fallstricke mit Zwei-Wege-Bindung in WPF sein könnte, da es einfacher wäre, am Ende mit Kopien der Tupel irgendwo im Code statt Referenzkopien.

Ich habe auch einige bequeme Operator-Überladung. Mit überladenen Vergleichsoperatoren kann ich beispielsweise problemlos Millimeter mit Zentimetern vergleichen. Allerdings gefällt mir die Idee nicht, == und != zu überladen, wenn mein Tupel eine Klasse ist, da die Konvention besagt, dass == und != ReferenceEquals für Referenztypen ist (im Gegensatz zu System.String, was eine andere klassische Diskussion ist). Wenn == und != überladen wird, wird jemand schreiben if (myValue == null) und eine unangenehme Laufzeitausnahme erhalten, wenn myValue sich eines Tages als null herausstellt.

Ein weiterer Aspekt ist, dass es in C# (anders als z.B. in C++) keine klare Möglichkeit gibt, Referenz- und Werttypen im Code zu unterscheiden, obwohl die Semantik sehr unterschiedlich ist. Ich befürchte, dass Benutzer meines Tupels (wenn es als struct deklariert ist) davon ausgehen, dass der Typ eine Klasse ist, da die meisten benutzerdefinierten Datenstrukturen dies sind und eine Referenzsemantik annehmen. Das ist ein weiteres Argument, warum man Klassen bevorzugen sollte, einfach weil es das ist, was der Benutzer erwartet und es gibt keine "." / "->", um sie zu unterscheiden. Im Allgemeinen würde ich fast immer eine Klasse verwenden, es sei denn, mein Profiler sagt mir, eine Struktur zu verwenden, einfach weil die Klassensemantik am ehesten von anderen Programmierern erwartet wird und C# nur vage Hinweise gibt, ob es das eine oder das andere ist.

Meine Fragen lauten also:

Welche anderen Überlegungen sollte ich anstellen, wenn ich entscheide, ob ich mich für einen Wert- oder einen Referenzwert entscheiden soll?

Kann == / != Überladung in einer Klasse gerechtfertigt sein in jede Umstände?

Programmierer nehmen Dinge an. Die meisten würden wahrscheinlich davon ausgehen, dass etwas namens "Punkt" ein Wertetyp ist. Was würden Sie annehmen, wenn Sie einen Code mit einem "UnitValue" lesen würden?

Was würden Sie angesichts meiner Nutzungsbeschreibung wählen?

9voto

Adam Robinson Punkte 176996

Die Vorteile einer Struktur sollte weniger Heap-Aktion geben mir eine bessere Leistung in Ausdrücke und weniger Stress auf den Garbage Collector sein

Ohne jeglichen Kontext ist dies eine gewaltige - und gefährliche - Übergeneralisierung. Eine Struktur ist no automatisch für den Stapel in Frage. Eine Struktur kann auf dem Stack abgelegt werden, wenn (und nur wenn) ihre Lebensdauer und Exposition sich nicht über die Funktion, die sie deklariert, hinaus erstreckt, sie nicht innerhalb dieser Funktion gepackt wird und wahrscheinlich noch eine Menge anderer Kriterien, die mir nicht sofort einfallen. Das bedeutet, dass die Einbindung in einen Lambda-Ausdruck oder einen Delegaten bedeutet, dass sie sowieso auf dem Heap gespeichert wird. Es geht darum, sich keine Sorgen zu machen, denn mit 99,9 % Wahrscheinlichkeit liegen die Engpässe woanders. .

Was das Überladen von Operatoren angeht, so hindert Sie nichts daran (weder technisch noch philosophisch), Operatoren auf Ihren Typ zu überladen. Technisch gesehen haben Sie zwar Recht, dass Gleichheitsvergleiche zwischen Referenztypen standardmäßig semantisch äquivalent sind zu object.ReferenceEquals ist dies keine allgemeingültige Regel. Es gibt zwei grundlegende Dinge, die man bei der Überladung von Operatoren beachten sollte:

1.) (Und dies ist aus praktischer Sicht vielleicht das Wichtigste) Die Betreiber sind no polymorph. Das heißt, Sie werden nur Operatoren verwenden, die für die Typen wie sie referenziert sind und nicht so, wie sie tatsächlich existieren.

Wenn ich zum Beispiel einen Typ deklariere Foo die einen überladenen Gleichheitsoperator definiert, der immer einen true dann mache ich dies:

Foo foo1 = new Foo();
Foo foo2 = new Foo();
object obj1 = foo1;

bool compare1 = foo1 == foo2; // true
bool compare2 = foo1 == obj1; // false

Auch wenn obj1 ist in Wirklichkeit eine Instanz von Foo Der überladene Operator existiert nicht auf der Ebene der Typenhierarchie, auf die ich die Instanz referenziere, die in der Datei obj1 Referenz, so dass sie auf den Referenzvergleich zurückgreift.

2.) Vergleichsoperationen sollten deterministisch sein. Es sollte nicht möglich sein, dieselben zwei Instanzen mit dem überladenen Operator zu vergleichen und unterschiedliche Ergebnisse zu erzielen. In der Praxis führt diese Art von Anforderung normalerweise dazu, dass die Typen unveränderlich sind (da die Fähigkeit, den Unterschied zwischen einem oder mehreren Werten in einer Klasse zu erkennen und dennoch true aus einem Gleichheitsoperator ist eher kontraintuitiv), aber im Grunde bedeutet es nur, dass Sie sollten nicht in der Lage sein, einen Zustandswert innerhalb einer Instanz zu ändern, der das Ergebnis einer Vergleichsoperation verändert . Wenn es in Ihrem Szenario Sinn macht, mutieren zu können einige der Instanzzustandsinformationen, ohne dass sich dies auf das Ergebnis eines Vergleichs auswirkt, dann gibt es keinen Grund, warum Sie dies nicht tun sollten. Das ist nur ein seltener Fall.

1voto

Konrad Rudolph Punkte 503837

Allerdings gefällt mir die Idee nicht, == und != zu überladen, wenn mein Tupel eine Klasse ist, da die Konvention ist, dass == und != ReferenceEquals für Referenztypen ist

Nein, die Konvention ist etwas anders:

(im Gegensatz zu System.String, was eine weitere klassische Diskussion ist).

Nein, es ist dieselbe Diskussion.

Der springende Punkt ist nicht, ob ein Typ ein Referenz Typ. - Es geht darum, ob der Typ verhält sich als Wert. Dies gilt für String und dies sollte für jede Klasse gelten, für die Sie eine Überladung vornehmen möchten operator == y != .

Es gibt nur eine Sache, auf die Sie achten sollten, wenn Sie einen Typ entwerfen, der logischerweise ein Wert ist: Machen Sie ihn unveränderlich (siehe andere Diskussionen hier auf Stack Overflow), und implementieren Sie die Vergleichssemantik richtig:

Wenn == und != überladen sind, wird jemand if (myValue == null) schreiben und eine böse Laufzeitausnahme erhalten, wenn sich myValue eines Tages als null herausstellt.

Es sollte keine Ausnahme geben (immerhin, (string)null == null auch keine Ausnahme auslöst!), wäre dies ein Fehler in der Implementierung des überladenen Operators.

0voto

Ronald Wildenberg Punkte 30961

Vielleicht können Sie sich inspirieren lassen von dieser aktuelle Blogbeitrag von Eric Lippert. Das Wichtigste, was man bei der Verwendung von Structs beachten sollte, ist sie unveränderbar machen . Hier ist eine interessante Blog-Beitrag von Jon Skeet, wo eine veränderbare Struktur zu sehr schwer zu debuggenden Problemen führen kann.

0voto

Eilistraee Punkte 8040

Ich bin mir nicht sicher, dass die Leistungseinbußen durch das Boxing/Unboxing Ihres Wertes im UI-Code hier Ihr Hauptanliegen sein sollten. Diese Leistung Hit wird gering sein, verglichen mit dem Layout-Prozess zum Beispiel.

Sie könnten Ihre Frage auch anders formulieren: Wollen Sie, dass Ihr Typ veränderbar oder unveränderbar ist? Ich denke, Unveränderlichkeit wäre bei Ihren Spezifikationen logisch. Es ist ein Wert, Sie haben es selbst gesagt, indem Sie ihn UnitValue genannt haben. Als Entwickler wäre ich ziemlich überrascht, dass ein UnitValue kein Wert ist ;) => Verwenden Sie eine unveränderliche Struktur

Außerdem hat die Null keinen Sinn für eine Messung. Auch die Gleichheit und der Vergleich sollten nach den Regeln der Messung durchgeführt werden.

Nein, ich sehe keinen sachdienlichen Grund, in Ihrem Fall einen Ref-Typ anstelle eines Werttyps zu verwenden.

0voto

TheFogger Punkte 2359

Meiner Meinung nach verlangt Ihr Entwurf nach einem Wertetyp Semantik für Ihr Tupel. <7.0, mm> sollte vom Standpunkt des Programmierers immer gleich <7.0, mm> sein. <7.0, mm> ist genau die Summe seiner Teile und hat keine eigene Identität. Alles andere würde ich sehr verwirrend finden. Diese Art impliziert auch Unveränderlichkeit.

Ob Sie dies nun mit Structs oder Klassen implementieren, hängt von der Leistung ab und davon, ob Sie Nullwerte für jedes Tupel unterstützen müssen. Wenn Sie sich für Structs entscheiden, können Sie mit Nullable auskommen, wenn Sie Null nur in einigen wenigen Fällen unterstützen müssen.

Können Sie nicht auch einen Referenztyp-Wrapper für Ihre Tupel bereitstellen, der für Anzeigezwecke verwendet wird? Ich bin nicht zu vertraut mit WPF, aber ich würde mir vorstellen, dass dies alle die Boxing-Operationen beseitigen würde.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X