Freigeben über


Datensatztypen erstellen

Datensätze sind Typen, die wertbasierte Gleichheit verwenden. Sie können Datensätze als Bezugstypen oder Werttypen definieren. Zwei Variablen eines Datensatztyps sind gleich, wenn die Datensatztypdefinitionen identisch sind und bei jedem Feld die Werte in beiden Datensätzen gleich sind. Zwei Variablen eines Klassentyps sind gleich, wenn die objekte, auf die verwiesen wird, derselbe Klassentyp ist und die Variablen auf dasselbe Objekt verweisen. Wertbasierte Gleichheit impliziert andere Funktionen, die Sie wahrscheinlich in Datensatztypen verwenden möchten. Der Compiler generiert viele dieser Member, wenn Sie record anstelle von class deklarieren. Der Compiler generiert dieselben Methoden für record struct-Datentypen.

In diesem Tutorial lernen Sie Folgendes:

  • Entscheiden Sie, ob Sie einen record-Modifizierer zu einem class-Typ hinzufügen.
  • Deklarieren Sie Datensatztypen und Positionsdatensatztypen.
  • Ersetzen Sie Ihre Methoden durch compilergenerierte Methoden in Datensätzen.

Voraussetzungen

Sie müssen Ihren Computer so einrichten, dass .NET 6 oder höher ausgeführt wird. Der C#-Compiler ist mit Visual Studio 2022- oder dem .NET SDK-verfügbar.

Charakteristiken von Datensätzen

Sie definieren einen Datensatz, indem Sie einen Typ mit dem Schlüsselwort record deklarieren und eine class- oder struct-Deklaration ändern. Optional können Sie das Schlüsselwort class auslassen, um eine record class zu erstellen. Ein Datensatz befolgt eine wertebasierte Gleichheitssemantik. Um die Wertsemantik zu erzwingen, generiert der Compiler mehrere Methoden für Ihren Datensatztyp (sowohl für record class-Typen als auch für record struct-Typen):

Datensätze stellen auch eine Überschreibung von Object.ToString() zur Verfügung. Der Compiler synthetisiert Methoden zum Anzeigen von Datensätzen mit Object.ToString(). Sie erforschen diese Mitglieder, während Sie den Code für dieses Tutorial schreiben. Datensätze unterstützen with-Ausdrücke, um nicht destruktive Änderungen von Datensätzen zu ermöglichen.

Sie können auch Datensätze mit fester Breite mithilfe einer kürzeren Syntax deklarieren. Der Compiler synthetisiert weitere Methoden für Sie, wenn Sie Positionsdatensätze deklarieren:

  • Ein primärer Konstruktor, dessen Parameter den Positionsparametern in der Datensatzdeklaration entsprechen.
  • Öffentliche Eigenschaften für jeden Parameter eines primären Konstruktors. Dabei handelt es sich um init-only-Eigenschaften für record class- und readonly record struct-Typen. Für record struct-Typen sind es read-write-Eigenschaften.
  • Eine Deconstruct-Methode zum Extrahieren von Eigenschaften aus dem Datensatz

Erstellen von Temperaturdaten

Daten und Statistiken gehören zu den Szenarien, in denen Sie Datensätze verwenden möchten. Für dieses Tutorial erstellen Sie eine Anwendung, die Wärmesummen für verschiedene Verwendungszwecke berechnet. Gradtage sind ein Maß für Wärme (oder Hitzemangel) über einen Zeitraum von Tagen, Wochen oder Monaten. Wärmesummen verfolgen den Energieverbrauch und prognostizieren diesen. Mehr heißere Tage bedeuten mehr Klimaanlage und kältere Tage bedeuten mehr Ofennutzung. Wärmesummen helfen bei der Verwaltung von Pflanzenbeständen und korrelieren mit dem Pflanzenwachstum im Wechsel der Jahreszeiten. Wärmesummen werden außerdem zur Nachverfolgung von Tierwanderungen für Spezies verwendet, die sich dem Klima entsprechend bewegen.

Die Formel basiert auf der Mittleren Temperatur an einem bestimmten Tag und einer Basistemperatur. Um die Gradtage im Laufe der Zeit zu berechnen, benötigen Sie die Höchst- und Tiefsttemperatur jedes Tages für einen bestimmten Zeitraum. Beginnen wir mit dem Erstellen einer neuen Anwendung. Erstellen Sie eine neue Konsolenanwendung. Erstellen Sie einen neuen Datensatztyp in einer neuen Datei namens "DailyTemperature.cs":

