Udostępnij za pośrednictwem


Tworzenie typów rekordów

Rekordy to typy, które używają porównywania wartości. Rekordy można definiować jako typy referencyjne lub 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 implikuje inne możliwości, które prawdopodobnie chciałbyś mieć w typach rekordów. Kompilator generuje wiele z tych członków podczas deklarowania elementu record zamiast class. Kompilator generuje te same metody dla record struct typów.

W tym poradniku nauczysz się, jak:

  • Zdecyduj, czy dodać modyfikator record do typu class.
  • Deklarowanie typów rekordów i typów rekordów pozycyjnych.
  • Zastąp metody wygenerowane przez kompilator swoimi metodami w rekordach.

Wymagania wstępne

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ć semantykę wartości, kompilator generuje kilka metod dla rekordów (zarówno dla typów record class, jak i record struct).

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

Możesz także zadeklarować rekordy pozycyjne używając 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.
  • Publiczne właściwości dla parametrów głównego 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 chcesz używać rekordów. W tym samouczku utworzysz aplikację, która oblicza dni stopniowe dla różnych zastosowań. Stopniodni 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 ciepła pomagają zarządzać populacjami roślin i korelować wzrost roślin ze zmianami sezonowymi. 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 typem readonly record struct, ponieważ nie zamierzasz go 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 były do odczytu i zapisu, należy zadeklarować record struct zamiast readonly record struct. Typ DailyTemperature ma również podstawowy konstruktor , który ma dwa parametry zgodne z dwiema właściwościami. Aby zainicjować rekord DailyTemperature, użyj konstruktora podstawowego. 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 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 następującego ekranu (kilka wierszy usuniętych dla oszczędności 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.

Stopniodni obliczeniowe

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ą 65 stopni Fahrenheita jako podstawy zarówno w dniach stopni ogrzewania, jak i dniach stopni chłodzenia. Jest to temperatura, w której nie jest potrzebne ogrzewanie ani chłodzenie. Jeśli dzień ma średnią temperaturę 70 F, ten dzień wynosi pięć dni chłodzących i zero dni ogrzewania. Z drugiej strony, jeśli średnia temperatura wynosi 55 F, ten dzień wynosi 10 dni ogrzewania i 0 dni chłodzenia.

Te formuły można wyrazić jako małą hierarchię typów rekordów: abstrakcyjnego typu dnia stopni oraz dwóch konkretnych typów dla dni stopni ogrzewania i dni stopni chłodzenia. 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 pierwotnym rekordu bazowego. 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);

Dane wyjściowe są wyświetlane w następujący sposób:

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. Ale ten przykład pokazuje, dlaczego możesz chcieć zastąpić niektóre z syntetyzowanych metod dla 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>, testy 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 jest sealed i pochodzi z object (oznacza to, że nie deklaruje rekordu podstawowego), podpis jest protected virtual bool PrintMembers(StringBuilder builder);.
  • Jeśli typ rekordu nie jest sealed i dziedziczy z innego rekordu, podpis jest protected override bool PrintMembers(StringBuilder builder);

Te reguły najłatwiej zrozumieć, poznając cel PrintMembers. PrintMembers Dodaje informacje o każdej właściwości przechowywanej w typie rekordu do ciągu znaków. Kontrakt wymaga, aby rekordy podstawowe dodawały swoich członków do wyświetlania i zakłada, że pochodne członkowie dodają swoich członków. Każdy typ rekordu ToString generuje przesłonięcie, które wyglądają podobnie do poniższego 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 źle zdefiniujesz akcesory; język wymusza prawidłową sygnaturę. Jeśli zapomnisz poprawne modyfikatory dla dowolnej zsyntetyzowanej metody, kompilator wystawia ostrzeżenia lub błędy, które ułatwiają uzyskanie odpowiedniego podpisu.

Typ rekordu pozwala zadeklarować metodę ToString jako sealed. Zapobiega to dostarczaniu nowej implementacji rekordów pochodnych. Rekordy pochodne będą nadal zawierać nadpisanie PrintMembers. 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 nieniszcząca

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, którzy zostali dodani, wykonują obliczenia na wartościach rekordu, ale nie zmieniają stanu. Rekordy pozycyjne ułatwiają tworzenie niezmiennych typów referencyjnych.

Tworzenie niezmiennych typów odwołań oznacza, że chcesz użyć mutacji niezniszczającej. 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 stopni wzrostu zazwyczaj przyjmują 41 F jako wartość bazową i mierzą temperatury powyżej tej wartości. 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 i że te kopie są płytkimi kopiami. 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 akumulacji, i 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 klamrami 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 poradniku 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 kilku członków rekordu do kopiowania i porównywania rekordów. Możesz dodać innych członków, 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ę niedestrukcyjnej mutacji.

Rekordy dodają kolejny sposób definiowania typów. Definicje class służą do tworzenia hierarchii obiektowych, które koncentrują się na odpowiedzialności i zachowaniu obiektów. Tworzysz struct typy dla struktur danych, które przechowują dane i są wystarczająco małe, aby można je było wydajnie kopiować. Tworzysz typy record wtedy, gdy chcesz używać równości i porównywania opartego na wartościach, nie chcesz kopiować wartości i chcesz używać zmiennych referencyjnych. Tworzysz typy record struct, gdy chcesz mieć funkcje rekordów dla typu, który jest na tyle mały, aby można było go wydajnie kopiować.

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