Tworzenie typów rekordów

Rekordy to typy, które używają równości opartej na wartościach. Język C# 10 dodaje struktury rekordów, dzięki czemu można zdefiniować rekordy jako typy wartości. Dwie zmienne typu rekordu są równe, jeśli definicje typu rekordu są identyczne, a jeśli dla każdego pola wartości w obu rekordach są równe. Dwie zmienne typu klasy są równe, jeśli obiekty, do których odwołuje się, są tego samego typu klasy, a zmienne odwołują się do tego samego obiektu. Równość oparta na wartości oznacza inne możliwości, które prawdopodobnie będą potrzebne w typach rekordów. Kompilator generuje wiele z tych elementów członkowskich podczas deklarowania elementu recordclasszamiast . Kompilator generuje te same metody dla record struct typów.

Z tego samouczka dowiesz się, jak wykonywać następujące czynności:

  • Zdecyduj, czy dodasz record modyfikator do class typu.
  • Deklarowanie typów rekordów i typów rekordów pozycyjnych.
  • Zastąp metody metod wygenerowanych przez kompilator w rekordach.

Wymagania wstępne

Musisz skonfigurować maszynę do uruchamiania platformy .NET 6 lub nowszej, w tym kompilatora C# 10 lub nowszego. Kompilator języka C# 10 jest dostępny od programu Visual Studio 2022 lub zestawu .NET 6 SDK.

Charakterystyka rekordów

Rekord można zdefiniować, deklarując typ za pomocą słowa kluczowegorecord, modyfikując deklarację class lubstruct. Opcjonalnie możesz pominąć słowo kluczowe , class aby utworzyć element record class. Rekord jest zgodny z semantykami równości opartymi na wartościach. Aby wymusić semantyka wartości, kompilator generuje kilka metod dla typu rekordu (zarówno dla record class typów, jak i record struct typów):

Rekordy zapewniają również przesłonięcia elementu Object.ToString(). Kompilator syntetyzuje metody wyświetlania rekordów przy użyciu metody Object.ToString(). Poznasz tych członków podczas pisania kodu na potrzeby tego samouczka. Rekordy obsługują with wyrażenia umożliwiające niedestrukcyjną mutację rekordów.

Możesz również zadeklarować rekordy pozycyjne przy użyciu bardziej zwięzłej składni. Kompilator syntetyzuje więcej metod podczas deklarowania rekordów pozycyjnych:

  • Podstawowy konstruktor, którego parametry pasują do parametrów pozycyjnych w deklaracji rekordu.
  • Właściwości publiczne dla każdego parametru podstawowego konstruktora. Te właściwości są przeznaczone tylko dla record class typów i readonly record struct typów. W przypadku record struct typów są one odczytywane i zapisywane.
  • Metoda Deconstruct wyodrębniania właściwości z rekordu.

Kompilowanie danych dotyczących temperatury

Dane i statystyki należą do scenariuszy, w których należy używać rekordów. Na potrzeby tego samouczka utworzysz aplikację, która oblicza dni stopnia dla różnych zastosowań. Dni stopniowe są miarą ciepła (lub braku ciepła) w okresie dni, tygodni lub miesięcy. Stopień dni śledzi i przewiduje zużycie energii. Bardziej gorące dni oznaczają więcej klimatyzacji, a bardziej chłodniejsze dni oznaczają więcej użycia pieca. Dni stopni pomagają zarządzać populacjami roślin i skorelować wzrost roślin w miarę zmian sezonów. Dni stopni pomagają śledzić migracje zwierząt dla gatunków, które podróżują w celu dopasowania do klimatu.

Formuła jest oparta na średniej temperaturze w danym dniu i temperaturze bazowej. Aby obliczyć liczbę dni w czasie, będziesz potrzebować wysokiej i niskiej temperatury każdego dnia przez pewien czas. Zacznijmy od utworzenia nowej aplikacji. Utwórz nową aplikację konsolową. Utwórz nowy typ rekordu w nowym pliku o nazwie "DailyTemperature.cs":

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

