Vytvoření typů záznamů

Záznamy jsou typy, které používají rovnost na základě hodnot. C# 10 přidá struktury záznamů, abyste mohli definovat záznamy jako typy hodnot. Dvě proměnné typu záznamu jsou stejné, pokud jsou definice typu záznamu stejné a pokud jsou pro každé pole hodnoty v obou záznamech stejné. Dvě proměnné typu třídy jsou stejné, pokud jsou objekty odkazující na stejný typ třídy a proměnné odkazují na stejný objekt. Rovnost založená na hodnotách znamená další možnosti, které budete pravděpodobně chtít v typech záznamů. Kompilátor generuje mnoho z těchto členů, když deklarujete místo recordclass. Kompilátor generuje stejné metody pro record struct typy.

V tomto kurzu se naučíte:

  • Rozhodněte se, jestli do typu přidáte record modifikátor class .
  • Deklarujte typy záznamů a typy pozičních záznamů.
  • Nahraďte metody generované kompilátorem v záznamech.

Předpoklady

Budete muset nastavit počítač tak, aby běžel .NET 6 nebo novější, včetně kompilátoru C# 10 nebo novějšího. Kompilátor C# 10 je k dispozici od sady Visual Studio 2022 nebo sady .NET 6 SDK.

Charakteristiky záznamů

Záznam definujete deklarací typu pomocí klíčového record slova, úpravou class nebo struct deklarace. Volitelně můžete vynechat class klíčové slovo k vytvoření record class. Záznam se řídí sémantikou rovnosti na základě hodnot. Kompilátor vynucuje sémantiku hodnot několika metod pro váš typ záznamu (pro record class typy i record struct typy):

Záznamy také poskytují přepsání .Object.ToString() Kompilátor syntetizuje metody pro zobrazení záznamů pomocí Object.ToString(). Tyto členy prozkoumáte při psaní kódu pro tento kurz. Záznamy podporují with výrazy, které umožňují nedestruktivní mutování záznamů.

Poziční záznamy můžete deklarovat také pomocí stručnější syntaxe. Kompilátor pro vás syntetizuje více metod při deklaraci pozičních záznamů:

  • Primární konstruktor, jehož parametry odpovídají pozičním parametrům deklarace záznamu.
  • Veřejné vlastnosti pro každý parametr primárního konstruktoru Tyto vlastnosti jsou pouze inicializační pro record class typy a readonly record struct typy. U record struct typů se jedná o čtení i zápis.
  • Metoda Deconstruct extrakce vlastností ze záznamu.

Sestavení dat o teplotě

Data a statistiky patří mezi scénáře, ve kterých chcete použít záznamy. V tomto kurzu vytvoříte aplikaci, která počítá dny stupňů pro různá použití. Dny stupňů jsou měřením tepla (nebo nedostatkem tepla) v období dnů, týdnů nebo měsíců. Dny stupňů sledují a předpovídají spotřebu energie. Čím více horké dny, znamená více klimatizace a chladnější dny znamenají více využití pece. Dny stupňů pomáhají spravovat populace rostlin a korelovat s růstem rostlin při změně ročních období. Dny stupňů pomáhají sledovat migrace zvířat pro druhy, které cestují tak, aby odpovídaly klimatu.

Vzorec je založen na střední teplotě v daném dni a základní teplotě. K výpočtu stupně dnů v čase budete potřebovat vysokou a nízkou teplotu každý den po určitou dobu. Začněme vytvořením nové aplikace. Vytvořte novou konzolovou aplikaci. Vytvořte nový typ záznamu v novém souboru s názvem "DailyTemperature.cs":

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

Předchozí kód definuje poziční záznam. Záznam DailyTemperature je záznam readonly record struct, protože nemáte v úmyslu zdědit z něj a měl by být neměnný. Vlastnosti HighTemp jsou inicializační pouze vlastnosti, což znamená, že mohou být nastaveny v konstruktoru nebo pomocí inicializátoru LowTemp vlastností. Pokud byste chtěli, aby poziční parametry byly pro čtení i zápis, deklarujete místo record structreadonly record struct. Typ DailyTemperaturetaké primární konstruktor , který má dva parametry, které odpovídají dvěma vlastnostem. K inicializaci záznamu DailyTemperature použijete primární konstruktor. Následující kód vytvoří a inicializuje několik DailyTemperature záznamů. První používá pojmenované parametry k objasnění HighTemp a LowTemp. Zbývající inicializátory používají poziční parametry k inicializaci parametru HighTemp a 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) 
];

Do záznamů můžete přidat vlastní vlastnosti nebo metody, včetně pozičních záznamů. Budete muset vypočítat průměrnou teplotu pro každý den. Tuto vlastnost můžete přidat do záznamu DailyTemperature :

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