public readonly record struct DailyTemperature(double HighTemp, double LowTemp);

Mit dem obigen Code wird ein Datensatz mit fester Breite definiert. Der DailyTemperature-Datensatz gehört zum Typ readonly record struct, da er nicht vererben und unveränderlich sein sollte. Bei den Eigenschaften HighTemp und LowTemp handelt es sich um init-only-Eigenschaften, d. h., sie können im Konstruktor oder mithilfe eines Eigenschafteninitialisierers festgelegt werden. Wenn Lese-/Schreibzugriff auf positionale Parameter bestehen soll, müssen Sie record struct anstelle von readonly record struct deklarieren. Der Typ DailyTemperature verfügt ebenfalls über einen primären Konstruktor, der über zwei Parameter verfügt, die den zwei Eigenschaften entsprechen. Sie verwenden den primären Konstruktor zum Initialisieren eines DailyTemperature-Datensatzes. Der folgende Code erstellt und initialisiert mehrere DailyTemperature-Datensätze. Der erste Datensatz verwendet benannte Parameter, um HighTemp und LowTemp zu definieren. Die verbleibenden Initialisierer verwenden Positionsparameter, um HighTemp und LowTemp zu initialisieren:

private static DailyTemperature[] data = [
    new DailyTemperature(HighTemp: 57, LowTemp: 30), 
    new DailyTemperature(60, 35),
    new DailyTemperature(63, 33),
    new DailyTemperature(68, 29),
    new DailyTemperature(72, 47),
    new DailyTemperature(75, 55),
    new DailyTemperature(77, 55),
    new DailyTemperature(72, 58),
    new DailyTemperature(70, 47),
    new DailyTemperature(77, 59),
    new DailyTemperature(85, 65),
    new DailyTemperature(87, 65),
    new DailyTemperature(85, 72),
    new DailyTemperature(83, 68),
    new DailyTemperature(77, 65),
    new DailyTemperature(72, 58),
    new DailyTemperature(77, 55),
    new DailyTemperature(76, 53),
    new DailyTemperature(80, 60),
    new DailyTemperature(85, 66) 
];

Sie können Datensätzen eigene Eigenschaften oder Methoden hinzufügen, einschließlich Positionsdatensätze. Sie müssen die Mittlere Temperatur für jeden Tag berechnen. Diese Eigenschaft können Sie zum Datensatz DailyTemperature hinzufügen:

public readonly record struct DailyTemperature(double HighTemp, double LowTemp)
{
    public double Mean => (HighTemp + LowTemp) / 2.0;
}

Stellen Sie sicher, dass Sie diese Daten verwenden können. Fügen Sie der Main-Methode den folgenden Code hinzu:

foreach (var item in data)
    Console.WriteLine(item);

Führen Sie Ihre Anwendung aus, und Sie sehen die Ausgabe, die ähnlich wie die folgende Anzeige aussieht (mehrere Zeilen wurden für Leerzeichen entfernt):

DailyTemperature { HighTemp = 57, LowTemp = 30, Mean = 43.5 }
DailyTemperature { HighTemp = 60, LowTemp = 35, Mean = 47.5 }


DailyTemperature { HighTemp = 80, LowTemp = 60, Mean = 70 }
DailyTemperature { HighTemp = 85, LowTemp = 66, Mean = 75.5 }

Der obige Code zeigt die Ausgabe der Überschreibung von ToString an, die vom Compiler synthetisiert wurde. Wenn Sie einen anderen Text bevorzugen, können Sie ihre eigene Version von ToString schreiben, die verhindert, dass der Compiler eine Version für Sie synthesiert.

Berechnen von Wärmesummen

Um Gradtage zu berechnen, nehmen Sie die Differenz von einer Basistemperatur und der Mitteltemperatur an einem bestimmten Tag. Um die Wärme im Laufe der Zeit zu messen, verwerfen Sie alle Tage, an denen sich die mittlere Temperatur unter der Basislinie befindet. Um die Kälte im Laufe der Zeit zu messen, verwerfen Sie alle Tage, an denen die mittlere Temperatur über dem Bezugswert liegt. Beispielsweise verwendet die USA 65 F als Basis für Heiz- und Kühlgradtage. Das ist die Temperatur, bei der keine Heizung oder Kühlung benötigt wird. Wenn ein Tag eine mittlere Temperatur von 70 °F hat, beträgt er fünf Kühlgradtage und null Heizgradtage. Wenn die Mittlere Temperatur hingegen 55 F beträgt, beträgt dieser Tag 10 Heizgradtage und 0 Kühlgradtage.

