Sdílet prostřednictvím


Vytvoření typů záznamů

záznamy jsou typy, které používají hodnotovou rovnost. Záznamy můžete definovat jako odkazové typy nebo 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é pravděpodobně potřebujete v typech záznamů. Kompilátor generuje mnoho z těchto členů, když deklarujete record místo class. Kompilátor generuje stejné metody pro typy record struct.

V tomto kurzu se naučíte:

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

Požadavky

Musíte nastavit počítač tak, aby běžel .NET 6 nebo novější. Kompilátor jazyka C# je k dispozici s Visual Studio 2022 nebo .NET SDK.

Charakteristiky záznamů

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

Záznamy také poskytují přepsání hodnoty Object.ToString(). Kompilátor syntetizuje metody 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í mutaci záznamů.

Můžete také deklarovat poziční záznamy 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 pro typy record class a typy readonly record struct. U typů record struct jsou pro čtení i zápis .
  • Metoda Deconstruct pro extrakci vlastností ze záznamu.

Sestavení dat o teplotě

Data a statistiky patří mezi scénáře, ve kterých chcete použít záznamy. Pro účely tohoto kurzu vytvoříte aplikaci, která vypočítá dny stupňů pro různá použití. Dennostupně jsou ukazatelem tepla (nebo jeho nedostatku) za určité období dní, týdnů nebo měsíců. Dny stupňů sledují a předpovídají spotřebu energie. Více horkých dnů znamená více klimatizace a chladnější dny znamenají větší 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 readonly record struct, protože z něj nemáte v úmyslu dědit a měl by být neměnný. Vlastnosti HighTemp a LowTemp jsou pouze inicializovatelné vlastnosti , což znamená, že je lze nastavit v konstruktoru nebo pomocí inicializátoru vlastností. Pokud chcete, aby poziční parametry byly pro čtení i zápis, deklarujete record struct místo readonly record struct. Typ DailyTemperature má také primární konstruktor, který má dva parametry, které odpovídají dvěma vlastnostem. Primární konstruktor slouží k inicializaci DailyTemperature záznamu. 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 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ů. Potřebujete 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 Main přidejte následující kód:

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ů bylo odebráno pro mezeru):

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 syntetizován kompilátorem. Pokud dáváte přednost jinému textu, můžete napsat vlastní verzi ToString, která brání kompilátoru v synchronizaci verze za vás.

Výpočet stupňodnů

Pro výpočet denních stupňů vezmete rozdíl od referenční teploty a průměrné teploty v daném dni. Pro měření tepla v průběhu času vyloučíte všechny dny, ve kterých je střední teplota nižší než základní hodnota. Pokud chcete měřit chlad v průběhu času, vyřadíte všechny dny, ve kterých je střední teplota vyšší než referenční úroveň. Například USA používají jako základnu 65 F pro dobu vytápění i chlazení ve dnech. To je teplota, ve které není potřeba topení ani chlazení. Pokud má den průměrnou teplotu 70 °F, má tento den pět stupňů chlazení a nulový stupeň vytápění. Naopak, pokud je průměrná teplota 55 °F, je tento den 10 stupňů den vytápění a 0 stupňů den 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 do primárního konstruktoru přebírají základní teplotu a sérii záznamů denních 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í záznam DegreeDays je sdílenou základní třídou pro záznamy HeatingDegreeDays i CoolingDegreeDays. 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 denních stupňů 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í kopírovací konstruktor, členy rozhraní System.IEquatable<T>, testy rovnosti a nerovnosti 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 deklaraci record:

  • Pokud je typ záznamu sealednebo record struct, podpis je private bool PrintMembers(StringBuilder builder);
  • Pokud typ záznamu není sealed a odvozuje se od object (to znamená, že nedefinuje základní záznam), je podpis protected virtual bool PrintMembers(StringBuilder builder);.
  • Pokud typ záznamu není sealed a odvozuje se z jiného záznamu, signatura je protected override bool PrintMembers(StringBuilder builder);

Tato pravidla jsou nejsnadnější pochopit prostřednictvím pochopení účelu PrintMembers. PrintMembers přidává informace o každé vlastnosti datového typu 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í své členy. Každý typ záznamu syntetizuje překrytí ToString, které vypadá podobně jako v následujícím příkladu pro 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ý nezobrazuje typ kolekce:

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

Podpis deklaruje metodu virtual protected tak, aby odpovídala verzi kompilátoru. Nemějte obavy, pokud použijete špatné přístupové metody; 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.

Metodu ToString můžete deklarovat 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 výjimku PrintMembers. Pokud nechcete, aby zobrazoval typ modulu runtime záznamu, zapečetěli byste ToString. V předchozím příkladu byste ztratili informace o tom, kde záznam měří dobu vytápění nebo chladicího stupně.

Nedestruktivní mutace

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 k vytvoření neměnné struktury záznamu. Znovu se podívejte na předchozí deklarace pro HeatingDegreeDays a CoolingDegreeDays. Členové, kteří byli přidáni, provádějí výpočty s hodnotami záznamu, ale nemění 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 chcete použít nedestruktivní mutaci. Vytvoříte nové instance záznamů, které se podobají existujícím instancím záznamů pomocí výrazů with. Tyto výrazy jsou konstrukce kopírování s dodatečnými přiřazeními, která modifikují 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é předvádějí výrazy with. Nejprve vytvoříme nový záznam pro výpočet rostoucího stupně dnů pomocí stejných dat. Růstové teplotní dny obvykle používají 41 °F jako základní hodnotu a měří teploty nad touto hodnotou. Pokud chcete použít stejná data, můžete vytvořit nový záznam podobný 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 povrchové 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ů with můžete vytvářet nové záznamy s různými zdrojovými daty. 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);
}

K vytvoření kopií záznamů můžete také použít výrazy with. Mezi složené závorky výrazu with nezadávejte žádné vlastnosti. 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 návod ukázal několik aspektů záznamů. Záznamy poskytují stručnou syntaxi pro typy, jejichž základním účelem je ukládání dat. 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. Můžete vytvořit neměnné typy záznamů s vědomím, že žádný z členů vygenerovaných kompilátorem nezmění stav. A výrazy with usnadňují podporu nedestruktivní mutace.

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

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