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 record
class
zamiast . 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 doclass
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):
- Przesłonięcia elementu Object.Equals(Object).
- Metoda wirtualna
Equals
, której parametr jest typem rekordu. - Przesłonięcia elementu Object.GetHashCode().
- Metody dla i
operator ==
operator !=
. - Typy rekordów implementują .System.IEquatable<T>
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 ireadonly record struct
typów. W przypadkurecord 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 LowTemp
są wł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 struct
readonly record struct
zamiast . 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
, lubrecord struct
, podpis toprivate bool PrintMembers(StringBuilder builder);
- Jeśli typ rekordu nie
sealed
jest i pochodzi zobject
(oznacza to, że nie deklaruje rekordu podstawowego), podpis jestprotected virtual bool PrintMembers(StringBuilder builder);
- Jeśli typ rekordu nie
sealed
jest i pochodzi z innego rekordu, podpis jestprotected 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.