Sie können diese Formeln als kleine Hierarchie von Datensatztypen ausdrücken: einen abstrakten Gradtagtyp und zwei konkrete Typen für Heizgradtage und Kühlgradtage. Diese Typen können auch Positionsdatensätze sein. Sie verwenden eine Baselinetemperatur und eine Reihe täglicher Temperaturdatensätze als Argumente für den primären Konstruktor:

public abstract record DegreeDays(double BaseTemperature, IEnumerable<DailyTemperature> TempRecords);

public sealed record HeatingDegreeDays(double BaseTemperature, IEnumerable<DailyTemperature> TempRecords)
    : DegreeDays(BaseTemperature, TempRecords)
{
    public double DegreeDays => TempRecords.Where(s => s.Mean < BaseTemperature).Sum(s => BaseTemperature - s.Mean);
}

public sealed record CoolingDegreeDays(double BaseTemperature, IEnumerable<DailyTemperature> TempRecords)
    : DegreeDays(BaseTemperature, TempRecords)
{
    public double DegreeDays => TempRecords.Where(s => s.Mean > BaseTemperature).Sum(s => s.Mean - BaseTemperature);
}

Der abstrakte DegreeDays-Datensatz ist die gemeinsame Basisklasse für die Datensätze HeatingDegreeDays und CoolingDegreeDays. Die primären Konstruktordeklarationen der abgeleiteten Datensätze zeigen, wie die Initialisierung des Basisdatensatzes verwaltet wird. Ihr abgeleiteter Datensatz deklariert Parameter für alle Parameter im primären Konstruktor des Basisdatensatzes. Der Basisdatensatz deklariert und initialisiert diese Eigenschaften. Der abgeleitete Datensatz blendet sie nicht aus, erstellt und initialisiert nur Eigenschaften für Parameter, die nicht im Basisdatensatz deklariert sind. In diesem Beispiel fügen die abgeleiteten Datensätze keine neuen primären Konstruktorparameter hinzu. Testen Sie Ihren Code, indem Sie der Main-Methode den folgenden Code hinzufügen:

var heatingDegreeDays = new HeatingDegreeDays(65, data);
Console.WriteLine(heatingDegreeDays);

var coolingDegreeDays = new CoolingDegreeDays(65, data);
Console.WriteLine(coolingDegreeDays);

Sie erhalten eine Ausgabe wie die folgende Anzeige:

HeatingDegreeDays { BaseTemperature = 65, TempRecords = record_types.DailyTemperature[], DegreeDays = 85 }
CoolingDegreeDays { BaseTemperature = 65, TempRecords = record_types.DailyTemperature[], DegreeDays = 71.5 }

Compiler-synthetisierte Methoden definieren

Ihr Code berechnet die richtige Anzahl von Heiz- und Kühlgradtagen über diesen Zeitraum. In diesem Beispiel wird jedoch gezeigt, warum Sie einige der synthetisierten Methoden für Datensätze ersetzen möchten. Sie können Ihre eigene Version einer der compilersynthetisierten Methoden in einem Datensatztyp deklarieren, mit Ausnahme der Klonmethode. Die Klonmethode hat einen vom Compiler generierten Namen, und Sie können keine andere Implementierung bereitstellen. Die synthetisierten Methoden umfassen einen Kopierkonstruktor, die Member der System.IEquatable<T>-Schnittstelle, Gleichheits- und Ungleichheitstests sowie GetHashCode(). Zu diesem Zweck synthetisieren Sie PrintMembers. Sie könnten auch Ihre eigene Version von ToString deklarieren, jedoch stellt PrintMembers eine bessere Option für Vererbungsszenarios dar. Um Eine eigene Version einer synthetisierten Methode bereitzustellen, muss die Signatur mit der synthetisierten Methode übereinstimmen.

