1. Warum wurden record überhaupt eingeführt?
In C# (und .NET generell) waren lange Zeit die Hauptbausteine Klassen (class) und Strukturen (struct). Aber beide sind für manche Aufgaben nicht optimal. Klassen sind Referenztypen, veränderbar und werden standardmäßig nach Referenz verglichen (mit sehr wenigen Ausnahmen). Strukturen sind Werttypen (werden beim Übergeben kopiert), werden standardmäßig byteweise verglichen und sind meistens veränderbar (bis readonly struct eingeführt wurde).
Wenn du aber einfach und natürlich Daten speichern willst, die sich bequem nach Wert vergleichen lassen, schnell geklont werden können und du dir keine Sorgen machen willst, dass dein Objekt irgendwo verändert wird – musstest du bisher viele Workarounds bauen oder auf Libraries wie ValueTuple oder sogar System.Tuple zurückgreifen. Aber die sind alle nicht so schön und ausdrucksstark, wie man es gerne hätte.
Deshalb kam in C# 9 der record – ein Datentyp, der kurze Deklaration, sichere Unveränderlichkeit und wertbasiertes Verhalten kombiniert.
2. Alle vier Typen auf einen Blick
| class | struct | record | record struct | |
|---|---|---|---|---|
| Kategorie | Referenztyp | Werttyp | Referenztyp | Werttyp |
| Mutierbarkeit | Standardmäßig – ja | Standardmäßig – ja | Standardmäßig – nein (init) | Standardmäßig – nein (init) |
| Vergleich | Nach Referenz (==) | Nach Wert (Felder) | Nach Wert (Felder/Properties) | Nach Wert (Felder) |
| Klonen | Nur manuell | Nur manuell | Eingebaute Unterstützung (with) | Eingebaute Unterstützung (with) |
| Vererbung | Ja | Nein | Ja | Nein |
| Unveränderlichkeit | Muss selbst implementiert werden | Muss selbst implementiert werden | Sehr einfach umzusetzen | Sehr einfach umzusetzen |
| Syntax | Am längsten | Kurz | Am kürzesten (positional) | Ziemlich kurz |
| Verwendung in Collections | Nach Referenz | Kopien | Nach Referenz | Kopien |
Visuelles Schema
+----------------+ +----------------+ +--------------------+
| class | | struct | | record |
+----------------+ +----------------+ +--------------------+
| Reference Type | | Value Type | | Reference Type |
| Mutable | | Mutable | | Immutable (init) |
| == : Reference | | == : By Fields | | == : By Value |
+----------------+ +----------------+ +--------------------+
3. Die eigentlichen Unterschiede zwischen record, class und struct
Verhalten im Speicher: Referenz oder Wert?
- class und record sind Referenztypen. Wenn du sie an eine Funktion übergibst, wird die Referenz auf das Objekt kopiert.
- struct und record struct sind Werttypen. Sie werden immer byteweise kopiert (außer du übergibst sie explizit als Referenz).
class PointClass { public int X; public int Y; }
struct PointStruct { public int X; public int Y; }
record PointRecord(int X, int Y);
record struct PointRecordStruct(int X, int Y);
void Demo()
{
var pc = new PointClass { X = 1, Y = 2 };
var ps = new PointStruct { X = 1, Y = 2 };
var pr = new PointRecord(1, 2);
var prs = new PointRecordStruct(1, 2);
ChangeY(pc); // pc.Y wird geändert!
ChangeY(ps); // ps.Y wird nicht geändert – Kopie!
ChangeY(pr); // pr.Y wird geändert!
ChangeY(prs); // prs.Y wird nicht geändert – Kopie!
}
void ChangeY(dynamic p) { p.Y = 99; }
Falls dir dynamic hier wie Magie vorkommt – ja, das ist nur fürs Beispiel, damit du siehst, dass sich bei structs der Wert nicht ändert, aber bei class oder record (Referenztyp) schon.
Vergleich: Wie weiß ich, ob zwei Objekte gleich sind?
- class: Vergleich nach Referenz (== – true nur, wenn es dasselbe Objekt im Speicher ist), außer du überschreibst Equals.
- struct: Vergleich nach Wert aller Felder (standardmäßig).
- record: Vergleich nach Wert aller Felder/Properties, die im primären Konstruktor angegeben sind.
class Foo { public int A; public int B; }
record Bar(int A, int B);
var foo1 = new Foo { A = 42, B = 1 };
var foo2 = new Foo { A = 42, B = 1 };
var bar1 = new Bar(42, 1);
var bar2 = new Bar(42, 1);
Console.WriteLine(foo1 == foo2); // False! Verschiedene Objekte
Console.WriteLine(bar1 == bar2); // True! Werte sind gleich
Fun Fact:
Mit record struct ist es noch cooler: Sie vergleichen "nach Wert" wie normale structs, aber mit Syntax und Features von record.
4. Unveränderlichkeit: Wer garantiert was?
Vergleich der Objektsicherheit:
- class: Standardmäßig leicht veränderbar, außer du machst alle Felder explizit readonly.
- struct: Dasselbe, aber du kannst readonly struct deklarieren, dann können Felder und Properties nicht geändert werden.
- record: Felder werden meist mit init deklariert, d.h. sie können nur im Konstruktor oder Objekt-Initializer (with) gesetzt werden. Das ist praktisch für sichere Datenübergabe.
- record struct: Auch hier kannst du readonly record struct machen und bekommst Unveränderlichkeit für Werttypen mit den coolen record-Features.
record Person(string Name, int Age);
var p1 = new Person("Alexej", 23);
// p1.Age = 24; // Fehler! Nur init
var p2 = p1 with { Age = 24 }; // Funktioniert! Erstellt Kopie mit neuen Daten
Wenn du ein großes Projekt mit Dutzenden von Entity-Objekten hast, helfen dir record, viele Bugs zu vermeiden, die entstehen, wenn "irgendwo jemand ein Feld geändert hat – und alles ist kaputt".
5. Syntax: Wie sehen Deklarationen aus und wie bleibt man klar?
// class
public class Product
{
public int Id { get; init; }
public string Name { get; init; }
}
// struct
public struct Point
{
public int X { get; set; }
public int Y { get; set; }
}
// record
public record Product(int Id, string Name);
// record struct
public record struct Point(int X, int Y);
Wie du siehst, ist die Deklaration bei record-Typen am kürzesten. Du bekommst Konstruktor, Destruktor, Gleichheit, ToString() und vieles mehr "out of the box".
| Syntax | Was du gratis bekommst | Kann man erben? |
|---|---|---|
| class | fast nichts | Ja |
| struct | fast nichts | Nein |
| record | ToString, Equals, Deconstruct, with | Ja |
| record struct | ToString, Equals, Deconstruct, with | Nein |
6. Klonen und der Operator with
Nur record und record struct haben den speziellen Operator with, mit dem du ein Objekt easy kopieren und einzelne Properties ändern kannst.
record User(string Name, int Age);
var user1 = new User("Irina", 28);
var user2 = user1 with { Age = 29 }; // user2.Name == "Irina", user2.Age == 29
Bei einer Klasse (class) musst du eine Kopiermethode selbst schreiben, und wenn du vergisst, ein Feld zu kopieren – hallo Bugs.
7. Vererbung: Wer kann was?
- class: Unterstützt Standardvererbung (Klassenhierarchien, virtuelle Methoden, Abstraktionen usw.).
- struct: Unterstützt keine Vererbung (kann nur Interfaces implementieren).
- record: Unterstützt Vererbung, aber mit Einschränkungen (z.B. Vererbung nur zwischen record-Typen, nicht mit class).
- record struct: Unterstützt keine Vererbung, wie normale structs.
record Animal(string Name);
record Dog(string Name, string Breed) : Animal(Name); // Okay!
class Vehicle { }
class Car : Vehicle { } // Okay!
// struct kann nicht von struct erben
Mehr zur Vererbung im nächsten Level :P
8. Wo werden welche Typen in der Praxis eingesetzt?
- class: Große Objekte mit viel Verhalten, langem Lebenszyklus, veränderbarem Zustand, Hierarchie (z.B. Business-Logik, UI-Komponenten).
- struct: Kleine Wertobjekte, wo schnelles Kopieren, kein GC und minimaler Overhead wichtig sind (z.B. Koordinaten, Farben, Summen – alles, was sich leicht und schnell klonen lässt).
- record: DTO (Data Transfer Object), Parameterobjekte, Konfigurationsparameter, unveränderliche Zustände, Berechnungsergebnisse, die sich bequem nach Inhalt vergleichen lassen.
- record struct: Werttypen, wo Unveränderlichkeit und record-Verhalten gebraucht werden, aber ohne unnötige Heap-Allocations.
9. Typische Fehler und Stolperfallen
Manche Entwickler denken, dass record einfach ein "Ersatz für class" ist. Das stimmt nicht! Wenn du ein Objekt brauchst, das seinen Zustand im Lebenszyklus ändern soll – nimm class.
Wenn du Objekte nach Referenz vergleichen willst (z.B. im Singleton-Pattern oder wenn der Lebenszyklus kritisch ist) – nimm class.
Wenn du ein Wertobjekt brauchst, das sich wie eine Zahl oder ein Punkt im Koordinatensystem verhält – struct oder record struct.
Wenn du mit unveränderlichen, leicht vergleichbaren Objekten arbeitest, die oft zwischen App-Schichten übergeben, in Collections gespeichert, geloggt und serialisiert werden – dann ist record dein Freund.
Denk auch daran: Wenn du einen record-Klasse mit nur lesbaren Properties deklarierst, aber die verschachtelten Objekte vergisst – die inneren Felder können trotzdem verändert werden, wenn sie selbst mutierbar sind.
record Student(string Name, List<int> Noten);
var s1 = new Student("Anton", new List<int>() {5,5,5});
var s2 = s1 with { };
s2.Noten.Add(2); // Beide Objekte sind jetzt "verflucht" mit einer 2! s1.Noten == s2.Noten
GO TO FULL VERSION