Datensätze (C#-Referenz)

Sie verwenden den record-Modifizierer, um einen Verweistyp zu definieren, der integrierte Funktionalität zum Kapseln von Daten bereitstellt. C# 10 ermöglicht die record class-Syntax als Synonym, um einen Verweistyp zu verdeutlichen und record struct, um einen Werttyp mit ähnlicher Funktionalität zu definieren.

Wenn Sie einen primären Konstruktor für einen Datensatz deklarieren, generiert der Compiler öffentliche Eigenschaften für die Parameter des primären Konstruktors. Die Parameter des primären Konstruktors für einen Datensatz werden als Positionsparameter bezeichnet. Der Compiler erstellt Positionseigenschaften, die den primären Konstruktor oder Positionsparameter spiegeln. Der Compiler synthetisiert keine Eigenschaften für Parameter primärer Konstruktoren für Typen, die nicht über den record-Modifizierer verfügen.

Die folgenden beiden Beispiele veranschaulichen record- (oder record class-) Verweistypen:

public record Person(string FirstName, string LastName);
public record Person
{
    public required string FirstName { get; init; }
    public required string LastName { get; init; }
};

Die folgenden beiden Beispiele veranschaulichen record struct-Werttypen:

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; }
}

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

public record Person
{
    public required string FirstName { get; set; }
    public required string LastName { get; set; }
};

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; }
}

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:

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. Der record struct-Typ wurde in C# 10 eingeführt.

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 Eigenschaft für jeden Positionsparameter, der in der Datensatzdeklaration bereitgestellt wird.
    • Für - und -Typen handelt es sich um eine recordinit-onlyreadonly record struct-Eigenschaft.
    • Für record struct-Typen ist dies eine Lese-/Schreibeigenschaft.
  • 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. 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 die gewünschte Eigenschaft ist, können Sie eine eigene Eigenschaft mit demselben Namen definieren. Beispielsweise können Sie die Barrierefreiheit oder Änderbarkeit bearbeiten oder eine Implementierung für den get- oder set-Accessor bereitstellen. Wenn Sie die Eigenschaft in Ihrer Quelle deklarieren, müssen Sie sie über den Positionsparameter des Datensatzes initialisieren. Wenn ihre Eigenschaft eine automatisch implementierte Eigenschaft ist, müssen Sie die Eigenschaft initialisieren. Wenn Sie Ihrer Quelle ein Unterstützungsfeld hinzufügen, müssen Sie das Unterstützungsfeld initialisieren. Der generierte Dekonstruktor verwendet 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.

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; } = [];
};

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

Wenn Sie Gleichheitsmethoden nicht überschreiben oder ersetzen, 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 struct-Typen sind zwei Objekte gleich, wenn sie denselben Typ aufweisen und die gleichen Werte speichern.
  • Für Typen mit dem record-Modifizierer (record class, record struct und readonly record struct) sind zwei Objekte gleich, wenn sie denselben Typ aufweisen und 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 Datensätzen wird die Implementierung vom Compiler synthetisiert, und es werden deklarierte Datenmember verwendet.

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 mehrere Methoden, inklusive:

  • Eine Überschreibung von Object.Equals(Object) Es ist ein Fehler, wenn die Außerkraftsetzung explizit deklariert wird.

    Diese Methode wird als Grundlage für die statische Object.Equals(Object, Object)-Methode verwendet, wenn beide Parameter nicht NULL sind.

  • Ein virtual oder sealed, Equals(R? other), wobei R der Datensatztyp ist. Diese Methode implementiert IEquatable<T>. Diese Methode kann explizit deklariert werden.

  • Wenn der Datensatztyp von einem Basisdatensatztyp Base abgeleitet wird, Equals(Base? other). Es ist ein Fehler, wenn die Außerkraftsetzung explizit deklariert wird. Wenn Sie Ihre eigene Implementierung von Equals(R? other) bereitstellen, stellen Sie auch eine Implementierung von GetHashCode bereit.

  • Eine Überschreibung von Object.GetHashCode() Diese Methode kann explizit deklariert werden.

  • Überschreibungen der Operatoren == und !=. Es ist ein Fehler, wenn die Operatoren explizit deklariert werden.

  • Wenn der Datensatztyp von einem Basisdatensatztyp abgeleitet wird, protected override Type EqualityContract { get; };. Diese Eigenschaft kann explizit deklariert werden. Weitere Informationen finden Sie unter Gleichheit in Vererbungshierarchien.

Wenn ein Datensatztyp über eine Methode verfügt, die mit der Signatur einer synthetisierten Methode übereinstimmt, die explizit deklariert werden kann, wird diese Methode vom Compiler nicht synthetisiert.

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. Explizit deklarierte Eigenschaften müssen eine init- oder set-Zugriffsmethode aufweisen, damit sie in einem with-Ausdruck geändert werden können.

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 so vorgehen, 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. Die Werte von record struct werden bei Zuweisung kopiert.

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:

<Datensatztypname> { <Eigenschaftsname> = <Wert>, <Eigenschaftsname> = <Wert>, ...}

Die für <value> ausgegebene Zeichenfolge ist die Zeichenfolge, die von ToString() für den Typ der Eigenschaft zurückgegeben wird. Im folgenden Beispiel ist ChildNames ein System.Array, wobei ToStringSystem.String[] zurückgibt:

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. Sie können eine konsistente Zeichenfolgendarstellung in einer Hierarchie von record-Typen erstellen. (Abgeleitete Datensätze umfassen weiterhin eine PrintMembers-Methode, die für alle abgeleiteten Eigenschaften 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 lautet, ist diese Eigenschaft effektiv versiegelt (sealed), da der Typ sealed ist.

Wenn Code zwei Instanzen eines abgeleiteten Typs vergleicht, überprüfen die synthetisierten Gleichheitsmethoden alle Datenmember der Basis- und abgeleiteten Typen auf Gleichheit. Die synthetisierte GetHashCode-Methode verwendet die GetHashCode-Methode aus allen im Basistyp und im abgeleiteten deklarierten Datenmembern. Die Datenmember eines record-Elements umfassen alle deklarierten Felder und das vom Compiler synthetisierte Sicherungsfeld für alle automatisch implementierten Eigenschaften.

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 ist (beachten Sie, dass der einschließende Typ sealed ist, so dass die Methode effektiv sealed lautet): protected 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 und höheren Versionen synthetisiert der Compiler PrintMembers in abgeleiteten Datensätzen selbst dann, 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

Das Schlüsselwort record ist ein Modifizierer für einen class- oder struct-Typ. Das Hinzufügen des record-Modifizierers schließt das weiter oben in diesem Artikel beschriebene Verhalten ein. Es gibt keine generische Einschränkung, die erfordert, dass ein Typ ein Datensatz ist. Eine record class erfüllt die class-Einschränkung. Eine record struct erfüllt die struct-Einschränkung. 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 diesen Features finden Sie in den folgenden Featurevorschlägen:

Weitere Informationen