Skapa posttyper

Poster är typer som använder värdebaserad likhet. C# 10 lägger till poststrukturer så att du kan definiera poster som värdetyper. Två variabler av en posttyp är lika med om posttypdefinitionerna är identiska, och om värdena i båda posterna för varje fält är lika. Två variabler av en klasstyp är lika om de objekt som avses är samma klasstyp och variablerna refererar till samma objekt. Värdebaserad likhet innebär andra funktioner som du förmodligen vill ha i posttyper. Kompilatorn genererar många av dessa medlemmar när du deklarerar en record i stället för en class. Kompilatorn genererar samma metoder för record struct typer.

I den här självstudien får du lära dig att:

  • Bestäm om du lägger till modifieraren i record en class typ.
  • Deklarera posttyper och positionella posttyper.
  • Ersätt dina metoder med kompilatorgenererade metoder i poster.

Förutsättningar

Du måste konfigurera datorn så att den kör .NET 6 eller senare, inklusive kompilatorn C# 10 eller senare. C# 10-kompilatorn är tillgänglig från och med Visual Studio 2022 eller .NET 6 SDK.

Egenskaper för poster

Du definierar en post genom att deklarera en typ med nyckelordet record , ändra en class eller struct deklaration. Du kan också utelämna nyckelordet class för att skapa en record class. En post följer värdebaserad likhetssemantik. För att framtvinga värdesemantik genererar kompilatorn flera metoder för posttypen (både för record class typer och record struct typer):

Poster ger också en åsidosättning av Object.ToString(). Kompilatorn syntetiserar metoder för att visa poster med hjälp av Object.ToString(). Du kommer att utforska dessa medlemmar när du skriver koden för den här självstudien. Poster stöder with uttryck för att aktivera icke-destruktiv mutation av poster.

Du kan också deklarera positionella poster med hjälp av en mer koncis syntax. Kompilatorn syntetiserar fler metoder åt dig när du deklarerar positionella poster:

  • En primär konstruktor vars parametrar matchar positionsparametrarna i postdeklarationen.
  • Offentliga egenskaper för varje parameter i en primär konstruktor. Dessa egenskaper är init-only för record class typer och readonly record struct typer. För record struct typer är de skrivskyddade.
  • En Deconstruct metod för att extrahera egenskaper från posten.

Skapa temperaturdata

Data och statistik är några av de scenarier där du vill använda poster. I den här självstudien skapar du ett program som beräknar examensdagar för olika användningsområden. Examensdagar är ett mått på värme (eller brist på värme) under en period av dagar, veckor eller månader. Examensdagar spårar och förutsäger energianvändning. Fler varmare dagar innebär mer luftkonditionering, och kallare dagar innebär mer ugnsanvändning. Examensdagar hjälper till att hantera växtpopulationer och korrelerar med växttillväxten när årstiderna förändras. Examensdagar hjälper till att spåra djurmigreringar för arter som reser för att matcha klimatet.

Formeln baseras på medeltemperaturen på en viss dag och en baslinjetemperatur. För att beräkna graddagar över tid behöver du den höga och låga temperaturen varje dag under en tidsperiod. Vi börjar med att skapa ett nytt program. Skapa ett nytt konsolprogram. Skapa en ny posttyp i en ny fil med namnet "DailyTemperature.cs":

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

Föregående kod definierar en positionell post. Posten DailyTemperature är en readonly record struct, eftersom du inte tänker ärva från den, och den bör vara oföränderlig. HighTemp Egenskaperna och LowTemp är endast init-egenskaper, vilket innebär att de kan anges i konstruktorn eller med hjälp av en egenskapsinitierare. Om du vill att positionsparametrarna ska vara skrivskyddade deklarerar du en record struct i stället för en readonly record struct. Typen DailyTemperature har också en primär konstruktor som har två parametrar som matchar de två egenskaperna. Du använder den primära konstruktorn för att initiera en DailyTemperature post. Följande kod skapar och initierar flera DailyTemperature poster. Den första använder namngivna parametrar för att klargöra HighTemp och LowTemp. De återstående initierarna använder positionsparametrar för att initiera HighTemp och 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) 
];

Du kan lägga till egna egenskaper eller metoder i poster, inklusive positionella poster. Du måste beräkna medeltemperaturen för varje dag. Du kan lägga till egenskapen i posten DailyTemperature :

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

