Datensätze (C#-Referenz)

Ab C# 9 verwenden Sie das record-Schlüsselwort, um einen Verweistyp zu definieren, der integrierte Funktionen zum Kapseln von Daten bereitstellt. Sie können Datensatztypen mit unveränderlichen Eigenschaften erstellen, indem Sie Positionsparameter oder Standardeigenschaftensyntax verwenden:

public record Person(string FirstName, string LastName);
public record Person
{
    public string FirstName { get; init; } = default!;
    public string LastName { get; init; } = default!;
};

Sie können auch Datensatztypen mit änderbaren Eigenschaften und Feldern erstellen:

public record Person
{
    public string FirstName { get; set; } = default!;
    public string LastName { get; set; } = default!;
};

Datensätze können zwar änderbar sein, sind jedoch primär dafür vorgesehen, unveränderliche Datenmodelle zu unterstützen. Der Datensatztyp bietet die folgenden Funktionen:

Sie können auch Strukturtypen verwenden, um datenzentrierte Typen zu entwerfen, die Wertgleichheit und wenig oder kein Verhalten bereitstellen. In C# 10.0 und höher können Sie record struct-Typen mithilfe von Positionsparametern oder Standardeigenschaftssyntax definieren:

public readonly record struct Point(double X, double Y, double Z);
public record struct Point
{
    public double X {  get; init; }
    public double Y {  get; init; }
    public double Z {  get; init; }
}

Datensatzstrukturen können ebenfalls veränderlich sein, sowohl Positionsdatensatzstrukturen als auch Datensatzstrukturen ohne Positionsparameter:

public record struct DataMeasurement(DateTime TakenAt, double Measurement);
public record struct Point
{
    public double X { get; set; }
    public double Y { get; set; }
    public double Z { get; set; }
}

Die Beispiele oben zeigen einige Unterschiede zwischen Datensätzen, die Verweistypen sind, und Datensätzen, die Werttypen sind:

  • Das record- oder record class-Element deklariert einen Verweistyp. Das class-Schlüsselwort ist optional, kann aber für Leser mehr Klarheit schaffen. Das record struct-Element deklariert einen Werttyp.
  • Positionseigenschaften sind in record class- und readonly record struct-Elementen unveränderlich. Sie sind in record struct-Elementen veränderbar.

Im weiteren Verlauf dieses Artikels werden sowohl record class- als auch record struct-Typen erläutert. Die Unterschiede werden in den einzelnen Abschnitten ausführlich beschrieben. Sie sollten sich zwischen record class- und record struct-Elementen entscheiden, ähnlich wie bei der Entscheidung zwischen class- und struct-Elementen. Der Begriff Datensatz wird verwendet, um Verhalten zu beschreiben, das für alle Datensatztypen gilt. record struct oder record class wird verwendet, um das Verhalten zu beschreiben, das nur für Struktur- bzw. Klassentypen gilt.

Positionssyntax für die Eigenschaftendefinition

Sie können Positionsparameter verwenden, um die Eigenschaften eines Datensatzes zu deklarieren und die Eigenschaftswerte zu initialisieren, wenn Sie eine Instanz erstellen:

public record Person(string FirstName, string LastName);

public static void Main()
{
    Person person = new("Nancy", "Davolio");
    Console.WriteLine(person);
    // output: Person { FirstName = Nancy, LastName = Davolio }
}

Wenn Sie die Positionssyntax für die Eigenschaftendefinition verwenden, erstellt der Compiler Folgendes:

  • Eine öffentliche, automatisch implementierte Init-only-Eigenschaft für jeden Positionsparameter, der in der Datensatzdeklaration bereitgestellt wird.
    • Für record- und readonly record struct-Typen: Eine init-only-Eigenschaft kann nur im Konstruktor oder mit einem Eigenschafteninitialisierer festgelegt werden.
    • Für record struct-Typen: Eine Lese-/Schreibeigenschaft, die in einem Konstruktor, einem Eigenschafteninitialisierer oder einer Zuweisung nach der Erstellung festgelegt werden kann.
  • Ein primärer Konstruktor, dessen Parameter mit den Parametern mit fester Breite der Datensatzdeklaration übereinstimmen
  • Bei Datensatzstrukturtypen ein parameterloser Konstruktor, der jedes Feld auf seinen Standardwert festlegt.
  • Eine Deconstruct-Methode mit einem out-Parameter für jeden Positionsparameter, der in der Datensatzdeklaration bereitgestellt wird. Diese Methode wird nur bereitgestellt, wenn zwei oder mehr Positionsparameter vorhanden sind. Die Methode dekonstruiert Eigenschaften, die mithilfe der Positionssyntax definiert wurden. Es werden Eigenschaften ignoriert, die mithilfe der Standardeigenschaftensyntax definiert werden.

Sie sollten einem dieser Elemente, die der Compiler aus der Datensatzdefinition erstellt, Attribute hinzufügen. Sie können einem beliebigen Attribut, das Sie auf die Eigenschaften des Datensatzes mit Feldern fester Breite anwenden, ein Ziel hinzufügen. Im folgenden Beispiel wird System.Text.Json.Serialization.JsonPropertyNameAttribute auf jede Eigenschaft des Person-Datensatzes angewendet. Das property:-Ziel gibt an, dass das Attribut auf die vom Compiler generierte Eigenschaft angewendet wird. Andere Werte sind beispielsweise field: zum Anwenden des Attributs auf das Feld und param: zum Anwenden des Attributs auf den Parameter.

/// <summary>
/// Person record type
/// </summary>
/// <param name="FirstName">First Name</param>
/// <param name="LastName">Last Name</param>
/// <remarks>
/// The person type is a positional record containing the
/// properties for the first and last name. Those properties
/// map to the JSON elements "firstName" and "lastName" when
/// serialized or deserialized.
/// </remarks>
public record Person([property: JsonPropertyName("firstName")]string FirstName, 
    [property: JsonPropertyName("lastName")]string LastName);

Das vorherige Beispiel zeigt auch, wie XML-Dokumentationskommentare für den Datensatz erstellt werden. Sie können das <param>-Tag hinzufügen, um die Dokumentation für die Parameter des primären Konstruktors hinzuzufügen.

Wenn die generierte, automatisch implementierte Eigenschaftendefinition nicht das Gewünschte ist, können Sie eine eigene Eigenschaft mit demselben Namen definieren. Wenn Sie dies tun, verwenden der generierte Konstruktor und Dekonstruktor Ihre Eigenschaftendefinition. Im folgenden Beispiel werden beispielsweise die FirstName- und LastName-Eigenschaften eines Positionsdatensatzes public deklariert, der Positionsparameter Id wird jedoch auf internal beschränkt. Sie können diese Syntax für Datensätze und Datensatzstrukturtypen verwenden. Sie müssen die explizite Zuweisung der deklarierten Eigenschaft dem entsprechenden Positionsparameter hinzufügen.

public record Person(string FirstName, string LastName, string Id)
{
    internal string Id { get; init; } = Id;
}

public static void Main()
{
    Person person = new("Nancy", "Davolio", "12345");
    Console.WriteLine(person.FirstName); //output: Nancy

}

Ein Datensatztyp muss keine Positionseigenschaften deklarieren. Sie können einen Datensatz ohne Positionseigenschaften deklarieren, und Sie können andere Felder und Eigenschaften deklarieren, wie im folgenden Beispiel gezeigt:

public record Person(string FirstName, string LastName)
{
    public string[] PhoneNumbers { get; init; } = Array.Empty<string>();
};

Wenn Sie Eigenschaften mit der Standardeigenschaftensyntax definieren, aber den Zugriffsmodifizierer weglassen, sind die Eigenschaften implizit private.

Unveränderlichkeit

Ein Positionsdatensatz und eine schreibgeschützte Positionsdatensatzstruktur deklarieren init-only-Eigenschaften. Eine Positionsdatensatzstruktur deklariert Lese-/Schreibeigenschaften. Sie können diese Standardwerte überschreiben, wie im vorherigen Abschnitt gezeigt.

Die Unveränderlichkeit kann hilfreich sein, wenn Sie einen threadsicheren datenzentrierten Typ benötigen oder davon abhängig sind, dass ein Hashcode in einer Hashtabelle unverändert bleibt. Die Unveränderlichkeit ist jedoch nicht für alle Datenszenarios geeignet. Entity Framework Core unterstützt beispielsweise keine Updates mit unveränderlichen Entitätstypen.

Unabhängig davon, ob init-only-Eigenschaften aus Positionsparametern (record class oder readonly record struct) oder durch Angabe von init-Zugriffsmethoden erstellt wurden, weisen sie eine flache Unveränderlichkeit auf. Nach der Initialisierung können Sie den Wert der Werttypeigenschaften oder den Verweis der Verweistypeigenschaften nicht ändern. Allerdings können die Daten, auf die eine Verweistypeigenschaft verweist, geändert werden. Das folgende Beispiel zeigt, dass der Inhalt einer unveränderlichen Verweistypeigenschaft (in diesem Fall ein Array) änderbar ist:

public record Person(string FirstName, string LastName, string[] PhoneNumbers);

public static void Main()
{
    Person person = new("Nancy", "Davolio", new string[1] { "555-1234" });
    Console.WriteLine(person.PhoneNumbers[0]); // output: 555-1234

    person.PhoneNumbers[0] = "555-6789";
    Console.WriteLine(person.PhoneNumbers[0]); // output: 555-6789
}

Die für Datensatztypen eindeutigen Features werden von durch den Compiler synthetisierte Methoden implementiert, und keine dieser Methoden beeinträchtigt die Unveränderlichkeit durch Ändern des Objektzustands. Sofern nicht angegeben, werden die synthetisierten Methoden für record-, record struct- und readonly record struct-Deklarationen generiert.

Wertgleichheit

Für jeden Typ, den Sie definieren, können Sie Object.Equals(Object) überschreiben und operator == überladen. Wenn Sie Equals nicht überschreiben oder operator == überladen, bestimmt der von Ihnen deklarierte Typ, wie Gleichheit definiert wird:

  • Bei class-Typen sind zwei Objekte gleich, wenn sie auf das gleiche Objekt im Arbeitsspeicher verweisen.
  • Bei record-Typen sind zwei Objekte gleich, wenn sie die gleichen Werte speichern und denselben Typ aufweisen.
  • Bei struct-Typen sind zwei Objekte gleich, wenn sie die gleichen Werte speichern.
  • Bei record struct- und readonly record struct-Typen sind zwei Objekte gleich, wenn sie die gleichen Werte speichern.

Die Definition von Gleichheit für record struct entspricht der Definition für struct. Der Unterschied besteht darin, dass sich die Implementierung für struct in ValueType.Equals(Object) befindet und auf Reflexion basiert. Bei record struct wird die Implementierung vom Compiler synthetisiert und verwendet die deklarierten Datenmember.

Verweisgleichheit ist für einige Datenmodelle erforderlich. Entity Framework Core hängt beispielsweise von der Verweisgleichheit ab, um sicherzustellen, dass für konzeptionell eine Entität nur eine Instanz eines Entitätstyps verwendet wird. Aus diesem Grund sind Datensätze und Datensatzstrukturen für die Verwendung als Entitätstypen in Entity Framework Core nicht geeignet.

Im folgenden Beispiel wird die Wertgleichheit von Datensatztypen veranschaulicht:

public record Person(string FirstName, string LastName, string[] PhoneNumbers);

public static void Main()
{
    var phoneNumbers = new string[2];
    Person person1 = new("Nancy", "Davolio", phoneNumbers);
    Person person2 = new("Nancy", "Davolio", phoneNumbers);
    Console.WriteLine(person1 == person2); // output: True

    person1.PhoneNumbers[0] = "555-1234";
    Console.WriteLine(person1 == person2); // output: True

    Console.WriteLine(ReferenceEquals(person1, person2)); // output: False
}

Zum Implementieren von Wertgleichheit synthetisiert der Compiler die folgenden Methoden:

Sie können eigene Implementierungen schreiben, um diese synthetisierten Methoden zu ersetzen. Wenn ein Datensatztyp über eine Methode verfügt, die mit der Signatur einer beliebigen synthetisierten Methode übereinstimmt, wird diese Methode vom Compiler nicht synthetisiert.

Wenn Sie Ihre eigene Implementierung von Equals in einem Datensatztyp bereitstellen, stellen Sie auch eine Implementierung von GetHashCode bereit.

Nichtdestruktive Mutation

Wenn eine Instanz mit einigen Änderungen kopieren müssen, können Sie einen with-Ausdruck verwenden, um eine nichtdestruktive Mutation zu erzielen. Ein with-Ausdruck erstellt eine neue Datensatzinstanz, bei der es sich um eine Kopie einer vorhandenen Datensatzinstanz handelt, bei der bestimmte Eigenschaften und Felder geändert wurden. Mit der Objektinitialisierersyntax können Sie die zu ändernden Werte angeben, wie im folgenden Beispiel gezeigt:

public record Person(string FirstName, string LastName)
{
    public string[] PhoneNumbers { get; init; }
}

public static void Main()
{
    Person person1 = new("Nancy", "Davolio") { PhoneNumbers = new string[1] };
    Console.WriteLine(person1);
    // output: Person { FirstName = Nancy, LastName = Davolio, PhoneNumbers = System.String[] }

    Person person2 = person1 with { FirstName = "John" };
    Console.WriteLine(person2);
    // output: Person { FirstName = John, LastName = Davolio, PhoneNumbers = System.String[] }
    Console.WriteLine(person1 == person2); // output: False

    person2 = person1 with { PhoneNumbers = new string[1] };
    Console.WriteLine(person2);
    // output: Person { FirstName = Nancy, LastName = Davolio, PhoneNumbers = System.String[] }
    Console.WriteLine(person1 == person2); // output: False

    person2 = person1 with { };
    Console.WriteLine(person1 == person2); // output: True
}

Der with-Ausdruck kann Positionseigenschaften festlegen oder Eigenschaften, die mithilfe der Standardeigenschaftensyntax erstellt werden. Damit Nicht-Positionseigenschaften in einem with-Ausdruck geändert werden können, müssen sie eine init- oder set-Zugriffsmethode aufweisen.

Das Ergebnis eines with-Ausdrucks ist eine flache Kopie. Dies bedeutet, dass bei einer Verweiseigenschaft nur der Verweis auf eine Instanz kopiert wird. Sowohl der ursprüngliche Datensatz als auch die Kopie verfügen über einen Verweis auf dieselbe Instanz.

Um dieses Feature für record class-Typen zu implementieren, synthetisiert der Compiler eine Klonmethode und einen Kopierkonstruktor. Die virtuelle Klonmethode gibt einen neuen Datensatz zurück, der vom Kopierkonstruktor initialisiert wurde. Wenn Sie einen with-Ausdruck verwenden, erstellt der Compiler Code, der die Klonmethode aufruft, und legt dann die Eigenschaften fest, die im with-Ausdruck angegeben werden.

Wenn Sie ein anderes Kopierverhalten benötigen, können Sie einen eigenen Kopierkonstruktor in record class schreiben. Wenn Sie dies tun, synthetisiert der Compiler keinen Kopierkonstruktor. Legen Sie den Konstruktor als private fest, wenn der Datensatz sealed ist, und legen Sie ihn andernfalls als protected fest. Der Compiler synthetisiert keinen Kopierkonstruktor für record struct-Typen. Sie können einen solchen schreiben, aber der Compiler generiert keine entsprechenden Aufrufe für with-Ausdrücke. Stattdessen verwendet der Compiler Zuweisung.

Sie können die Klonmethode nicht überschreiben, und Sie können keinen Member mit dem Namen Clone in einem Datensatztyp erstellen. Der tatsächliche Name der Klonmethode wird vom Compiler generiert.

Integrierte Formatierung für die Anzeige

Datensatztypen verfügen über eine vom Compiler generierte ToString-Methode, die die Namen und Werte der öffentlichen Eigenschaften und Felder anzeigt. Die ToString-Methode gibt eine Zeichenfolge in folgendem Format zurück:

<record type name> { <property name> = <value>, <property name> = <value>, ...}

Für Verweistypen wird der Typname des Objekts, auf das die Eigenschaft verweist, anstelle des Eigenschaftenwerts angezeigt. Im folgenden Beispiel ist das Array ein Verweistyp, sodass System.String[] anstelle der tatsächlichen Arrayelementwerte angezeigt wird:

Person { FirstName = Nancy, LastName = Davolio, ChildNames = System.String[] }

Um dieses Feature zu implementieren, synthetisiert der Compiler in record class-Typen eine virtuelle PrintMembers-Methode und eine ToString-Überschreibung. In record struct-Typen ist dieser Member private. Mit der ToString-Überschreibung wird ein StringBuilder-Objekt mit dem Typnamen gefolgt von einer öffnenden eckigen Klammer erstellt. Es ruft PrintMembers auf, um Eigenschaftennamen und Werte hinzuzufügen, und fügt dann die schließende Klammer hinzu. Das folgende Beispiel zeigt Code, der mit dem Inhalt der synthetisierten Überschreibung vergleichbar ist:

public override string ToString()
{
    StringBuilder stringBuilder = new StringBuilder();
    stringBuilder.Append("Teacher"); // type name
    stringBuilder.Append(" { ");
    if (PrintMembers(stringBuilder))
    {
        stringBuilder.Append(" ");
    }
    stringBuilder.Append("}");
    return stringBuilder.ToString();
}

Sie können Ihre eigene Implementierung von PrintMembers oder der ToString-Überschreibung bereitstellen. Beispiele finden Sie weiter unten in diesem Artikel im Abschnitt PrintMembers-Formatierung in abgeleiteten Datensätzen. In C# 10 und höheren Versionen kann Ihre Implementierung von ToString den sealed-Modifizierer enthalten, der den Compiler daran hindert, eine ToString-Implementierung für abgeleitete Datensätze zu synthetisieren. Das bedeutet, dass die ToString-Ausgabe keine Laufzeittypinformationen enthält. (Alle Member und Werte werden angezeigt, da für abgeleitete Datensätze weiterhin eine PrintMembers-Methode generiert wird.)

Vererbung

Dieser Abschnitt gilt nur für record class-Typen.

Ein Datensatz kann von einem anderen Datensatz erben. Ein Datensatz kann jedoch nicht von einer Klasse erben, und eine Klasse kann nicht von einem Datensatz erben.

Positionsparameter in abgeleiteten Datensatztypen

Der abgeleitete Datensatz deklariert Positionsparameter für alle Parameter im primären Konstruktor des Basisdatensatzes. Der Basisdatensatz deklariert und initialisiert diese Eigenschaften. Der abgeleitete Datensatz blendet diese nicht aus. Stattdessen erstellt und initialisiert er nur Eigenschaften für Parameter, die nicht in seinem Basisdatensatz deklariert sind.

Im folgenden Beispiel wird die Vererbung mit Positionseigenschaftensyntax veranschaulicht:

public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);
public static void Main()
{
    Person teacher = new Teacher("Nancy", "Davolio", 3);
    Console.WriteLine(teacher);
    // output: Teacher { FirstName = Nancy, LastName = Davolio, Grade = 3 }
}