Pojďme se ujistit, že tato data můžete použít. Do metody přidejte následující kód Main :

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

Spusťte aplikaci a zobrazí se výstup podobný následujícímu zobrazení (několik řádků odebráno pro místo):

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 }

Předchozí kód ukazuje výstup z přepsání ToString syntetizované kompilátorem. Pokud dáváte přednost jinému textu, můžete napsat vlastní verzi ToString , která brání kompilátoru v syntetizaci verze za vás.

Dny výpočetního stupně

Pokud chcete vypočítat dny, vezmete rozdíl od základní teploty a střední teploty v daném dni. Pokud chcete měřit teplo v průběhu času, zahoďte všechny dny, ve kterých je střední teplota nižší než směrný plán. Pokud chcete měřit chlad v průběhu času, zahodíte všechny dny, ve kterých je střední teplota vyšší než směrný plán. Například USA používají jako základnu 65F jak pro vytápění, tak pro dny chladicího stupně. To je teplota, ve které není potřeba topení ani chlazení. Pokud má den průměrnou teplotu 70F, je tento den pět dní chlazení a nulový stupeň vytápění dny. Pokud je naopak průměrná teplota 55F, je tento den 10 dní ve stupních vytápění a 0 dnů chlazení.

Tyto vzorce můžete vyjádřit jako malou hierarchii typů záznamů: typ dne abstraktního stupně a dva konkrétní typy pro dny vytápění a dny stupně chlazení. Tyto typy mohou být také poziční záznamy. Jako argumenty primárního konstruktoru přebírají základní teplotu a posloupnost záznamů o denní teplotě:

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

Abstraktní DegreeDays záznam je sdílenou základní třídou pro HeatingDegreeDays záznamy i CoolingDegreeDays záznamy. Deklarace primárního konstruktoru na odvozených záznamech ukazují, jak spravovat inicializaci základního záznamu. Odvozený záznam deklaruje parametry pro všechny parametry v primárním konstruktoru základního záznamu. Základní záznam deklaruje a inicializuje tyto vlastnosti. Odvozený záznam je neskryje, ale vytvoří a inicializuje vlastnosti parametrů, které nejsou deklarovány v základním záznamu. V tomto příkladu odvozené záznamy nepřidají nové parametry primárního konstruktoru. Otestujte kód přidáním následujícího kódu do metody Main :

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

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

Zobrazí se výstup podobný následujícímu zobrazení:

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

Definování syntetizovaných metod kompilátoru

Kód vypočítá správný počet dnů ve stupních vytápění a chlazení v daném časovém období. Tento příklad ale ukazuje, proč můžete chtít nahradit některé syntetizované metody pro záznamy. Můžete deklarovat vlastní verzi libovolné z metod syntetizovaných kompilátorem v typu záznamu s výjimkou metody klonování. Metoda klonování má vygenerovaný název kompilátoru a nemůžete zadat jinou implementaci. Tyto syntetizované metody zahrnují konstruktor kopírování, členy System.IEquatable<T> rozhraní, rovnost a nerovnost testy a GetHashCode(). Za tímto účelem syntetizujete PrintMembers. Můžete také deklarovat vlastní ToString, ale PrintMembers poskytuje lepší možnost pro scénáře dědičnosti. Pokud chcete poskytnout vlastní verzi syntetizované metody, musí podpis odpovídat syntetizované metodě.

Prvek TempRecords ve výstupu konzoly není užitečný. Zobrazí typ, ale nic jiného. Toto chování můžete změnit poskytnutím vlastní implementace syntetizované PrintMembers metody. Podpis závisí na modifikátorech použitých na record deklaraci:

  • Pokud je sealedtyp záznamu record structnebo a , podpis je private bool PrintMembers(StringBuilder builder);
  • Pokud typ záznamu není sealed a odvozuje se od object něj (to znamená, že deklaruje základní záznam), podpis je protected virtual bool PrintMembers(StringBuilder builder);
  • Pokud typ záznamu není sealed a odvozuje se z jiného záznamu, podpis je protected override bool PrintMembers(StringBuilder builder);

Tato pravidla jsou nejsnadnější pochopit pochopením účelu PrintMembers. PrintMembers přidá informace o každé vlastnosti v typu záznamu do řetězce. Smlouva vyžaduje, aby základní záznamy přidaly své členy do zobrazení a předpokládá, že odvozené členy přidají jejich členy. Každý typ záznamu ToString syntetizuje přepsání, které vypadá podobně jako v následujícím příkladu:HeatingDegreeDays

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

Deklarujete metodu PrintMembers v záznamu DegreeDays , který nevytiskne typ kolekce:

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