Powyższy kod definiuje rekord pozycyjny. Rekord DailyTemperature jest wartością readonly record struct, ponieważ nie zamierzasz jej dziedziczyć i powinien być niezmienny. Właściwości HighTemp i LowTempwłaściwościami inicjowania tylko, co oznacza, że można je ustawić w konstruktorze lub za pomocą inicjatora właściwości. Jeśli chcesz, aby parametry pozycyjne do odczytu i zapisu, należy zadeklarować wartość record structreadonly record structzamiast . Typ DailyTemperature ma również podstawowy konstruktor , który ma dwa parametry zgodne z dwiema właściwościami. Aby zainicjować rekord, należy użyć konstruktora podstawowego DailyTemperature . Poniższy kod tworzy i inicjuje kilka DailyTemperature rekordów. Pierwsze używa nazwanych parametrów w celu wyjaśnienia parametrów HighTemp i LowTemp. Pozostałe inicjatory używają parametrów pozycyjnych do inicjowania parametrów HighTemp i LowTemp:

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) 
];

Możesz dodać własne właściwości lub metody do rekordów, w tym rekordy pozycyjne. Należy obliczyć średnią temperaturę dla każdego dnia. Możesz dodać właściwość do rekordu DailyTemperature :

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

Upewnijmy się, że możesz używać tych danych. Dodaj następujący kod do metody Main :

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

Uruchom aplikację i zobaczysz dane wyjściowe podobne do poniższego ekranu (kilka wierszy usuniętych dla miejsca):

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 }

Powyższy kod przedstawia dane wyjściowe z przesłonięcia syntetyzowanego ToString przez kompilator. Jeśli wolisz inny tekst, możesz napisać własną wersję ToString , która uniemożliwia kompilatorowi synchronizowanie wersji.

Dni stopni obliczeniowych

Aby obliczyć liczbę dni, należy wziąć różnicę od temperatury bazowej i średniej temperatury w danym dniu. Aby zmierzyć ciepło w czasie, należy odrzucić wszystkie dni, w których średnia temperatura jest poniżej punktu odniesienia. Aby zmierzyć zimno w czasie, należy odrzucić wszystkie dni, w których średnia temperatura przekracza punkt odniesienia. Na przykład STANY Zjednoczone używają wartości 65F jako podstawy zarówno w dniach ogrzewania, jak i chłodzenia. Jest to temperatura, w której nie jest potrzebne ogrzewanie ani chłodzenie. Jeśli dzień ma średnią temperaturę 70F, ten dzień wynosi pięć dni chłodzących i zero dni ogrzewania. Z drugiej strony, jeśli średnia temperatura wynosi 55F, ten dzień wynosi 10 dni ogrzewania i 0 dni chłodzenia.

Te formuły można wyrazić jako małą hierarchię typów rekordów: typ dnia abstrakcyjnego stopnia i dwa konkretne typy dni ogrzewania i dni ochładzania. Mogą to być również rekordy pozycyjne. Przyjmują one temperaturę bazową i sekwencję rekordów dziennej temperatury jako argumenty dla podstawowego konstruktora:

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

Rekord abstrakcyjny DegreeDays jest udostępnioną klasą bazową dla rekordów HeatingDegreeDays i CoolingDegreeDays . Podstawowe deklaracje konstruktora w rekordach pochodnych pokazują, jak zarządzać inicjowaniem rekordu podstawowego. Rekord pochodny deklaruje parametry dla wszystkich parametrów w konstruktorze podstawowym rekordu podstawowego. Rekord podstawowy deklaruje i inicjuje te właściwości. Rekord pochodny nie ukrywa ich, ale tworzy i inicjuje właściwości parametrów, które nie są deklarowane w rekordzie podstawowym. W tym przykładzie rekordy pochodne nie dodają nowych podstawowych parametrów konstruktora. Przetestuj kod, dodając następujący kod do metody Main :

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

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