Nu ska vi se till att du kan använda dessa data. Lägg till följande kod i din Main metod:

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

Kör programmet så visas utdata som ser ut ungefär så här (flera rader har tagits bort för utrymme):

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 }

Föregående kod visar utdata från åsidosättningen av ToString syntetiserad av kompilatorn. Om du föredrar annan text kan du skriva en egen version av ToString den som förhindrar att kompilatorn syntetiserar en version åt dig.

Dagar för beräkningsexamen

För att beräkna gradersdagar tar du skillnaden från en baslinjetemperatur och medeltemperaturen en viss dag. Om du vill mäta värme över tid tar du bort alla dagar där medeltemperaturen ligger under baslinjen. Om du vill mäta kyla över tid tar du bort alla dagar där medeltemperaturen ligger över baslinjen. Usa använder till exempel 65F som bas för både uppvärmnings- och kylningsgrader. Det är temperaturen där ingen uppvärmning eller kylning behövs. Om en dag har en medeltemperatur på 70F, är den dagen fem kylning grader dagar och noll uppvärmning grader dagar. Omvänt, om medeltemperaturen är 55F, är den dagen 10 uppvärmning grader dagar och 0 kylning grader dagar.

Du kan uttrycka dessa formler som en liten hierarki med posttyper: en abstrakt typ av examensdag och två konkreta typer för uppvärmningsgrader och dagar med kylningsgrader. Dessa typer kan också vara positionella poster. De tar en baslinjetemperatur och en sekvens med dagliga temperaturposter som argument till den primära konstruktorn:

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

Den abstrakta DegreeDays posten är den delade basklassen för både posterna HeatingDegreeDays och CoolingDegreeDays . De primära konstruktordeklarationerna på de härledda posterna visar hur du hanterar initiering av basposter. Din härledda post deklarerar parametrar för alla parametrar i den primära konstruktorn för basposten. Basposten deklarerar och initierar dessa egenskaper. Den härledda posten döljer dem inte, utan skapar och initierar bara egenskaper för parametrar som inte deklareras i basposten. I det här exemplet lägger de härledda posterna inte till nya primära konstruktorparametrar. Testa koden genom att lägga till följande kod i din Main metod:

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

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

Du får utdata som i följande visning:

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

Definiera kompilatorsyntetiserade metoder

Koden beräknar rätt antal dagar för uppvärmning och kylning under den tidsperioden. Men det här exemplet visar varför du kanske vill ersätta några av de syntetiserade metoderna för poster. Du kan deklarera din egen version av någon av de kompilatorsyntetiserade metoderna i en posttyp förutom klonmetoden. Klonmetoden har ett kompilatorgenererat namn och du kan inte ange någon annan implementering. Dessa syntetiserade metoder omfattar en kopieringskonstruktor, medlemmarna System.IEquatable<T> i gränssnittet, likhets- och ojämlikhetstester och GetHashCode(). För det här ändamålet ska du syntetisera PrintMembers. Du kan också deklarera dina egna ToString, men PrintMembers ger ett bättre alternativ för arvsscenarier. Om du vill ange din egen version av en syntetiserad metod måste signaturen matcha den syntetiserade metoden.

Elementet TempRecords i konsolens utdata är inte användbart. Den visar typen, men inget annat. Du kan ändra det här beteendet genom att tillhandahålla en egen implementering av den syntetiserade PrintMembers metoden. Signaturen är beroende av modifierare som tillämpas på deklarationen record :

  • Om en posttyp är sealed, eller en record struct, är signaturen private bool PrintMembers(StringBuilder builder);
  • Om en posttyp inte sealed är och härleds från object (det vill säga att den inte deklarerar en baspost) är signaturen protected virtual bool PrintMembers(StringBuilder builder);
  • Om en posttyp inte sealed är och härleds från en annan post, är signaturen protected override bool PrintMembers(StringBuilder builder);

De här reglerna är enklast att förstå genom att förstå syftet med PrintMembers. PrintMembers lägger till information om varje egenskap i en posttyp i en sträng. Kontraktet kräver basposter för att lägga till sina medlemmar i visningen och förutsätter att härledda medlemmar lägger till sina medlemmar. Varje posttyp syntetiserar en ToString åsidosättning som liknar följande exempel för HeatingDegreeDays:

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

Du deklarerar en PrintMembers metod i posten DegreeDays som inte skriver ut typen av samling:

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