Gleichheit in Vererbungshierarchien

Dieser Abschnitt gilt für record class-Typen, aber nicht für record struct-Typen. Damit zwei Datensatzvariablen gleich sind, muss der Laufzeittyp gleich sein. Die Typen der enthaltenden Variablen können unterschiedlich sein. Der Vergleich geerbter Gleichheit wird im folgenden Codebeispiel veranschaulicht:

public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);
public static void Main()
{
    Person teacher = new Teacher("Nancy", "Davolio", 3);
    Person student = new Student("Nancy", "Davolio", 3);
    Console.WriteLine(teacher == student); // output: False

    Student student2 = new Student("Nancy", "Davolio", 3);
    Console.WriteLine(student2 == student); // output: True
}

Im Beispiel werden alle Variablen als Person deklariert, auch wenn die Instanz ein abgeleiteter Typ von Student oder Teacher ist. Die Instanzen verfügen alle über dieselben Eigenschaften und Eigenschaftswerte. student == teacher gibt jedoch False zurück, obwohl beide Person-Typvariablen sind, und student == student2 gibt True zurück, obwohl die eine eine Person-Variable und die andere eine Student-Variable ist. Der Gleichheitstest hängt vom Laufzeittyp des tatsächlichen Objekts ab, nicht vom deklarierten Typ der Variablen.