Zostaną wyświetlone dane wyjściowe podobne do następujących:

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

Definiowanie metod syntetyzowanych kompilatora

Kod oblicza prawidłową liczbę dni ogrzewania i chłodzenia w danym okresie. W tym przykładzie pokazano jednak, dlaczego warto zastąpić niektóre z syntetyzowanych metod rekordów. Możesz zadeklarować własną wersję dowolnej z metod syntetyzowanych kompilatora w typie rekordu z wyjątkiem metody clone. Metoda klonowania ma nazwę wygenerowaną przez kompilator i nie można podać innej implementacji. Te syntetyzowane metody obejmują konstruktor kopiujący, składowe interfejsu System.IEquatable<T> , równości i nierówności oraz GetHashCode(). W tym celu zsyntetyzujesz PrintMembers. Możesz również zadeklarować własne ToString, ale PrintMembers zapewnia lepszą opcję dla scenariuszy dziedziczenia. Aby zapewnić własną wersję metody syntetyzowanej, podpis musi być zgodny z syntetyzowanym sposobem.

Element TempRecords w danych wyjściowych konsoli nie jest przydatny. Wyświetla typ, ale nic innego. To zachowanie można zmienić, udostępniając własną implementację syntetyzowanej PrintMembers metody. Podpis zależy od modyfikatorów zastosowanych do deklaracji record :

  • Jeśli typ rekordu to sealed, lub record struct, podpis to private bool PrintMembers(StringBuilder builder);
  • Jeśli typ rekordu nie sealed jest i pochodzi z object (oznacza to, że nie deklaruje rekordu podstawowego), podpis jest protected virtual bool PrintMembers(StringBuilder builder);
  • Jeśli typ rekordu nie sealed jest i pochodzi z innego rekordu, podpis jest protected override bool PrintMembers(StringBuilder builder);

Te reguły są najłatwiej zrozumieć poprzez zrozumienie celu .PrintMembers PrintMembers Dodaje informacje o każdej właściwości w typie rekordu do ciągu. Kontrakt wymaga, aby rekordy podstawowe dodawały swoje elementy członkowskie do wyświetlania i zakłada, że pochodne elementy członkowskie będą dodawać ich składowe. Każdy typ rekordu ToString syntetyzuje przesłonięcia, które wygląda podobnie do następującego przykładu dla HeatingDegreeDays:

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

Zadeklarujesz metodę PrintMembers w rekordzie DegreeDays , która nie wyświetla typu kolekcji:

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

Podpis deklaruje metodę zgodną z wersją virtual protected kompilatora. Nie martw się, jeśli otrzymasz metody dostępu źle; język wymusza prawidłowy podpis. Jeśli zapomnisz poprawne modyfikatory dla dowolnej zsyntetyzowanej metody, kompilator wystawia ostrzeżenia lub błędy, które ułatwiają uzyskanie odpowiedniego podpisu.

W języku C# 10 lub nowszym można zadeklarować metodę ToString jako sealed typ rekordu. Zapobiega to dostarczaniu nowej implementacji rekordów pochodnych. Rekordy pochodne będą nadal zawierać PrintMembers przesłonięcia. Jeśli nie chcesz, aby był wyświetlany typ środowiska uruchomieniowego rekordu, należy go przypieczętować ToString . W poprzednim przykładzie utracisz informacje o tym, gdzie rekord mierzył dni ogrzewania lub chłodzenia.

Mutacja niedestrukcyjną

Syntetyzowane składowe w klasie rekordów pozycyjnych nie modyfikują stanu rekordu. Celem jest łatwiejsze tworzenie niezmiennych rekordów. Pamiętaj, że deklarujesz obiekt , readonly record struct aby utworzyć niezmienną strukturę rekordu. Ponownie przyjrzyj się poprzednim deklaracjom dla HeatingDegreeDays i CoolingDegreeDays. Członkowie dodani wykonują obliczenia na wartościach rekordu, ale nie są w staniemutacji. Rekordy pozycyjne ułatwiają tworzenie niezmiennych typów odwołań.

Tworzenie niezmiennych typów odwołań oznacza, że należy użyć mutacji niedestrukcyjnej. Tworzone są nowe wystąpienia rekordów podobne do istniejących wystąpień rekordów przy użyciu with wyrażeń. Te wyrażenia są konstrukcją kopii z dodatkowymi przypisaniami, które modyfikują kopię. Wynikiem jest nowe wystąpienie rekordu, w którym każda właściwość została skopiowana z istniejącego rekordu i opcjonalnie zmodyfikowana. Oryginalny rekord pozostaje niezmieniony.

Dodajmy kilka funkcji do programu, które demonstrują with wyrażenia. Najpierw utwórzmy nowy rekord w celu obliczenia rosnącego stopnia dni przy użyciu tych samych danych. Dni rosnącego stopnia zwykle używają wartości 41F jako punktu odniesienia i mierzy temperatury powyżej punktu odniesienia. Aby użyć tych samych danych, możesz utworzyć nowy rekord podobny do coolingDegreeDays, ale z inną temperaturą bazową:

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

Można porównać liczbę stopni obliczonych z liczbami wygenerowanymi z wyższą temperaturą punktu odniesienia. Należy pamiętać, że rekordy są typami referencyjnymi, a kopie te są płytkie. Tablica danych nie jest kopiowana, ale oba rekordy odwołują się do tych samych danych. Fakt ten jest zaletą w jednym innym scenariuszu. W przypadku dni rosnącego stopnia warto śledzić sumę w ciągu ostatnich pięciu dni. Nowe rekordy można tworzyć z różnymi danymi źródłowymi przy użyciu with wyrażeń. Poniższy kod tworzy kolekcję tych akumulowania, a następnie wyświetla wartości:

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

Możesz również użyć with wyrażeń do tworzenia kopii rekordów. Nie określaj żadnych właściwości między nawiasami klamrowymi wyrażenia with . Oznacza to utworzenie kopii i nie zmieniaj żadnych właściwości:

var growingDegreeDaysCopy = growingDegreeDays with { };

Uruchom zakończoną aplikację, aby wyświetlić wyniki.

Podsumowanie

W tym samouczku pokazano kilka aspektów rekordów. Rekordy zapewniają zwięzłą składnię typów, w których podstawowym zastosowaniem jest przechowywanie danych. W przypadku klas zorientowanych na obiekty podstawowe zastosowanie definiuje obowiązki. Ten samouczek koncentruje się na rekordach pozycyjnych, w których można użyć zwięzłej składni do deklarowania właściwości rekordu. Kompilator syntetyzuje kilka elementów członkowskich rekordu do kopiowania i porównywania rekordów. Możesz dodać inne elementy członkowskie, których potrzebujesz dla typów rekordów. Można utworzyć niezmienne typy rekordów, wiedząc, że żaden z elementów członkowskich wygenerowanych przez kompilator nie będzie modyfikował stanu. Wyrażenia with ułatwiają obsługę mutacji niedestrukcyjnej.

Rekordy dodają kolejny sposób definiowania typów. Definicje służą class do tworzenia hierarchii zorientowanych na obiekty, które koncentrują się na obowiązkach i zachowaniu obiektów. Typy są tworzone struct dla struktur danych, które przechowują dane i są wystarczająco małe, aby wydajnie kopiować. Typy są tworzone record , gdy chcesz używać równości i porównania opartego na wartościach, nie chcesz kopiować wartości i chcesz używać zmiennych referencyjnych. Typy są tworzone record struct , gdy chcesz, aby funkcje rekordów dla typu, który jest wystarczająco mały, aby wydajnie kopiować.

Więcej informacji na temat rekordów można uzyskać w artykule referencyjnym języka C# dla typu rekordu oraz specyfikacji proponowanego typu rekordu i specyfikacji struktury rekordów.