Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Der record Modifizierer bietet integrierte Funktionen zum Kapseln von Daten. Die und record class die record Syntax definieren Referenztypen. Die record struct Syntax definiert einen Werttyp.
Die C#-Sprachreferenz dokumentiert die zuletzt veröffentlichte Version der C#-Sprache. Außerdem enthält sie erste Dokumentation für Features in der öffentlichen Vorschau für die kommende Sprachversion.
In der Dokumentation werden alle Features identifiziert, die in den letzten drei Versionen der Sprache oder in der aktuellen öffentlichen Vorschau eingeführt wurden.
Tipp
Informationen dazu, wann ein Feature erstmals in C# eingeführt wurde, finden Sie im Artikel zum Versionsverlauf der C#-Sprache.
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 primären Konstruktorparameter für einen Datensatz sind Positionsparameter. 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:
- Präzise Syntax zum Erstellen eines Verweistyps mit unveränderlichen Eigenschaften
- Integriertes Verhalten, das für einen datenzentrierten Verweistyp nützlich ist:
- Unterstützung für Vererbungshierarchien
Die Beispiele oben zeigen einige Unterschiede zwischen Datensätzen, die Verweistypen sind, und Datensätzen, die Werttypen sind:
- Das
record- oderrecord class-Element deklariert einen Verweistyp. Dasclass-Schlüsselwort ist optional, kann aber für Leser mehr Klarheit schaffen. Dasrecord struct-Element deklariert einen Werttyp. - Positionseigenschaften sind in - und
record class-Elementenreadonly record struct. Sie sind in -Elementenrecord struct.
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. Entscheiden Sie zwischen einer und einer record class ähnlichen Entscheidung zwischen einem record struct und einem class.struct Der Ausdrucksdatensatz beschreibt das Verhalten, 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 Eigenschafts- und Felddefinition
Verwenden Sie Positionsparameter, um Eigenschaften eines Datensatzes zu deklarieren oder Eigenschafts- oder Feldwerte zu initialisieren. Im folgenden Beispiel wird ein Datensatz mit zwei Positionseigenschaften erstellt:
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 öffentlich implementierte Eigenschaft für jeden Positionsparameter, der in der Datensatzdeklaration bereitgestellt wird.
- 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 einemout-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 können diesen Elementen, 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 Eigenschaftsdefinition nicht ihren Vorstellungen entspricht, definieren Sie ihre eigene Eigenschaft oder ihr Feld mit demselben Namen. Sie könnten zum Beispiel die Zugriffsfreiheit oder die Veränderbarkeit ändern oder eine Implementierung für den get- oder set-Accessor bereitstellen wollen. Wenn Sie das Mitglied in Ihrem Quellcode deklarieren, müssen Sie es aus dem Positionsparameter der Aufzeichnung 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 Eigenschafts- oder Felddefinition. 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
}
Wenn Sie ein Feld anstelle einer Eigenschaft erstellen möchten, weisen Sie den Positionsparameter einem Feld zu, wie im folgenden Beispiel gezeigt:
public record Person(string FirstName, string LastName, string Id)
{
internal readonly string Id = Id; // this.Id set to parameter 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; } = [];
};
Eigenschaften, die der Compiler aus Positionsparametern generiert, sind public. Sie deklarieren die Zugriffsmodifizierer für alle Eigenschaften, die Sie explizit deklarieren.
Unveränderlichkeit
Eine Positionsdatensatzklasse 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.
Unveränderlichkeit kann nützlich sein, wenn Sie einen datenorientierten Typ als threadsicher benötigen oder von einem Hashcode abhängig sind, der in einer Hashtabelle gleich bleibt. Die Unveränderlichkeit ist jedoch nicht für alle Datenszenarien geeignet. Entity Framework Core unterstützt beispielsweise keine Aktualisierung mit unveränderlichen Entitätstypen.
Init-only-Eigenschaften, unabhängig davon, ob sie aus Positionsparametern (record class und readonly record struct) oder durch Angeben init von Accessoren erstellt wurden, weisen 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 structundreadonly 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. Beispielsweise hängt Entity Framework Core von der Referenzgleichheit ab, um sicherzustellen, dass nur eine Instanz eines Entitätstyps für das Konzept einer Entität verwendet wird. Aus diesem Grund sind Datensätze und Datensatzstrukturen nicht für die Verwendung als Entitätstypen in Entity Framework Core 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 Sie die Außerkraftsetzung explizit deklarieren.
Diese Methode wird als Grundlage für die statische Object.Equals(Object, Object)-Methode verwendet, wenn beide Parameter nicht NULL sind.
Ein
virtualodersealed,Equals(R? other), wobeiRder Datensatztyp ist. Diese Methode implementiert IEquatable<T>. Sie können diese Methode explizit deklarieren.Wenn der Datensatztyp von einem Basisdatensatztyp
Baseabgeleitet wird,Equals(Base? other). Es ist ein Fehler, wenn Sie die Außerkraftsetzung explizit deklarieren. Wenn Sie Ihre eigene Implementierung vonEquals(R? other)bereitstellen, stellen Sie auch eine Implementierung vonGetHashCodebereit.Eine Überschreibung von Object.GetHashCode() Sie können diese Methode explizit deklarieren.
Außerkraftsetzungen von Operator
==und Operator!=. Es ist ein Fehler, wenn Sie die Operatoren explizit deklarieren.Wenn der Datensatztyp von einem Basisdatensatztyp abgeleitet wird,
protected override Type EqualityContract { get; };. Sie können diese Eigenschaft explizit deklarieren. Weitere Informationen finden Sie unter Gleichheit in Vererbungshierarchien.
Der Compiler synthetisiert keine Methode, wenn ein Datensatztyp über eine Methode verfügt, die der Signatur einer synthetisierten Methode entspricht und die Methode explizit deklariert werden darf.
Nichtdestruktive Mutation
Wenn Sie eine Instanz mit einigen Änderungen kopieren müssen, verwenden Sie einen with Ausdruck, um eine nicht destruktive Mutation zu erzielen. Ein with Ausdruck erstellt eine neue Datensatzinstanz, die eine Kopie einer vorhandenen Datensatzinstanz ist, aber mit angegebenen Eigenschaften und Feldern geändert. Verwenden Sie die Objektinitialisierungssyntax , um die zu ändernden Werte anzugeben, 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. Bei einer Referenzeigenschaft kopiert der Ausdruck nur den Verweis auf eine Instanz. 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 der with Ausdruck enthält.
Von Bedeutung
Der Compiler synthetisiert auch einen öffentlichen parameterlosen Konstruktor, wenn der Datensatz keinen primären Konstruktor oder benutzerdefinierte Konstruktoren enthält. Dieser parameterlose Konstruktor initialisiert alle Felder mit ihren Standardwerten. Ohne diesen synthetisierten Konstruktor ist kein öffentlicher Konstruktor verfügbar.
Wenn Sie ein anderes Kopierverhalten benötigen, schreiben Sie ihren eigenen Kopierkonstruktor in einem record class. Wenn Sie dies tun, synthetisiert der Compiler keins. Erstellen Sie den Konstruktor private , wenn der Datensatz ist sealed. Andernfalls machen Sie es protected. 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.
Von Bedeutung
In den vorherigen Beispielen sind alle Eigenschaften unabhängig. Keine der Eigenschaften wird aus anderen Eigenschaftswerten berechnet. Ein with Ausdruck kopiert zuerst die vorhandene Datensatzinstanz und ändert dann alle Eigenschaften oder Felder, die der with Ausdruck enthält. Berechnete Eigenschaften in record Typen sollten beim Zugriff berechnet und nicht initialisiert werden, wenn die Instanz erstellt wird. Andernfalls könnte eine Eigenschaft den berechneten Wert basierend auf der ursprünglichen Instanz und nicht der geänderten Kopie zurückgeben.
Sie stellen die Richtigkeit für berechnete Eigenschaften sicher, indem Sie den Wert für den Zugriff berechnen, wie in der folgenden Deklaration dargestellt:
public record Point(int X, int Y)
{
public double Distance => Math.Sqrt(X * X + Y * Y);
}
Der vorhergehende Datensatztyp berechnet den Zeitpunkt des Distance Zugriffs, wie im folgenden Beispiel gezeigt:
Point p1 = new Point(3, 4);
Console.WriteLine($"Original point: {p1}");
p1 = p1 with { Y = 8 };
Console.WriteLine($"Modified point: {p1}");
// Output:
// Original point: Point { X = 3, Y = 4, Distance = 5 }
// Modified point: Point { X = 3, Y = 8, Distance = 8.54400374531753 }
Vergleichen Sie diesen Ansatz mit der folgenden Deklaration, wobei die Distance Eigenschaft als Teil der Initialisierung einer neuen Instanz berechnet und zwischengespeichert wird:
public record PointInit(int X, int Y)
{
public double Distance { get; } = Math.Sqrt(X * X + Y * Y);
}
Da Distance der Wert als Teil der Initialisierung berechnet wird, wird der Wert berechnet und zwischengespeichert, bevor der with Ausdruck den Wert der Y Kopie ändert. Das Ergebnis ist, dass der Abstand falsch ist:
PointInit pt1 = new PointInit(3, 4);
Console.WriteLine($"Original point: {pt1}");
pt1 = pt1 with { Y = 8 };
Console.WriteLine($"Incorrect Modified point: {pt1}");
// Output:
// Original point: PointInit { X = 3, Y = 4, Distance = 5 }
// Modified point: PointInit { X = 3, Y = 8, Distance = 5 }
Die Distance Berechnung ist für jeden Zugriff nicht teuer. Einige berechnete Eigenschaften erfordern jedoch möglicherweise Zugriff auf mehr Daten oder eine umfangreichere Berechnung. Verwenden Sie in diesen Fällen anstelle eines Datensatzes einen class Typ, und berechnen Sie den zwischengespeicherten Wert, wenn eine der Komponenten den Wert ändert.
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 im folgenden 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. Ihre Implementierung von ToString könnte den Modifikator sealed enthalten, der verhindert, dass der Compiler eine ToString-Implementierung für abgeleitete Datensätze synthetisiert. 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 einer record umfassen alle deklarierten Felder und das compilersynthetisierte 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 vonobjectabgeleitet 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 Typsealedist, so dass die Methode effektivsealedlautet):protected override bool PrintMembers(StringBuilder builder); - Für einen Datensatz, der nicht
sealedist und von einem Objekt abgeleitet wird:protected virtual bool PrintMembers(StringBuilder builder); - Für einen Datensatz, der nicht
sealedist 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
Der Compiler synthetisiert PrintMembers in abgeleiteten Datensätzen, auch 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 es sich bei dem Variablentyp um einen Basisdatensatz handelt, gibt der Destrukturvorgang nur die Basisdatensatzeigenschaften zurück, es sei denn, das Objekt wird in den abgeleiteten Typ umgeleitet. 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: