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átorrecord
. - 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
):
- Přepsání Object.Equals(Object).
- Virtuální
Equals
metoda, jejíž parametr je typ záznamu. - Potlačení Object.GetHashCode().
- Metody pro
operator ==
aoperator !=
. - Typy záznamů implementují System.IEquatable<T>.
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 typyreadonly 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
sealed
neborecord struct
, podpis jeprivate bool PrintMembers(StringBuilder builder);
- Pokud typ záznamu není
sealed
a odvozuje se odobject
(to znamená, že nedefinuje základní záznam), je podpisprotected virtual bool PrintMembers(StringBuilder builder);
. - Pokud typ záznamu není
sealed
a odvozuje se z jiného záznamu, signatura jeprotected 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 .