Das TempRecords-Element in der Konsolenausgabe ist nicht nützlich. Es zeigt den Typ an, erfüllt aber sonst keinen Zweck. Sie können dieses Verhalten ändern, indem Sie ihre eigene Implementierung der synthetisierten PrintMembers-Methode bereitstellen. Die Signatur hängt von Modifizierern ab, die auf die record-Deklaration angewendet werden:

  • Wenn ein Datensatztyp sealed oder record struct ist, lautet die Signatur private bool PrintMembers(StringBuilder builder);.
  • Wenn ein Datensatztyp nicht sealed ist und von object abgeleitet wird (d. h., er deklariert keinen Basisdatensatz), lautet die Signatur protected virtual bool PrintMembers(StringBuilder builder);.
  • Wenn ein Datensatztyp nicht sealed ist und von einem anderen Datensatz abgeleitet ist, ist die Signatur protected override bool PrintMembers(StringBuilder builder);

Diese Regeln sind am einfachsten zu verstehen, wenn Sie den Zweck von PrintMembers verstehen. PrintMembers fügt Informationen zu jeder Eigenschaft in einem Datensatztyp zu einer Zeichenfolge hinzu. Der Vertrag erfordert, dass Basisdatensätze ihre Member zur Anzeige hinzufügen, und geht davon aus, dass abgeleitete Member ihre Member hinzufügen. Jeder Datensatztyp synthetisiert eine ToString-Überschreibung, die dem folgenden Beispiel für HeatingDegreeDays ähnelt:

public override string ToString()
{
    StringBuilder stringBuilder = new StringBuilder();
    stringBuilder.Append("HeatingDegreeDays");
    stringBuilder.Append(" { ");
    if (PrintMembers(stringBuilder))
    {
        stringBuilder.Append(" ");
    }
    stringBuilder.Append("}");
    return stringBuilder.ToString();
}

Sie deklarieren eine PrintMembers-Methode im Datensatz DegreeDays, der den Typ der Sammlung nicht ausgibt:

protected virtual bool PrintMembers(StringBuilder stringBuilder)
{
    stringBuilder.Append($"BaseTemperature = {BaseTemperature}");
    return true;
}

Die Signatur deklariert eine virtual protected Methode, die mit der Version des Compilers übereinstimmt. Sie müssen sich keine Sorgen darüber machen, dass Sie die falschen Zugriffsmethoden verwenden, da die Sprache die richtige Signatur erzwingt. Wenn Sie die richtigen Modifizierer für eine der synthetisierten Methoden vergessen, gibt der Compiler Warnungen oder Fehler aus, die Ihnen helfen, die richtige Signatur festzulegen.

Sie können die ToString-Methode als sealed in einem Datensatztyp deklarieren. Dadurch wird verhindert, dass abgeleitete Datensätze eine neue Implementierung bereitstellen. Abgeleitete Datensätze enthalten weiterhin die Überschreibung PrintMembers. Die ToString-Methode sollte versiegelt werden, wenn der Laufzeittyp des Datensatzes nicht angezeigt werden soll. Im vorherigen Beispiel würde dabei die Information verloren gehen, wo der Datensatz Wärme- oder Kältesummen gemessen hat.

Nicht destruktive Mutation

Die synthetisierten Member in einer positionellen Datensatzklasse ändern nicht den Status des Datensatzes. Das Ziel ist, dass Sie unveränderliche Datensätze einfacher erstellen können. Denken Sie daran, dass Sie eine readonly record struct deklarieren, um eine unveränderliche Datensatzstruktur zu erstellen. Sehen Sie sich noch einmal die vorherigen Deklarationen für HeatingDegreeDays und CoolingDegreeDays an. Die hinzugefügten Member führen Berechnungen der Werte für den Datensatz durch, ändern aber nicht den Zustand. Positionsdatensätze vereinfachen das Erstellen unveränderlicher Verweistypen.

Das Erstellen unveränderlicher Referenztypen bedeutet, dass Sie nicht destruktive Mutation verwenden möchten. Sie erstellen neue Datensatzinstanzen, die bestehenden Datensatzinstanzen ähnlich sind, indem Sie with-Ausdrücke verwenden. Diese Ausdrücke sind eine Kopierkonstruktion mit zusätzlichen Zuweisungen, die die Kopie ändern. Das Ergebnis ist eine neue Datensatzinstanz, in der jede Eigenschaft aus dem vorhandenen Datensatz kopiert und optional geändert wurde. Der ursprüngliche Datensatz bleibt unverändert.