Signaturen deklarerar en virtual protected metod som matchar kompilatorns version. Oroa dig inte om du får åtkomsterna fel; språket framtvingar rätt signatur. Om du glömmer rätt modifierare för någon syntetiserad metod utfärdar kompilatorn varningar eller fel som hjälper dig att få rätt signatur.

I C# 10 och senare kan du deklarera ToString metoden som sealed i en posttyp. Det förhindrar att härledda poster tillhandahåller en ny implementering. Härledda poster innehåller fortfarande åsidosättningen PrintMembers . Du skulle försegla ToString om du inte vill att den ska visa postens körningstyp. I föregående exempel skulle du förlora informationen om var posten mätte uppvärmnings- eller kylningsgradsdagar.

Icke-destruktiv mutation

De syntetiserade medlemmarna i en positionell postklass ändrar inte postens tillstånd. Målet är att du enklare kan skapa oföränderliga poster. Kom ihåg att du deklarerar en readonly record struct för att skapa en oföränderlig poststruct. Titta igen på föregående deklarationer för HeatingDegreeDays och CoolingDegreeDays. De medlemmar som har lagts till utför beräkningar på värdena för posten, men muterar inte tillståndet. Positionsposter gör det enklare för dig att skapa oföränderliga referenstyper.

Att skapa oföränderliga referenstyper innebär att du vill använda icke-destruktiv mutation. Du skapar nya postinstanser som liknar befintliga postinstanser med hjälp av with uttryck. Dessa uttryck är en kopieringskonstruktion med ytterligare tilldelningar som ändrar kopian. Resultatet är en ny postinstans där varje egenskap har kopierats från den befintliga posten och eventuellt ändrats. Den ursprungliga posten är oförändrad.

Nu ska vi lägga till några funktioner i ditt program som demonstrerar with uttryck. Först ska vi skapa en ny post för att beräkna växande graddagar med samma data. Växande graddagar använder vanligtvis 41F som baslinje och mäter temperaturer över baslinjen. Om du vill använda samma data kan du skapa en ny post som liknar coolingDegreeDays, men med en annan bastemperatur:

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

Du kan jämföra antalet grader som beräknas med de tal som genereras med en högre baslinjetemperatur. Kom ihåg att poster är referenstyper och att dessa kopior är ytliga kopior. Matrisen för data kopieras inte, men båda posterna refererar till samma data. Detta faktum är en fördel i ett annat scenario. För växande examensdagar är det bra att hålla reda på summan för de senaste fem dagarna. Du kan skapa nya poster med olika källdata med hjälp av with uttryck. Följande kod skapar en samling av dessa ackumuleringar och visar sedan värdena:

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

Du kan också använda with uttryck för att skapa kopior av poster. Ange inga egenskaper mellan klammerparenteserna för with uttrycket. Det innebär att skapa en kopia och inte ändra några egenskaper:

var growingDegreeDaysCopy = growingDegreeDays with { };

Kör det färdiga programmet för att se resultatet.

Sammanfattning

Den här självstudien visade flera aspekter av poster. Poster ger en kortfattad syntax för typer där den grundläggande användningen är lagring av data. För objektorienterade klasser är den grundläggande användningen att definiera ansvarsområden. Den här självstudien fokuserar på positionella poster, där du kan använda en koncis syntax för att deklarera egenskaperna för en post. Kompilatorn syntetiserar flera medlemmar i posten för att kopiera och jämföra poster. Du kan lägga till andra medlemmar som du behöver för dina posttyper. Du kan skapa oföränderliga posttyper med vetskapen om att ingen av de kompilatorgenererade medlemmarna skulle mutera tillståndet. Och with uttryck gör det enkelt att stödja icke-destruktiv mutation.

Poster lägger till ett annat sätt att definiera typer. Du använder class definitioner för att skapa objektorienterade hierarkier som fokuserar på objektens ansvarsområden och beteende. Du skapar struct typer för datastrukturer som lagrar data och är tillräckligt små för att kopiera effektivt. Du skapar record typer när du vill ha värdebaserad likhet och jämförelse, inte vill kopiera värden och vill använda referensvariabler. Du skapar record struct typer när du vill ha funktionerna i poster för en typ som är tillräckligt liten för att kopiera effektivt.

Du kan lära dig mer om poster i C#-språkreferensartikeln för posttypen och den föreslagna posttypspecifikationen och poststruktureringsspecifikationen.