Um dieses Verhalten zu implementieren, synthetisiert der Compiler eine EqualityContract-Eigenschaft, die ein Type-Objekt zurückgibt, das mit dem Typ des Datensatzes übereinstimmt. Das EqualityContract-Element ermöglicht den Gleichheitsmethoden, den Laufzeittyp von Objekten bei der Überprüfung auf Gleichheit zu vergleichen. Wenn der Basistyp eines Datensatzes object ist, ist diese Eigenschaft virtual. Wenn der Basistyp einem anderen Datensatztyp entspricht, ist diese Eigenschaft eine Überschreibung. Wenn der Datensatztyp sealed ist, lautet diese Eigenschaft sealed.

Beim Vergleichen von zwei Instanzen eines abgeleiteten Typs überprüfen die synthetisierten Gleichheitsmethoden alle Eigenschaften der Basis- und abgeleiteten Typen auf Gleichheit. Die synthetisierte GetHashCode-Methode verwendet die GetHashCode-Methode aus allen im Basistyp und im abgeleiteten Datensatztyp deklarierten Eigenschaften und Feldern.

with-Ausdrücke in abgeleiteten Datensätzen

Das Ergebnis eines with-Ausdrucks weist denselben Laufzeittyp auf wie der Operand des Ausdrucks. Alle Eigenschaften des Laufzeittyps werden kopiert, Sie können jedoch nur Eigenschaften des Kompilierzeittyps festlegen, wie im folgenden Beispiel gezeigt:

