Självstudie: Skapa sammansatta tilldelningsoperatorer

C#14 lägger till användardefinierade sammansatta tilldelningsoperatorer som gör det möjligt att ändra en datastruktur på plats i stället för att skapa en ny instans. I tidigare versioner av C#, uttrycket:

a += b;

Expanderades till följande kod:

// compiler-generated code prior to C# 13:
var tmp = a + b;
a = tmp;

Beroende på typen av aleder den här expansionen till överdrivna allokeringar för att skapa nya instanser eller kopiera värdena för flera egenskaper för att ange värden på kopian. Att lägga till en användardefinierad operator för += anger att en typ kan göra ett bättre jobb genom att uppdatera målobjektet på plats.

C# stöder den befintliga expansionen, men den använder den bara när en sammansatt användardefinierad operator inte är tillgänglig.

I den här handledningen kommer du att:

  • Kör startexemplet.
  • Identifiera flaskhalsar i koden.
  • Implementera nya sammansatta tilldelningsoperatorer.
  • Analysera det slutförda exemplet.

Förutsättningar

Analysera det ursprungliga provet

Kör startprogrammet. Du kan hämta den från dotnet/docs GitHub-lagringsplatsen. Exempelprogrammet simulerar spårning av konsertnärvaro på en teaterlokal. Simuleringen modellerar realistiska ankomstmönster under hela kvällen, från tidiga deltagare till huvudrusningen före showtime. Den här simuleringen visar objektallokeringarna när du använder traditionella operatorer jämfört med de effektivitetsvinster som är möjliga med användardefinierade sammansatta tilldelningsoperatorer.

Appen spårar närvaro genom flera teatergrindar (huvudvåning och balkongsektioner) när konsertbesökare anländer. Varje grind bibehåller antalet deltagare med hjälp av en GateAttendance datapost. Under simuleringen uppdaterar koden ofta dessa antal med hjälp av stegvisa (++) och tilläggsåtgärder (+=). Följande kod visar en del av simuleringen:

// Gate 1 - busiest entrance (target: ~100-130 people)
gates.MainFloorGates[0] += random.Next(8, 15);     // Corporate group
++gates.MainFloorGates[0];                          // Single patron
gates.MainFloorGates[0] += random.Next(20, 30);    // Tour/large group arrival
gates.MainFloorGates[0] += random.Next(5, 12);     // Family groups
++gates.MainFloorGates[0];                          // Solo attendee

// Gate 2 - second busiest (target: ~85-115 people)
gates.MainFloorGates[1] = gates.MainFloorGates[1] + random.Next(6, 12);  // Group booking
++gates.MainFloorGates[1];                          // Single patron
gates.MainFloorGates[1] += random.Next(18, 28);    // Large family/reunion
gates.MainFloorGates[1] += random.Next(8, 15);     // Corporate/business group
gates.MainFloorGates[1] += random.Next(4, 8);      // Couples/small groups
++gates.MainFloorGates[1];                          // Individual patron

Identifiera flaskhalsar i koden

Med traditionella operatorer skapar varje åtgärd en ny GateAttendance instans, vilket leder till betydande minnesallokeringar. Startprogrammet GateAttendance är oföränderligt. Koden kan inte ändra objektets tillstånd när det har initierats. Det designbeslutet kräver kopiering av objekt om du behöver ändra tillstånd.

När du kör simuleringen visas följande detaljerade utdata:

  • Närvaro vid varje gate under olika ankomstperioder.
  • Total närvarospårning över alla portar.
  • En slutlig omfattande rapport med närvarostatistik.

Följande text visar några exempelutdata:

Peak arrival time - all gates busy...

Peak rush period completed - all gates processed heavy traffic.

--- Gate Status After Main Rush (7:15 PM) ---
Main Floor Gates:
  Main-Floor-Gate-1: 145 attendees
  Main-Floor-Gate-2: 168 attendees
  Main-Floor-Gate-3: 149 attendees
  Main-Floor-Gate-4:  71 attendees
  Main Floor Subtotal: 533 attendees

Balcony Gates:
  Balcony-Gate-Left: 164 attendees
  Balcony-Gate-Right: 134 attendees
  Balcony Subtotal: 298 attendees

Total Current Attendance: 831 / 1000

--- Late Arrivals (7:15 PM - 7:30 PM) ---
Final patrons arriving before curtain...

Final arrivals processed - concert about to begin!

Granska startpostklassen GateAttendance :

public record class GateAttendance(string GateId)
{
    public int Count { get; init; }

    public static GateAttendance operator ++(GateAttendance gate)
    {
        GateAttendance updateGate = gate with { Count = gate.Count + 1 };
        return updateGate;
    }

    public static GateAttendance operator +(GateAttendance gate, int partySize)
    {
        GateAttendance updateGate = gate with { Count = gate.Count + partySize };
        return updateGate;
    }
}

Posten InitialImplementation.GateAttendance visar den traditionella metoden för överlagring av operatorer i C#. Observera hur både inkrementsoperatorn (++) och additionsoperatorn (+) skapar helt nya instanser med hjälp av uttrycket GateAttendance med with. Varje gång du skriver gate++ eller gate += partySizeallokerar operatorerna en ny postinstans med det uppdaterade Count värdet och returnerar sedan den nya instansen. Även om den här metoden upprätthåller oföränderlighet och trådsäkerhet, sker den på bekostnad av frekventa minnesallokeringar. I scenarier med många åtgärder, till exempel konsertsimuleringen med hundratals närvarouppdateringar, ackumuleras dessa allokeringar snabbt, vilket kan påverka prestanda och öka skräpinsamlingstrycket.