Podpis deklaruje metodu virtual protected odpovídající verzi kompilátoru. Nemějte obavy, pokud dostanete přístupové objekty špatně; jazyk vynucuje správný podpis. Pokud zapomenete správné modifikátory pro libovolnou syntetizovanou metodu, kompilátor vydá upozornění nebo chyby, které vám pomůžou získat správný podpis.

V jazyce C# 10 a novějším můžete deklarovat metodu ToString jako sealed v typu záznamu. Tím se zabrání, aby odvozené záznamy poskytovaly novou implementaci. Odvozené záznamy budou stále obsahovat přepsání PrintMembers . Pokud nechcete, aby zobrazoval typ modulu runtime záznamu, zapečetěli ToString byste ho. V předchozím příkladu byste ztratili informace o tom, kde záznam měří dobu vytápění nebo chladicího stupně.

Nedestruktivní mutaci

Syntetizované členy třídy pozičních záznamů nemění stav záznamu. Cílem je, abyste snadněji vytvářeli neměnné záznamy. Nezapomeňte, že deklarujete, readonly record struct že vytvoříte neměnnou strukturu záznamu. Znovu se podívejte na předchozí deklarace pro HeatingDegreeDays a CoolingDegreeDays. Přidaní členové provádějí výpočty s hodnotami záznamu, ale neztlumují stav. Poziční záznamy usnadňují vytváření neměnných referenčních typů.

Vytváření neměnných referenčních typů znamená, že budete chtít použít nedestruktivní mutaci. Vytvoříte nové instance záznamů, které se podobají existujícím instancím záznamů pomocí with výrazů. Tyto výrazy jsou konstrukce kopírování s dalšími přiřazeními, která upravují kopii. Výsledkem je nová instance záznamu, kde každá vlastnost byla zkopírována z existujícího záznamu a volitelně změněna. Původní záznam se nezmění.

Pojďme do programu přidat několik funkcí, které demonstrují with výrazy. Nejprve vytvoříme nový záznam pro výpočet rostoucího stupně dnů pomocí stejných dat. Rostoucí stupeň dnů obvykle používá 41F jako směrný plán a měří teploty nad směrný plán. Pokud chcete použít stejná data, můžete vytvořit nový záznam, který se podobá coolingDegreeDays, ale s jinou základní teplotou:

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

Můžete porovnat počet stupňů vypočítaných s čísly vygenerovanými s vyšší základní teplotou. Mějte na paměti, že záznamy jsou odkazové typy a tyto kopie jsou mělké kopie. Pole dat se nezkopíruje, ale oba záznamy odkazují na stejná data. Tato skutečnost je výhodou v jednom jiném scénáři. U rostoucích dnů stupňů je užitečné sledovat celkový součet za posledních pět dnů. Pomocí výrazů můžete vytvářet nové záznamy s různými zdrojovými daty with . Následující kód sestaví kolekci těchto akumulace a pak zobrazí hodnoty:

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

Pomocí výrazů můžete také with vytvářet kopie záznamů. Nezadávejte žádné vlastnosti mezi složenými závorkami výrazu with . To znamená, že vytvoříte kopii a nezměníte žádné vlastnosti:

var growingDegreeDaysCopy = growingDegreeDays with { };

Spuštěním dokončené aplikace zobrazte výsledky.

Shrnutí

Tento kurz ukázal několik aspektů záznamů. Záznamy poskytují stručnou syntaxi pro typy, ve kterých se základní použití ukládá data. Pro objektově orientované třídy je základním použitím definování odpovědností. Tento kurz se zaměřuje na poziční záznamy, kde můžete pomocí stručné syntaxe deklarovat vlastnosti záznamu. Kompilátor syntetizuje několik členů záznamu pro kopírování a porovnávání záznamů. Pro typy záznamů můžete přidat další členy, které potřebujete. Neměnné typy záznamů můžete vytvořit s vědomím, že žádný z členů vygenerovaných kompilátorem neztlumí stav. A with výrazy usnadňují podporu nedestruktivního mutování.

Záznamy přidávají další způsob, jak definovat typy. Definice slouží class k vytváření hierarchií orientovaných na objekty, které se zaměřují na zodpovědnosti a chování objektů. Vytváříte struct typy pro datové struktury, které ukládají data a jsou dostatečně malé, aby bylo kopírování efektivní. Typy vytvoříte record , když chcete rovnost a porovnání založené na hodnotách, nechcete kopírovat hodnoty a chcete použít referenční proměnné. Typy vytváříte record struct , pokud chcete, aby funkce záznamů pro typ, který je dostatečně malý k efektivnímu kopírování.

Další informace o záznamech najdete v referenčním článku jazyka C# pro typ záznamu a specifikaci navrhovaného typu záznamu a specifikaci struktury záznamů.