Als Nächstes fügen Sie zur Veranschaulichung von with-Ausdrücken einige Features zu Ihrem Programm hinzu. Zuerst erstellen Sie einen neuen Datensatz zum Berechnen steigender Wärmesummen mithilfe derselben Daten. Für steigende Wärmesummen werden in der Regel 41 F als Baseline-Temperatur verwendet und sie messen Temperaturen über der Baseline. Um dieselben Daten zu verwenden, können Sie einen neuen Datensatz erstellen, der dem coolingDegreeDaysähnelt, aber mit einer anderen Grundtemperatur:

// Growing degree days measure warming to determine plant growing rates
var growingDegreeDays = coolingDegreeDays with { BaseTemperature = 41 };
Console.WriteLine(growingDegreeDays);

Sie können die Anzahl der berechneten Grad mit den Zahlen vergleichen, die mit einer höheren Basistemperatur generiert wurden. Denken Sie daran, dass Datensätze Verweistypen sind und dass es sich bei diesen Kopien um flache Kopien handelt. Das Array für die Daten wird nicht kopiert, aber beide Datensätze beziehen sich auf dieselben Daten. Diese Tatsache ist ein Vorteil in einem anderen Szenario. Bei steigenden Wärmesummen ist es nützlich, die Gesamtsumme der letzten fünf Tage zu überwachen. Sie können neue Datensätze mit unterschiedlichen Quelldaten erstellen, indem Sie with Ausdrücke verwenden. Der folgende Code erstellt eine Auflistung dieser Akkumulationen und zeigt dann die Werte an:

// showing moving accumulation of 5 days using range syntax
List<CoolingDegreeDays> movingAccumulation = new();
int rangeSize = (data.Length > 5) ? 5 : data.Length;
for (int start = 0; start < data.Length - rangeSize; start++)
{
    var fiveDayTotal = growingDegreeDays with { TempRecords = data[start..(start + rangeSize)] };
    movingAccumulation.Add(fiveDayTotal);
}
Console.WriteLine();
Console.WriteLine("Total degree days in the last five days");
foreach(var item in movingAccumulation)
{
    Console.WriteLine(item);
}

Sie können auch with Ausdrücke verwenden, um Kopien von Datensätzen zu erstellen. Geben Sie keine Eigenschaften zwischen den geschweiften Klammern für den with-Ausdruck an. Dies bedeutet, dass eine Kopie erstellt wird und keine Eigenschaften geändert werden:

var growingDegreeDaysCopy = growingDegreeDays with { };

Starten Sie die fertige Anwendung, um die Ergebnisse zu sehen.

Zusammenfassung

In diesem Tutorial wurden verschiedene Aspekte von Datensätzen vorgestellt. Records bieten eine prägnante Syntax für Typen, deren primäre Aufgabe die Datenspeicherung ist. Für objektorientierte Klassen definiert die grundlegende Verwendung Verantwortlichkeiten. Im Mittelpunkt dieses Tutorials standen positionale Datensätze, in denen Sie die Eigenschaften eines Datensatzes mit einer bündigen Syntax deklarieren können. Der Compiler synthetisiert mehrere Elemente des Datensatzes zum Kopieren und Vergleichen von Datensätzen. Sie können beliebige andere Member hinzufügen, die Sie für Ihre Datensatztypen benötigen. Sie können unveränderliche Datensatztypen erstellen und sich gewiss sein, das kein vom Compiler generierter Member den Zustand ändern würde. Und with-Ausdrücke vereinfachen zudem die Unterstützung nicht destruktiver Änderungen.

Datensätze fügen eine weitere Möglichkeit zum Definieren von Typen hinzu. Sie verwenden class Definitionen, um objektorientierte Hierarchien zu erstellen, die sich auf die Verantwortlichkeiten und das Verhalten von Objekten konzentrieren. Sie erstellen struct Typen für Datenstrukturen, die Daten speichern und klein genug sind, um effizient zu kopieren. Sie erstellen record Typen, wenn Sie wertbasierte Gleichheit und Vergleich benötigen, keine Werte kopieren möchten und Referenzvariablen verwenden möchten. Sie erstellen record struct-Typen, wenn Sie die Merkmale von Datensätzen für einen Typ verwenden möchten, der klein genug für ein effizientes Kopieren ist.

In der C#-Sprachreferenz zu Datensätzen, in der vorgeschlagenen Spezifikation des Datensatztyps sowie in der Spezifikation zur Datensatzstruktur erfahren Sie mehr über Datensätze.