Om du vill mäta det här allokeringsbeteendet kan du prova att köra spårningsverktyget för .NET-objektallokering i Visual Studio. När du profilerar den aktuella implementeringen under konsertsimuleringen upptäcker du att den allokerar 134 GateAttendance objekt för att slutföra den relativt lilla simuleringen. Varje operatörsanrop skapar en ny instans som visar hur snabbt allokeringar kan ackumuleras i verkliga scenarier. Den här mätningen ger en konkret baslinje för att jämföra de prestandaförbättringar du uppnår med sammansatta tilldelningsoperatorer.

Implementera sammansatta tilldelningsoperatorer

C# 14 introducerar användardefinierade sammansatta tilldelningsoperatorer som aktiverar mutationer på plats i stället för att skapa nya instanser. Dessa operatorer är ett effektivare alternativ till det traditionella mönstret samtidigt som den välbekanta syntaxen för sammansatt tilldelning bibehålls.

Sammansatta tilldelningsoperatorer använder en ny syntax som deklarerar void returmetoder med nyckelordet operator . Lägg till följande operatorer i GateAttendance klassen:

public void operator +=(int value) => this.property += value;
public void operator ++() => this.property++;

De viktigaste skillnaderna från traditionella operatorer är:

  • Mutation: De ändrar den aktuella instansen direkt med .this
  • Inga nya instanser: Till skillnad från traditionella operatorer som returnerar nya objekt ändrar sammansatta operatorer befintliga.
  • Returtyp: Sammansatta tilldelningsoperatorer returnerar void, inte själva typen.

När kompilatorn stöter på sammansatta tilldelningsuttryck som a += b eller ++aföljer den den här matchningsordningen:

  1. Sök efter sammansatt tilldelningsoperator: Om typen definierar en användardefinierad sammansatt tilldelningsoperator (till exempel += eller ++), använder du den direkt.
  2. Återställning till traditionell expansion: Om det inte finns någon sammansatt operator expanderar du till den traditionella formen (a = a + b).

Det innebär att du kan implementera båda metoderna samtidigt. De sammansatta operatorerna har företräde när de är tillgängliga, men de traditionella operatorerna fungerar som reserv för scenarier där sammansatt tilldelning inte är lämplig.

Sammansatta tilldelningsoperatorer ger flera fördelar:

  • Minskade allokeringar: Ändra objekt på plats i stället för att skapa nya instanser.
  • Förbättrad prestanda: Eliminera tillfälligt skapande av objekt och minska skräpinsamlingsbelastningen.
  • Välbekant syntax: Använd samma +=++ syntax som utvecklare redan känner till.
  • Bakåtkompatibilitet: Traditionella operatorer fortsätter att fungera som reservlösningar.

De nya sammansatta tilldelningsoperatorerna visas i följande kod:

public void operator ++() => Count++;

public void operator +=(int partySize) => Count += partySize;

Anmärkning

Utvecklare som är bekanta med C++ kanske undrar varför endast en ++ eller -- en operatör krävs. Kompilatorn genererar koden för att använda antingen uttrycket före eller efter ändringen som returvärde. Den kompilatorgenererade koden utför tilldelningen med antingen det ursprungliga värdet eller det ändrade värdet baserat på om pre-increment (++x) eller post-increment (x++) anropades.

Analysera färdigt exempel

Nu när du har implementerat de sammansatta tilldelningsoperatorerna är det dags att mäta prestandaförbättringen. Om du vill mäta den dramatiska skillnaden i minnesallokeringar kör du spårningsverktyget för .NET-objektallokering igen på den uppdaterade koden.

När du profilerar programmet med de sammansatta tilldelningsoperatorerna aktiverade ser du en anmärkningsvärd minskning: endast 10 GateAttendance objekt allokeras under hela konsertsimuleringen, jämfört med de tidigare 134 allokeringarna. Den här uppdateringen representerar en minskning med 92% objektallokeringar!

De återstående 10 allokeringarna kommer från det första skapandet av GateAttendance instanserna för varje teatergrind (fyra huvudvåningsgrindar + två balkonggrindar = sex initiala instanser), samt några fler allokeringar från andra delar av simuleringen som inte använder de sammansatta operatorerna.

Den här allokeringsminskningen innebär verkliga prestandafördelar:

  • Minskat minnestryck: Mindre frekventa skräpinsamlingscykler.
  • Bättre cachelokalitet: Färre objektskapanden innebär mindre minnesfragmentering.
  • Förbättrad genomströmning: CPU-cykler sparade från allokerings- och insamlingsoverhead.
  • Skalbarhet: Fördelar multipliceras i scenarier med högre åtgärdsvolymer.

Prestandaförbättringen blir ännu viktigare i produktionsprogram där liknande mönster sker i mycket större skala – tänk dig att spåra miljontals transaktioner, uppdatera tusentals räknare eller bearbeta dataströmmar med hög frekvens.

Prova att identifiera andra möjligheter för sammansatta tilldelningsoperatorer i kodbasen. Leta efter mönster där du använder traditionella tilldelningsåtgärder som gates.MainFloorGates[1] = gates.MainFloorGates[1] + 4 och fundera på om de kan dra nytta av sammansatt tilldelningssyntax. Vissa av dessa åtgärder används += redan i simuleringskoden, men principen gäller för alla scenarion där du upprepade gånger ändrar objekt i stället för att skapa nya instanser.

Som ett sista experiment ändrar du GateAttendance typen från en record class till en record struct. Det är en annan optimering, och den fungerar i den här simuleringen eftersom struct har ett litet minnesfotavtryck. Att kopiera en GateAttendance struct är inte en dyr åtgärd. Trots detta uppnår du små förbättringar.