public record Point(int X, int Y)
{
    public int Zbase { get; set; }
};
public record NamedPoint(string Name, int X, int Y) : Point(X, Y)
{
    public int Zderived { get; set; }
};

public static void Main()
{
    Point p1 = new NamedPoint("A", 1, 2) { Zbase = 3, Zderived = 4 };

    Point p2 = p1 with { X = 5, Y = 6, Zbase = 7 }; // Can't set Name or Zderived
    Console.WriteLine(p2 is NamedPoint);  // output: True
    Console.WriteLine(p2);
    // output: NamedPoint { X = 5, Y = 6, Zbase = 7, Name = A, Zderived = 4 }

    Point p3 = (NamedPoint)p1 with { Name = "B", X = 5, Y = 6, Zbase = 7, Zderived = 8 };
    Console.WriteLine(p3);
    // output: NamedPoint { X = 5, Y = 6, Zbase = 7, Name = B, Zderived = 8 }
}

PrintMembers-Formatierung in abgeleiteten Datensätzen

Die synthetisierte PrintMembers-Methode eines abgeleiteten Datensatztyps ruft die Basisimplementierung auf. Das Ergebnis ist, dass alle öffentlichen Eigenschaften und Felder sowohl von abgeleiteten als auch Basistypen in der ToString-Ausgabe enthalten sind, wie im folgenden Beispiel gezeigt:

public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);

public static void Main()
{
    Person teacher = new Teacher("Nancy", "Davolio", 3);
    Console.WriteLine(teacher);
    // output: Teacher { FirstName = Nancy, LastName = Davolio, Grade = 3 }
}

Sie können Ihre eigene Implementierung der PrintMembers-Methode bereitstellen. Wenn Sie dies tun, verwenden Sie die folgende Signatur:

  • Für einen sealed-Datensatz, der von object abgeleitet wird (deklariert keinen Basisdatensatz): private bool PrintMembers(StringBuilder builder);
  • Für einen sealed-Datensatz, der von einem anderen Datensatz abgeleitet wird: protected sealed override bool PrintMembers(StringBuilder builder);
  • Für einen Datensatz, der nicht sealed ist und von einem Objekt abgeleitet wird: protected virtual bool PrintMembers(StringBuilder builder);
  • Für einen Datensatz, der nicht sealed ist und von einem anderen Datensatz abgeleitet wird: protected override bool PrintMembers(StringBuilder builder);

Im Folgenden finden Sie ein Beispiel für Code, der die synthetisierten PrintMembers-Methoden ersetzt, ein Beispiel für einen Datensatztyp, der vom Objekt abgeleitet wird, und ein Beispiel für einen Datensatztyp, der von einem anderen Datensatz abgeleitet wird:

