Ein Struct ist ein Werttyp, den du selbst definierst. Er sieht aus wie eine Klasse, mit Feldern, Properties, Methoden und Konstruktoren, aber eine Variable eines Struct-Typs enthält die Daten direkt statt einer Referenz auf ein Objekt. Dieser eine Unterschied ändert, wie sich Zuweisung, Methodenaufrufe, Gleichheit und null verhalten.
Wertsemantik: Zuweisung kopiert
Die eingebauten numerischen Typen, bool, char, DateTime, TimeSpan und Guid sind alle Structs. Deine eigenen funktionieren genauso:
Ausgabe:
struct: s1.X = 1, s2.X = 50
class: c1.X = 50, c2.X = 50
after Move: s1.X = 1, c1.X = 150
Beim Struct ist s2 eine unabhängige Kopie, und auch Move arbeitet mit einer eigenen Kopie. Damit eine Methode den Struct des Aufrufers ändern kann, übergib ihn per Referenz mit ref (siehe ref und out). Um das Kopieren eines großen Structs ohne Änderungserlaubnis zu vermeiden, hat C# 7.2 in-Parameter eingeführt.
Weitere Folgen davon, ein Werttyp zu sein:
- Eine Struct-Variable kann nie
nullsein.PointS p = null;kompiliert nicht. NimmPointS?(einen Nullable-Werttyp), wenn „kein Wert“ eine Bedeutung hat. - Ein nicht initialisierter Struct besteht nur aus Nullen: numerische Felder
0,bool-Felderfalse, Referenzfeldernull. - Ein Struct kann nicht von einem anderen Struct oder einer Klasse erben, und nichts kann von einem Struct erben. Er kann Interfaces implementieren.
- Lokale Struct-Variablen liegen meist auf dem Stack oder im enthaltenden Objekt, das Erzeugen eines Structs allokiert also nichts auf dem Heap.
Die Falle mit einer Liste von Structs: CS1612
Weil das Lesen eines Structs eine Kopie erzeugt, kompiliert diese harmlos aussehende Zeile nicht:
var points = new List<PointS> { new PointS(1, 2) };
points[0].X = 10;
// error CS1612: Cannot modify the return value of 'List<PointS>.this[int]' because it is not a variable
Der Indexer von List<T> ist eine Methode, die eine Kopie des Elements zurückgibt. X auf dieser temporären Kopie zu setzen würde nichts ändern, deshalb stoppt dich der Compiler. Derselbe Fehler tritt auf, wenn eine Property einen Struct zurückgibt: order.Location.X = 10. Kopieren, ändern, zurückschreiben:
Ausgabe:
35
7
Arrays sind die Ausnahme: array[0] ist das Element selbst, keine Kopie. Der wiederkehrende Ärger hier ist der Grund für den üblichen Rat, Structs unveränderlich zu machen: Wenn sich ein Struct nicht ändern lässt, geht nichts verloren, wenn man eine Kopie ändert.
Konstruktoren und Standardwerte
In C# 7 bis 9 sind die Regeln für Struct-Konstruktoren streng:
- Du kannst keinen parameterlosen Konstruktor deklarieren.
new PointS()existiert immer und setzt jedes Feld auf null. - Ein Konstruktor, den du deklarierst, muss jedes Feld (und jede Auto-Property) zuweisen, bevor er zurückkehrt.
- Feldinitialisierer (
public int X = 1;) sind bei Instanzfeldern nicht erlaubt.
Ausgabe:
19.90 EUR
0.00 (none)
0.00 (none)
0.00 (none)
Money zeigt auch, warum der Standardwert zählt: Ein auf null gesetztes Money hat eine null-Währung, und dein Code muss damit umgehen, weil Arrays, default(T) und nicht initialisierte Felder alle eines erzeugen.
Neuere Versionen haben diese Regeln gelockert:
// C# 10: explicit parameterless constructors and field initializers
public struct Settings
{
public int Retries = 3;
public Settings() { }
}
// C# 11: fields you do not assign in a constructor are zeroed automatically,
// instead of being a compile error.
Ein parameterloser Konstruktor aus C# 10 läuft bei new Settings(), aber nicht bei default(Settings) oder Array-Elementen, die weiterhin nur aus Nullen bestehen. Diese Aufteilung überrascht Leute, nutze sie also vorsichtig.
readonly struct (C# 7.2)
Den Struct selbst als readonly zu markieren lässt den Compiler die Unveränderlichkeit durchsetzen: Jedes Feld muss readonly sein und jede Auto-Property schreibgeschützt.
public readonly struct Temperature
{
public double Celsius { get; }
public Temperature(double celsius) { Celsius = celsius; }
public Temperature WarmerBy(double delta) => new Temperature(Celsius + delta); // returns a new value
}
Neben der dokumentierten Absicht hilft es der Performance: Wird ein nicht readonly Struct in einem readonly-Feld gespeichert oder als in-Parameter übergeben, kopiert der Compiler ihn vor jedem Methodenaufruf (er kann nicht wissen, dass die Methode ihn nicht ändert). Ein readonly struct braucht keine solchen defensiven Kopien. In C# 7.0 kannst du trotzdem jedes Feld readonly machen, wie es Money mit schreibgeschützten Properties tut: Das gibt dir die Unveränderlichkeit, aber nicht die gesparten Kopien, weil der Compiler nur einem als readonly deklarierten Struct vertraut.
Gleichheit
Equals vergleicht bei einem Struct standardmäßig Feld für Feld, was das gewünschte Verhalten eines Werts ist. Aber die Standardimplementierung (ValueType.Equals) kann Reflection verwenden und ist langsam, und der Operator == ist überhaupt nicht definiert: a == b auf deinem eigenen Struct ist Fehler CS0019. Implementiere beides, wenn der Struct verglichen wird:
Ausgabe:
True
True
True
IEquatable<T> zu implementieren zählt bei Collections: HashSet<T>, Dictionary<TKey, TValue> und List<T>.Contains rufen Equals(GridCell) direkt auf, statt jeden Wert zu boxen, um Equals(object) aufzurufen. Record Structs (unten) erzeugen all das für dich.
Boxing
Einen Struct in object oder einen Interface-Typ umzuwandeln boxt ihn: Die Runtime kopiert den Wert in ein neues Heap-Objekt. Die Box und das Original sind danach unabhängig:
Ausgabe:
2
0
Boxing kostet jedes Mal eine Allokation, deshalb waren alte nicht generische Collections wie ArrayList mit Werttypen langsam, und deshalb haben Generics sie ersetzt. Es bedeutet auch, dass ein veränderlicher Struct, auf den über ein Interface zugegriffen wird, in seiner Box geändert wird, nicht im Original, ein weiterer Grund, Structs unveränderlich zu halten.
Struct gegenüber Klasse
| struct | class | |
|---|---|---|
| Art | Werttyp | Referenztyp |
| Zuweisung und Parameter | kopieren die Daten | kopieren die Referenz |
Kann null sein | nein (T? schon) | ja |
| Standardwert | alle Felder auf null | null |
| Vererbung | keine; kann Interfaces implementieren | eine Basisklasse, Interfaces |
== | nicht definiert, außer du überlädst ihn | Referenzgleichheit, außer überladen |
Standard von Equals | vergleicht Felder | vergleicht Referenzen |
| Allokation | inline (Stack oder enthaltendes Objekt) | Heap, vom Garbage Collector verwaltet |
| Gut für | kleine unveränderliche Werte | Entitäten, großer oder geteilter Zustand |
Wann ein Struct passt
Wähle einen Struct, wenn alles davon zutrifft: Der Typ ist ein logischer Wert (eine Koordinate, ein Geldbetrag, eine Farbe, ein Datumsbereich), er ist klein (die Richtlinie von Microsoft nennt etwa 16 Byte, ungefähr vier ints), er ist unveränderlich, und du brauchst keine Vererbung. Structs zahlen sich aus, wenn du sehr viele davon erzeugst, zum Beispiel Millionen Punkte in einem Array, weil sie pro Element eine Heap-Allokation und einen Eintrag für den Garbage Collector vermeiden.
Wähle eine Klasse für alles mit Identität (ein Kunde, eine Bestellung), alles Große, alles, was von mehreren Stellen geändert wird, und wann immer du unsicher bist. Ein großer veränderlicher Struct bringt dir die Kosten des Kopierens plus die Verwirrung durch das Ändern von Kopien.
record struct (C# 10)
C# 10 hat record struct eingeführt, das in einer Zeile Wertgleichheit, ==, ToString und Unterstützung für with für einen Struct erzeugt:
public readonly record struct Coordinate(double Lat, double Lng);
var home = new Coordinate(38.72, -9.14);
var same = new Coordinate(38.72, -9.14);
Console.WriteLine(home == same); // True
Console.WriteLine(home); // Coordinate { Lat = 38.72, Lng = -9.14 }
var north = home with { Lat = 38.80 };
Ein einfaches record struct hat veränderliche positionelle Properties; readonly record struct macht sie init-only, was meist gewollt ist. Die Seite zu Records behandelt die Klassenversion und zeigt die Member, die der Compiler schreibt.
Häufige Fehler
- Einen Struct über eine Kopie ändern.
list[0].X = 1(CS1612), eineforeach-Variable (CS1654), das Ergebnis eines Property-Getters. Schreibe die geänderte Kopie zurück oder mach den Struct unveränderlich. - Große Structs. Jede Zuweisung und jeder Aufruf kopiert alles. Ab einigen Feldern ist eine Klasse meist schneller.
- Den auf null gesetzten Standardwert vergessen. Arrays und
default(T)erzeugen Struct-Werte, ohne deinen Konstruktor auszuführen, Felder, die du dort validierst, können also trotzdem null odernullsein. - Mit
==vergleichen, ohne ihn zu definieren. CS0019. ImplementiereIEquatable<T>und die Operatoren oder nimm ein Record Struct. - Veränderliche Structs hinter Interfaces. Boxing bedeutet, dass die Änderung an einer Kopie passiert, die du nicht ansiehst.
Häufig gestellte Fragen
Was ist ein Struct in C#?
Ein Struct ist ein benutzerdefinierter Werttyp: struct Point { public int X; public int Y; }. Eine Variable eines Struct-Typs enthält die Daten selbst statt einer Referenz auf ein Objekt, Zuweisung und Übergabe an eine Methode kopieren also den ganzen Wert. int, double, DateTime und Guid sind alle Structs.
Was ist der Unterschied zwischen einem Struct und einer Klasse in C#?
Eine Klasse ist ein Referenztyp: Variablen teilen sich ein Objekt, und null ist erlaubt. Ein Struct ist ein Werttyp: Jede Variable hat ihre eigene Kopie, er kann nicht null sein (außer du nimmst Point?), er kann weder erben noch beerbt werden, und sein Standardwert hat alle Felder auf null. Structs passen zu kleinen, unveränderlichen Werten; Klassen zu Entitäten mit Identität und Verhalten.
Wann sollte ich in C# einen Struct statt einer Klasse verwenden?
Wenn der Typ einen einzelnen kleinen Wert darstellt (eine Koordinate, einen Geldbetrag, einen Datumsbereich), idealerweise unveränderlich ist, nach seinem Inhalt verglichen wird und in großer Zahl erzeugt wird, sodass das Vermeiden von Heap-Allokationen zählt. Die Richtlinie von Microsoft nennt außerdem eine Größe von etwa 16 Byte oder weniger. Hat der Typ eine Identität, viele Felder oder wird er über Referenzen verändert, nimm eine Klasse.
Warum bekomme ich „Cannot modify the return value because it is not a variable“?
Das ist Fehler CS1612, typischerweise durch list[0].X = 5 auf einer List<Point> von Structs. Der Indexer gibt eine Kopie des Structs zurück, eine Änderung der Kopie ginge also verloren, und der Compiler verweigert sie. Kopiere das Element in eine Variable, ändere es und weise es zurück: var p = list[0]; p.X = 5; list[0] = p;. Arrays haben dieses Problem nicht, weil array[0] das Element selbst bezeichnet.
Was ist ein readonly struct in C#?
readonly struct (C# 7.2) erklärt, dass kein Member des Structs seinen Zustand ändert: Alle Felder müssen readonly sein und Auto-Properties schreibgeschützt. Der Compiler setzt das durch, und so kann er defensive Kopien vermeiden, wenn der Struct mit in übergeben oder in einem readonly-Feld gespeichert wird. Die meisten Structs sollten readonly sein.