1. Das Problem der Datenübergabe
Angenommen, wir haben eine App für eine Schule und müssen Infos über einen Schüler zwischen verschiedenen Modulen weitergeben: Name, Geburtsjahr, Klasse. Wie machen wir das normalerweise?
Wir erstellen eine Klasse wie:
public class Student
{
public string Name { get; set; }
public int YearOfBirth { get; set; }
public string Class { get; set; }
}
Sieht vertraut aus. Aber dieser Ansatz hat ein paar Probleme:
- Wenn du zwei Studenten auf Gleichheit prüfst, wird standardmäßig nur die Referenz verglichen. Also: Zwei Studenten mit gleichen Feldern, aber unterschiedlichen Objekten — sind nicht gleich!
- Die Klasse kann nach der Erstellung geändert werden, was manchmal zu Fehlern führt (vor allem, wenn das Objekt schon irgendwo verwendet wird);
- Viel „Boilerplate“-Code: Konstruktoren, Vergleichsmethoden, Kopiermethoden (Clones), ToString.
Und wie du dir wahrscheinlich schon gedacht hast: C# kann uns diese Sorgen abnehmen! Hier kommt — record.
2. Was ist ein record?
record ist ein spezieller Typ in C#, der extra fürs Speichern von Daten gemacht wurde. Ein record hat zwei Hauptfeatures:
- Immutabilität (Unveränderlichkeit): Objekte vom Typ record sind standardmäßig unveränderlich, das heißt, ihre Properties werden einmal beim Erstellen gesetzt und können danach nicht mehr geändert werden (genauer gesagt: set-Setter sind privat). Man kann auch veränderliche records machen, aber standardmäßig ist alles immutable.
- Wertvergleich: Wenn zwei record-Objekte in allen Feldern die gleichen Werte haben, gelten sie als gleich (== und .Equals() funktionieren anders!).
Eigentlich ist record die perfekte Hülle, um Daten zwischen App-Schichten zu übergeben (z.B. von der DB zum Controller, vom Controller zum View usw.).
3. Syntax von record
Der einfachste Weg — Positionssyntax
Wenn wir einfach einen Satz Werte übergeben wollen, deklarieren wir den Typ mit einer kurzen Zeile:
public record Student(string Name, int YearOfBirth, string Class);
Was passiert im Hintergrund? Der Compiler generiert für uns:
- Automatische Properties nur zum Lesen (mit privatem Setter);
- Einen Konstruktor, der alle Parameter nimmt;
- Vergleichs- und Kopiermethoden;
- Ein cooles ToString, das die Ausgabe schön formatiert!
Verwendung von positional record
Probieren wir diesen neuen Typ mal in unserer Schul-App aus:
var student1 = new Student("Ivan", 2008, "8A");
var student2 = new Student("Maria", 2008, "8B");
Auf die Properties greifst du wie gewohnt zu (nur ändern geht nicht):
Console.WriteLine($"{student1.Name}, {student1.YearOfBirth}, {student1.Class}");
Versuch, ein Property nach dem Erstellen zu ändern
student1.Name = "Petr"; // Fehler! Property ist read-only.
Wenn du diese Zeile auskommentierst — meckert der Compiler sofort: Wert kann nicht für ein read-only Property gesetzt werden.
So sieht das automatisch generierte ToString aus
Console.WriteLine(student1); // Gibt aus: Student { Name = Ivan, YearOfBirth = 2008, Class = 8A }
Alles schön und verständlich, auch ohne manuelles Formatieren!
4. Vergleich von record-Objekten
Zur Erinnerung: Wenn du zwei verschiedene Objekte einer klassischen Klasse mit gleichen Daten erstellst, sind sie trotzdem nicht gleich:
var a = new Student("Ivan", 2008, "8A");
var b = new Student("Ivan", 2008, "8A");
Console.WriteLine(a == b); // Für Klasse: false
Wenn Student aber ein record ist, funktioniert Gleichheit so, wie du es erwartest:
public record Student(string Name, int YearOfBirth, string Class);
var a = new Student("Ivan", 2008, "8A");
var b = new Student("Ivan", 2008, "8A");
Console.WriteLine(a == b); // Für record: true!
Das heißt: Zwei records mit gleichen Feldern sind gleich, auch wenn es zwei verschiedene Objekte im Speicher sind.
5. Wie sieht das intern aus?
Studierende sind oft überrascht, wie viel der Compiler bei records für uns erledigt. Lass uns mal vergleichen, wie viel Code du für eine Klasse schreiben müsstest und was ein record macht.
Die gute alte Klasse von Hand geschrieben
public class Student
{
public string Name { get; }
public int YearOfBirth { get; }
public string Class { get; }
public Student(string name, int yearOfBirth, string @class)
{
Name = name;
YearOfBirth = yearOfBirth;
Class = @class; //Verweis auf die eigene Klasse
}
public override bool Equals(object? obj)
{
if (obj is not Student other) return false;
return Name == other.Name && YearOfBirth == other.YearOfBirth && Class == other.Class;
}
public override int GetHashCode()
{
return HashCode.Combine(Name, YearOfBirth, Class);
}
public override string ToString()
{
return $"Student {{ Name = {Name}, YearOfBirth = {YearOfBirth}, Class = {Class} }}";
}
}
Kein Wunder, dass Programmierer paranoid werden — wir schreiben das Gleiche zehnmal!
Record — eine Zeile
public record Student(string Name, int YearOfBirth, string Class);
6. Record und Immutabilität: Was geht, was nicht?
Bei records sind Properties standardmäßig read-only, und das verhindert viele Bugs. Aber wenn du unbedingt willst (z.B. für ein sehr altes API), kannst du auch veränderliche records machen:
public record MutableStudent
{
public string Name { get; set; }
public int YearOfBirth { get; set; }
public string Class { get; set; }
}
Jetzt kannst du die Felder ändern, aber du verlierst einige Vorteile (z.B. Sicherheit).
7. Destrukturierung von record
Da die Positionssyntax sehr wie bei Tuples ist, kannst du ein record easy destrukturieren:
var student = new Student("Ivan", 2008, "8A");
var (name, year, className) = student;
Console.WriteLine($"{name} - {year}, {className}"); // Ivan - 2008, 8A
Der Compiler generiert für jedes positional record eine Deconstruct-Methode, was die Arbeit mit LINQ, switch-patterns usw. viel angenehmer macht.
8. Record — Klasse oder struct?
Standardmäßig ist record ein Referenztyp, wie eine Klasse. Also gelten alle Besonderheiten von Referenztypen (Heap, Referenzkopie usw.).
Wenn du einen Werttyp willst (value type), gibt’s in C# auch dafür was — du kannst einfach schreiben:
public record struct Point(int X, int Y);
Aber zum Übergeben von Daten nimmt man fast immer die klassische Form von records, also Referenztyp. Mehr zu record struct gibt’s in den nächsten Vorlesungen :P
Vergleich class, struct, record
| Typ | Immutabilität standardmäßig | Wertvergleich | Einfache Destrukturierung | Auto ToString |
|---|---|---|---|---|
| class | Nein | Nein (per Referenz) | Nein | Nein |
| struct | Nein | Ja | Nein | Nein |
| record | Ja | Ja | Ja | Ja |
9. Besonderheiten und typische Fehler
Viele Anfänger verwechseln die Feinheiten bei records. Zum Beispiel erwarten manche, dass das Ändern eines Feldes bei einem record-Objekt auch ein anderes ändert (wie bei Klassen mit Referenzkopie). Nope! Außerdem: Wenn du with schreibst, denk dran, dass immer eine Kopie erstellt wird, nicht das Original geändert. Records sind perfekt für Logik, wo deine App auf Datenkonsistenz und Vorhersagbarkeit achtet.
Ja, wenn du Felder mit init statt set deklarierst, kannst du sie auch nur beim Erstellen oder mit with setzen, aber nicht später.
GO TO FULL VERSION