public abstract record Person(string FirstName, string LastName, string[] PhoneNumbers)
{
    protected virtual bool PrintMembers(StringBuilder stringBuilder)
    {
        stringBuilder.Append($"FirstName = {FirstName}, LastName = {LastName}, ");
        stringBuilder.Append($"PhoneNumber1 = {PhoneNumbers[0]}, PhoneNumber2 = {PhoneNumbers[1]}");
        return true;
    }
}

public record Teacher(string FirstName, string LastName, string[] PhoneNumbers, int Grade)
    : Person(FirstName, LastName, PhoneNumbers)
{
    protected override bool PrintMembers(StringBuilder stringBuilder)
    {
        if (base.PrintMembers(stringBuilder))
        {
            stringBuilder.Append(", ");
        };
        stringBuilder.Append($"Grade = {Grade}");
        return true;
    }
};

public static void Main()
{
    Person teacher = new Teacher("Nancy", "Davolio", new string[2] { "555-1234", "555-6789" }, 3);
    Console.WriteLine(teacher);
    // output: Teacher { FirstName = Nancy, LastName = Davolio, PhoneNumber1 = 555-1234, PhoneNumber2 = 555-6789, Grade = 3 }
}

Hinweis

In C# 10.0 und höheren Versionen synthetisiert der Compiler PrintMembers, wenn ein Basisdatensatz die ToString-Methode versiegelt hat. Sie können auch eine eigene Implementierung von PrintMembers erstellen.

Dekonstruktorverhalten in abgeleiteten Datensätzen

Die Deconstruct-Methode eines abgeleiteten Datensatzes gibt die Werte aller Positionseigenschaften des Kompilierzeittyps zurück. Wenn der Variablentyp ein Basisdatensatz ist, werden nur die Basisdatensatzeigenschaften dekonstruiert, es sei denn, das Objekt wird in den abgeleiteten Typ umgewandelt. Das folgende Beispiel zeigt, wie ein Dekonstruktor für einen abgeleiteten Datensatz aufgerufen wird.

public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);

public static void Main()
{
    Person teacher = new Teacher("Nancy", "Davolio", 3);
    var (firstName, lastName) = teacher; // Doesn't deconstruct Grade
    Console.WriteLine($"{firstName}, {lastName}");// output: Nancy, Davolio

    var (fName, lName, grade) = (Teacher)teacher;
    Console.WriteLine($"{fName}, {lName}, {grade}");// output: Nancy, Davolio, 3
}

Generische Einschränkungen

Es gibt keine generische Einschränkung, die erfordert, dass ein Typ ein Datensatz ist. Datensätze erfüllen entweder die class- oder die struct-Einschränkung. Um eine Einschränkung für eine bestimmte Hierarchie von Datensatztypen festzulegen, wenden Sie die Einschränkung wie eine Basisklasse auf den Basisdatensatz an. Weitere Informationen finden Sie unter Einschränkungen für Typparameter (C#-Programmierhandbuch).

C#-Sprachspezifikation

Weitere Informationen finden Sie im Abschnitt Klassen der C#-Sprachspezifikation.

Weitere Informationen zu in C# 9 und höher eingeführten Features finden Sie in den folgenden Featurevorschlägen:

Siehe auch