From 0ba12689b5b5e3ca09dfa615d1231007ac3f6096 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Fri, 12 Sep 2025 17:16:47 -0400 Subject: [PATCH 1/5] Outline and sample --- .gitignore | 1 + docs/csharp/toc.yml | 2 + .../compound-assignment-operators.md | 59 +++++ .../CompoundAssignment.csproj | 11 + .../CompoundAssignment/GateAttendance.cs | 24 ++ .../CompoundAssignment/GateAttendanceTests.cs | 248 ++++++++++++++++++ .../snippets/CompoundAssignment/Program.cs | 2 + 7 files changed, 347 insertions(+) create mode 100644 docs/csharp/whats-new/tutorials/compound-assignment-operators.md create mode 100644 docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/CompoundAssignment.csproj create mode 100644 docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/GateAttendance.cs create mode 100644 docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/GateAttendanceTests.cs create mode 100644 docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/Program.cs diff --git a/.gitignore b/.gitignore index fa3c66151b310..aebb12d48e08f 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ ehthumbs_vista.db [Bb]in/ [Oo]bj/ *.sln +*.slnx *.user # Ionide folder, used in F# for VSCode diff --git a/docs/csharp/toc.yml b/docs/csharp/toc.yml index a97f1a3196ae7..4192b1deb3902 100644 --- a/docs/csharp/toc.yml +++ b/docs/csharp/toc.yml @@ -174,6 +174,8 @@ items: href: whats-new/version-update-considerations.md - name: Tutorials items: + - name: Explore compound assignment + href: whats-new/tutorials/compound-assignment-operators.md - name: Explore primary constructors href: whats-new/tutorials/primary-constructors.md - name: Explore static interface members diff --git a/docs/csharp/whats-new/tutorials/compound-assignment-operators.md b/docs/csharp/whats-new/tutorials/compound-assignment-operators.md new file mode 100644 index 0000000000000..047574710aeab --- /dev/null +++ b/docs/csharp/whats-new/tutorials/compound-assignment-operators.md @@ -0,0 +1,59 @@ +--- +title: Explore instance compound assignment operators +description: "C# 14 enables instance compound assignment operators. These can provide better performance by minimizing allocations or copy operations. Learn how to create these operators." +author: billwagner +ms.author: wiwagn +ms.service: dotnet-csharp +ms.topic: tutorial +ms.date: 09/15/2025 + +#customer intent: As a C# developer, I want implement compound assignment operators so that my algorithms are more efficient. + +--- + +# Tutorial: Create compound assignment operators + + +[Introduce and explain the purpose of the article.] + + + +In this tutorial, you: + +> [!div class="checklist"] +> * Install prerequisites +> * Analyze the starting sample +> * Implement compound assignment operators +> * Analyze completed sample + +## Prerequisites + +- The .NET 10 preview SDK. Download it from the [.NET download site](https://dotnet.microsoft.com/download/dotnet/10.0). +- Visual Studio 2026 (preview). Download it from the [Visual Studio insiders page](https://visualstudio.microsoft.com/insiders/). + +## Analyze the starting sample + +- Run the app: understand what it does. +- Read the `GateAttendance` class closely. Note that the operators allocate new instances of objects. +- (optional) Run the Visual Studio performance profiler to count allocations (134 `GateAttendance` allocations) + +## Implement compound assignment operators + +## Analyze finished sample + +- (Optional) Run the profiler again. Now, only 10 instance of GateAttendance objects are allocated. +- Try and spot any additional allocations by replacing `+` operations with `+=` where possible. +- Discuss other options, like `struct` types. Point out that compound assignment helps here by avoiding copy operations + +## Related content + +- What's new. +- Operators in language reference +- [Analyze memory usage by using the .NET Object Allocation tool - Visual Studio](~/visualstudio/profiling/dotnet-alloc-tool.md) diff --git a/docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/CompoundAssignment.csproj b/docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/CompoundAssignment.csproj new file mode 100644 index 0000000000000..b9dbadc899514 --- /dev/null +++ b/docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/CompoundAssignment.csproj @@ -0,0 +1,11 @@ + + + + Exe + net10.0 + enable + enable + preview + + + diff --git a/docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/GateAttendance.cs b/docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/GateAttendance.cs new file mode 100644 index 0000000000000..98f57eca1babd --- /dev/null +++ b/docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/GateAttendance.cs @@ -0,0 +1,24 @@ +public record class GateAttendance(string GateId) +{ + private int count = 0; + + public int Count => count; + + 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; + } + + // New style: + public void operator ++() => this.count++; + + public void operator +=(int partySize) => this.count += partySize; +} + diff --git a/docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/GateAttendanceTests.cs b/docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/GateAttendanceTests.cs new file mode 100644 index 0000000000000..31a9ccfce8460 --- /dev/null +++ b/docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/GateAttendanceTests.cs @@ -0,0 +1,248 @@ +public static class TheaterConcertSimulationCompound +{ + private const int MAX_ATTENDANCE = 1000; + private const int EXPECTED_SALES = 850; + + public static void SimulateConcertAttendance() + { + Console.WriteLine("=== Royal Theater Concert Attendance Simulation ==="); + Console.WriteLine($"Venue Capacity: {MAX_ATTENDANCE} | Expected Sales: {EXPECTED_SALES}"); + Console.WriteLine("Concert: 'Symphony Under the Stars' - 7:30 PM\n"); + + // Initialize theater gates + var theaterGates = new TheaterGates(); + + Console.WriteLine("--- Gate Status Before Concert ---"); + PrintTheaterStatus(theaterGates); + + // Simulate pre-concert arrival patterns + SimulatePreConcertArrivals(theaterGates); + + Console.WriteLine("\n--- Gate Status After Pre-Concert Arrivals (6:30 PM) ---"); + PrintTheaterStatus(theaterGates); + + // Simulate main arrival rush + SimulateMainArrivalRush(theaterGates); + + Console.WriteLine("\n--- Gate Status After Main Rush (7:15 PM) ---"); + PrintTheaterStatus(theaterGates); + + // Simulate late arrivals + SimulateLateArrivals(theaterGates); + + Console.WriteLine("\n--- Final Attendance Report (7:30 PM - Concert Start) ---"); + GenerateFinalReport(theaterGates); + } + + private static void SimulatePreConcertArrivals(TheaterGates gates) + { + Console.WriteLine("--- Pre-Concert Arrivals (6:00 PM - 6:30 PM) ---"); + Console.WriteLine("Early patrons and season ticket holders arriving...\n"); + + // Early arrivals - mainly main floor, some balcony + // Main floor gates - moderate traffic + ++gates.MainFloorGates[0]; // Single patron + gates.MainFloorGates[0] += 2; // Couple + gates.MainFloorGates[0] += 6; // Corporate early group + gates.MainFloorGates[1] = gates.MainFloorGates[1] + 4; // Family + gates.MainFloorGates[1] += 8; // Season ticket holders + ++gates.MainFloorGates[2]; // Single patron + gates.MainFloorGates[2] += 5; // Early dinner group + gates.MainFloorGates[3] += 3; // Small group + gates.MainFloorGates[3] += 7; // VIP early access + ++gates.MainFloorGates[3]; // Individual VIP + + // Balcony gates - lighter but increased traffic + ++gates.BalconyGates[0]; // Single patron + gates.BalconyGates[0] += 4; // Early bird group + gates.BalconyGates[1] += 2; // Couple + gates.BalconyGates[1] += 6; // Student group with early tickets + ++gates.BalconyGates[1]; // Individual student + + Console.WriteLine("Early arrivals processed through all gates."); + } + + private static void SimulateMainArrivalRush(TheaterGates gates) + { + Console.WriteLine("\n--- Main Arrival Rush (6:30 PM - 7:15 PM) ---"); + Console.WriteLine("Peak arrival time - all gates busy...\n"); + + var random = new Random(); + + // Heavy traffic through main floor gates + for (int i = 0; i < 3; i++) + { + // 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 + + // Gate 3 - moderate traffic (target: ~70-95 people) + ++gates.MainFloorGates[2]; // Individual + gates.MainFloorGates[2] += random.Next(4, 8); // Small group + gates.MainFloorGates[2] += random.Next(15, 22); // Community/organization group + gates.MainFloorGates[2] += random.Next(10, 16); // Club/society members + ++gates.MainFloorGates[2]; // Solo attendee + gates.MainFloorGates[2] += random.Next(6, 12); // Friends/social group + + // Gate 4 - lighter but steady (target: ~60-85 people) + gates.MainFloorGates[3] += random.Next(3, 6); // Family group + gates.MainFloorGates[3] += random.Next(8, 15); // Celebration/event group + ++gates.MainFloorGates[3]; // Individual attendee + gates.MainFloorGates[3] += random.Next(5, 10); // Couples/pairs + + // Balcony gates - steady increased flow + // Left balcony gate (target: ~80-100 people) + ++gates.BalconyGates[0]; // Single patron + gates.BalconyGates[0] += random.Next(5, 10); // Small group + gates.BalconyGates[0] += random.Next(15, 25); // Student/educational group + gates.BalconyGates[0] += random.Next(10, 18); // Budget-conscious attendees + ++gates.BalconyGates[0]; // Individual + gates.BalconyGates[0] += random.Next(6, 12); // Senior/community group + + // Right balcony gate (target: ~70-95 people) + gates.BalconyGates[1] += random.Next(4, 8); // Small group + ++gates.BalconyGates[1]; // Individual + gates.BalconyGates[1] += random.Next(16, 24); // Academic/institutional group + gates.BalconyGates[1] += random.Next(10, 16); // Community organization + gates.BalconyGates[1] += random.Next(4, 8); // Young professionals/friends + ++gates.BalconyGates[1]; // Solo patron + } + + Console.WriteLine("Peak rush period completed - all gates processed heavy traffic."); + } + + private static void SimulateLateArrivals(TheaterGates gates) + { + Console.WriteLine("\n--- Late Arrivals (7:15 PM - 7:30 PM) ---"); + Console.WriteLine("Final patrons arriving before curtain...\n"); + + var random = new Random(); + + // Light but varied traffic as concert approaches + // Main floor gates - scattered late arrivals (1-8 people per gate) + ++gates.MainFloorGates[0]; // Last-minute arrival + gates.MainFloorGates[0] += random.Next(2, 6); // Delayed group + + gates.MainFloorGates[1] += random.Next(1, 4); // Rushing small group + gates.MainFloorGates[1] += random.Next(2, 7); // Traffic/parking delayed group + + ++gates.MainFloorGates[2]; // Single late arrival + gates.MainFloorGates[2] += random.Next(1, 5); // Delayed couple/small group + + gates.MainFloorGates[3] += random.Next(1, 4); // Late couple + gates.MainFloorGates[3] += random.Next(2, 6); // Last-minute purchasers + ++gates.MainFloorGates[3]; // Solo rush arrival + + // Balcony gates - lighter late traffic (1-5 people per gate) + ++gates.BalconyGates[0]; // Late balcony patron + gates.BalconyGates[0] += random.Next(1, 4); // Delayed small group + + gates.BalconyGates[1] += random.Next(1, 3); // Final arrivals + gates.BalconyGates[1] += random.Next(2, 5); // Work-delayed group + ++gates.BalconyGates[1]; // Individual latecomer + + Console.WriteLine("Final arrivals processed - concert about to begin!"); + } + + private static void PrintTheaterStatus(TheaterGates gates) + { + Console.WriteLine("Main Floor Gates:"); + for (int i = 0; i < gates.MainFloorGates.Length; i++) + { + var gate = gates.MainFloorGates[i]; + Console.WriteLine($" {gate.GateId}: {gate.Count,3} attendees"); + } + + var mainFloorTotal = gates.MainFloorGates.Sum(g => g.Count); + Console.WriteLine($" Main Floor Subtotal: {mainFloorTotal,3} attendees"); + + Console.WriteLine("\nBalcony Gates:"); + for (int i = 0; i < gates.BalconyGates.Length; i++) + { + var gate = gates.BalconyGates[i]; + Console.WriteLine($" {gate.GateId}: {gate.Count,3} attendees"); + } + + var balconyTotal = gates.BalconyGates.Sum(g => g.Count); + Console.WriteLine($" Balcony Subtotal: {balconyTotal,3} attendees"); + + var totalAttendance = mainFloorTotal + balconyTotal; + Console.WriteLine($"\nTotal Current Attendance: {totalAttendance,3} / {MAX_ATTENDANCE}"); + } + + private static void GenerateFinalReport(TheaterGates gates) + { + var mainFloorTotal = gates.MainFloorGates.Sum(g => g.Count); + var balconyTotal = gates.BalconyGates.Sum(g => g.Count); + var totalAttendance = mainFloorTotal + balconyTotal; + + PrintTheaterStatus(gates); + + Console.WriteLine("\n" + new string('=', 50)); + Console.WriteLine("FINAL CONCERT ATTENDANCE REPORT"); + Console.WriteLine(new string('=', 50)); + + Console.WriteLine($"Expected Sales: {EXPECTED_SALES,3}"); + Console.WriteLine($"Actual Attendance: {totalAttendance,3}"); + Console.WriteLine($"Venue Capacity: {MAX_ATTENDANCE,3}"); + + var attendanceRate = (double)totalAttendance / EXPECTED_SALES * 100; + var capacityUtilization = (double)totalAttendance / MAX_ATTENDANCE * 100; + + Console.WriteLine($"Attendance Rate: {attendanceRate,5:F1}% of expected"); + Console.WriteLine($"Capacity Utilization: {capacityUtilization,5:F1}% of maximum"); + + // Gate distribution analysis + Console.WriteLine($"\nGate Distribution:"); + Console.WriteLine($"Main Floor: {mainFloorTotal,3} ({(double)mainFloorTotal/totalAttendance*100:F1}%)"); + Console.WriteLine($"Balcony: {balconyTotal,3} ({(double)balconyTotal/totalAttendance*100:F1}%)"); + + // Performance indicators using switch expression + var performanceMessage = totalAttendance switch + { + var t when t >= EXPECTED_SALES * 0.95 => "\n✅ Excellent attendance! Concert exceeded expectations.", + var t when t >= EXPECTED_SALES * 0.85 => "\n✅ Good attendance! Concert met expectations.", + var t when t >= EXPECTED_SALES * 0.70 => "\n⚠️ Moderate attendance. Consider marketing review.", + _ => "\n❌ Low attendance. Significant marketing review needed." + }; + + Console.WriteLine(performanceMessage); + + Console.WriteLine("\nConcert begins! 🎼"); + } +} + +public struct TheaterGates +{ + // Main floor gates (4 gates) - initialized with default gate instances + public GateAttendance[] MainFloorGates { get; set; } = + [ + new GateAttendance("Main-Floor-Gate-1"), + new GateAttendance("Main-Floor-Gate-2"), + new GateAttendance("Main-Floor-Gate-3"), + new GateAttendance("Main-Floor-Gate-4") + ]; + + // Balcony gates (2 gates) - initialized with default gate instances + public GateAttendance[] BalconyGates { get; set; } = + [ + new GateAttendance("Balcony-Gate-Left"), + new GateAttendance("Balcony-Gate-Right") + ]; + + // Explicit constructor required when using field initializers in structs + public TheaterGates() + { + } +} \ No newline at end of file diff --git a/docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/Program.cs b/docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/Program.cs new file mode 100644 index 0000000000000..e304c657aaedb --- /dev/null +++ b/docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/Program.cs @@ -0,0 +1,2 @@ +TheaterConcertSimulationCompound.SimulateConcertAttendance(); + From 72d1172774032d9a194734e91c5fed65fbf58465 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Mon, 15 Sep 2025 15:59:34 -0400 Subject: [PATCH 2/5] First draft completed. Write the first draft, including extracted samples. --- .../compound-assignment-operators.md | 139 +++++++++++++++--- .../CompoundAssignment/GateAttendance.cs | 36 ++++- .../CompoundAssignment/GateAttendanceTests.cs | 10 +- .../snippets/CompoundAssignment/Program.cs | 2 +- 4 files changed, 155 insertions(+), 32 deletions(-) diff --git a/docs/csharp/whats-new/tutorials/compound-assignment-operators.md b/docs/csharp/whats-new/tutorials/compound-assignment-operators.md index 047574710aeab..9acdf139a1292 100644 --- a/docs/csharp/whats-new/tutorials/compound-assignment-operators.md +++ b/docs/csharp/whats-new/tutorials/compound-assignment-operators.md @@ -1,29 +1,34 @@ --- -title: Explore instance compound assignment operators -description: "C# 14 enables instance compound assignment operators. These can provide better performance by minimizing allocations or copy operations. Learn how to create these operators." +title: Explore user defined instance compound assignment operators +description: "C# 14 enables user defined instance compound assignment operators. These can provide better performance by minimizing allocations or copy operations. Learn how to create these operators." author: billwagner ms.author: wiwagn ms.service: dotnet-csharp ms.topic: tutorial ms.date: 09/15/2025 -#customer intent: As a C# developer, I want implement compound assignment operators so that my algorithms are more efficient. +#customer intent: As a C# developer, I want implement user defined instance compound assignment operators so that my algorithms are more efficient. --- # Tutorial: Create compound assignment operators +C#14.0 adds *user defined compound assignment operators* that enable mutating a data structure in place, rather than creating a new instance. In previous versions of C#, the expression: -[Introduce and explain the purpose of the article.] +```csharp +a += b; +``` - +Depending on the type of `a`, this leads to excessive allocations to create new instances, or copying the values of several properties to set values on the copy. Adding a user defined operator for `+=` indicates a type can do a better job by updating the destination object in place. + +C# still supports the existing expansion, but it uses it only when a compound user defined operator isn't available. In this tutorial, you: @@ -40,20 +45,116 @@ In this tutorial, you: ## Analyze the starting sample -- Run the app: understand what it does. -- Read the `GateAttendance` class closely. Note that the operators allocate new instances of objects. -- (optional) Run the Visual Studio performance profiler to count allocations (134 `GateAttendance` allocations) +Run the app to understand what it does. The sample application simulates concert attendance tracking at a theater venue. The simulation models realistic arrival patterns throughout the evening, from early patrons to the main rush before showtime. This simulation demonstrates the performance impact of object allocations when using traditional operators versus the efficiency gains possible with user-defined compound assignment operators. + +The app tracks attendance through multiple theater gates (main floor and balcony sections) as concert-goers arrive. Each gate maintains a count of attendees using a `GateAttendance` record. Throughout the simulation, the code frequently updates these counts using increment (`++`) and addition (`+=`) operations. The following code shows a portion of that simulation: + +:::code language="csharp" source="./snippets/CompoundAssignment/GateAttendanceTests.cs" id="Simulation"::: + +With traditional operators, each operation creates a new `GateAttendance` instance due to the immutable nature of records, leading to significant memory allocations. + +When you run the simulation, you'll see detailed output showing: + +- Gate-by-gate attendance numbers during different arrival periods. +- Total attendance tracking across all gates. +- A final comprehensive report with attendance statistics. + +You can see a portion of the output: + +```txt +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! +``` + +This realistic scenario provides an excellent test case for measuring the performance benefits of compound assignment operators, as the frequent count updates mirror common patterns in real applications where objects are repeatedly modified. + +Examine the starter `GateAttendance` record class: + +:::code language="csharp" source="./snippets/CompoundAssignment/GateAttendance.cs" id="GateAttendanceStarter"::: + +The `InitialImplementation.GateAttendance` record demonstrates the traditional approach to operator overloading in C#. Notice how both the increment operator (`++`) and addition operator (`+`) create entirely new instances of `GateAttendance` using the `with` expression. Each time you write `gate++` or `gate += partySize`, the operators allocate a new record instance with the updated `Count` value, then return that new instance. While this approach maintains immutability and thread safety, it comes at the cost of frequent memory allocations. In scenarios with many operations—like our concert simulation with hundreds of attendance updates—these allocations accumulate quickly, potentially impacting performance and increasing garbage collection pressure. + +To see this allocation behavior in action, try running the [.NET Object Allocation tracking tool](~/visualstudio/profiling/dotnet-alloc-tool) in Visual Studio. When you profile the current implementation during the concert simulation, you'll discover that it allocates 134 `GateAttendance` objects to complete the relatively small simulation. Each operator call creates a new instance, demonstrating how quickly allocations can accumulate in real-world scenarios. This measurement provides a concrete baseline for comparing the performance improvements you'll achieve with compound assignment operators. ## Implement compound assignment operators +C# 14 introduces user-defined compound assignment operators that enable in-place mutations instead of creating new instances. These operators provide a more efficient alternative to the traditional pattern while maintaining the familiar compound assignment syntax. + +Compound assignment operators use a new syntax that declares `void` return methods with the `operator` keyword: + +```csharp +public void operator +=(int value) => this.property += value; +public void operator ++() => this.property++; +``` + +The key differences from traditional operators are: + +- **Return type**: Compound assignment operators return `void`, not the type itself +- **Mutation**: They modify the current instance directly using `this` +- **No new instances**: Unlike traditional operators that return new objects, compound operators modify existing ones + +When the compiler encounters compound assignment expressions like `a += b` or `++a`, it follows this resolution order: + +1. **Check for compound assignment operator**: If the type defines a user-defined compound assignment operator (`+=`, `++`, etc.), use it directly +2. **Fallback to traditional expansion**: If no compound operator exists, expand to the traditional form (`a = a + b`) + +This means you can implement both approaches simultaneously. The compound operators take precedence when available, but the traditional operators serve as fallbacks for scenarios where compound assignment isn't suitable. + +Compound assignment operators provide several advantages: + +- **Reduced allocations**: Modify objects in-place instead of creating new instances +- **Improved performance**: Eliminate temporary object creation and reduce garbage collection pressure +- **Familiar syntax**: Use the same `+=`, `++` syntax developers already know +- **Backward compatibility**: Traditional operators continue to work as fallbacks + +The new compound assignment operators are shown in the following code: + +:::code language="csharp" source="./snippets/CompoundAssignment/GateAttendance.cs" id="CompoundAssignmentOperators"::: + ## Analyze finished sample -- (Optional) Run the profiler again. Now, only 10 instance of GateAttendance objects are allocated. -- Try and spot any additional allocations by replacing `+` operations with `+=` where possible. -- Discuss other options, like `struct` types. Point out that compound assignment helps here by avoiding copy operations +Now that you've implemented the compound assignment operators, it's time to measure the performance improvement. Run the [.NET Object Allocation tracking tool](~/visualstudio/profiling/dotnet-alloc-tool) again on the updated code to see the dramatic difference in memory allocations. + +When you profile the application with the compound assignment operators enabled, you'll observe a remarkable reduction: only **10 `GateAttendance` objects** are allocated during the entire concert simulation, compared to the previous 134 allocations. This represents a 92% reduction in object allocations! + +The remaining 10 allocations come from the initial creation of the `GateAttendance` instances for each theater gate (4 main floor gates + 2 balcony gates = 6 initial instances), plus a few additional allocations from other parts of the simulation that don't use the compound operators. + +This allocation reduction translates to real performance benefits: + +- **Reduced memory pressure**: Less frequent garbage collection cycles +- **Better cache locality**: Fewer object creations mean less memory fragmentation +- **Improved throughput**: CPU cycles saved from allocation and collection overhead +- **Scalability**: Benefits multiply in scenarios with higher operation volumes + +The performance improvement becomes even more significant in production applications where similar patterns occur at much larger scales—imagine tracking millions of transactions, updating thousands of counters, or processing high-frequency data streams. + +Try identifying other opportunities for compound assignment operators in the codebase. Look for patterns where you see traditional assignment operations like `gates.MainFloorGates[1] = gates.MainFloorGates[1] + 4` and consider whether they could benefit from compound assignment syntax. While some of these are already using `+=` in the simulation code, the principle applies to any scenario where you're repeatedly modifying objects rather than creating new instances. + +As a final experiment, change the `GateAttendance` type from a `record class` to a `record struct`. It's a different optimization, and it works in this simulation because the struct has a small memory footprint. Copying a `GateAttendance` struct isn't an expensive operation. Even so, you'll see small improvements. ## Related content -- What's new. -- Operators in language reference -- [Analyze memory usage by using the .NET Object Allocation tool - Visual Studio](~/visualstudio/profiling/dotnet-alloc-tool.md) +- [What's new in C# 14](../csharp-14.md) +- [Operator overloading - define unary, arithmetic, equality and comparison operators](../../language-reference/operators/operator-overloading.md) +- [Analyze memory usage by using the .NET Object Allocation tool - Visual Studio](~/visualstudio/profiling/dotnet-alloc-tool) diff --git a/docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/GateAttendance.cs b/docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/GateAttendance.cs index 98f57eca1babd..b113ff8e832a0 100644 --- a/docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/GateAttendance.cs +++ b/docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/GateAttendance.cs @@ -1,24 +1,44 @@ public record class GateAttendance(string GateId) { - private int count = 0; - - public int Count => count; + public int Count { get; private set; } public static GateAttendance operator ++(GateAttendance gate) { - GateAttendance updateGate = gate with { count = gate.count + 1 }; + 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 }; + GateAttendance updateGate = gate with { Count = gate.Count + partySize }; return updateGate; } - // New style: - public void operator ++() => this.count++; + // + public void operator ++() => Count++; - public void operator +=(int partySize) => this.count += partySize; + public void operator +=(int partySize) => Count += partySize; + // } +namespace InitialImplementation +{ + // + 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; + } + } + // +} diff --git a/docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/GateAttendanceTests.cs b/docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/GateAttendanceTests.cs index 31a9ccfce8460..07933b1d99c0c 100644 --- a/docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/GateAttendanceTests.cs +++ b/docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/GateAttendanceTests.cs @@ -1,4 +1,4 @@ -public static class TheaterConcertSimulationCompound +public static class TheaterConcertSimulation { private const int MAX_ATTENDANCE = 1000; private const int EXPECTED_SALES = 850; @@ -72,6 +72,7 @@ private static void SimulateMainArrivalRush(TheaterGates gates) // Heavy traffic through main floor gates for (int i = 0; i < 3; i++) { + // // Gate 1 - busiest entrance (target: ~100-130 people) gates.MainFloorGates[0] += random.Next(8, 15); // Corporate group ++gates.MainFloorGates[0]; // Single patron @@ -86,7 +87,8 @@ private static void SimulateMainArrivalRush(TheaterGates gates) 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 - + // + // Gate 3 - moderate traffic (target: ~70-95 people) ++gates.MainFloorGates[2]; // Individual gates.MainFloorGates[2] += random.Next(4, 8); // Small group @@ -226,7 +228,7 @@ private static void GenerateFinalReport(TheaterGates gates) public struct TheaterGates { // Main floor gates (4 gates) - initialized with default gate instances - public GateAttendance[] MainFloorGates { get; set; } = + public GateAttendance[] MainFloorGates { get; } = [ new GateAttendance("Main-Floor-Gate-1"), new GateAttendance("Main-Floor-Gate-2"), @@ -235,7 +237,7 @@ public struct TheaterGates ]; // Balcony gates (2 gates) - initialized with default gate instances - public GateAttendance[] BalconyGates { get; set; } = + public GateAttendance[] BalconyGates { get; } = [ new GateAttendance("Balcony-Gate-Left"), new GateAttendance("Balcony-Gate-Right") diff --git a/docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/Program.cs b/docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/Program.cs index e304c657aaedb..1ed24fe90c33b 100644 --- a/docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/Program.cs +++ b/docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/Program.cs @@ -1,2 +1,2 @@ -TheaterConcertSimulationCompound.SimulateConcertAttendance(); +TheaterConcertSimulation.SimulateConcertAttendance(); From f249f0d5592404a9bf72b26d4bc941c814c4949f Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Mon, 15 Sep 2025 16:32:11 -0400 Subject: [PATCH 3/5] proofread and fix warnings. --- .../compound-assignment-operators.md | 36 +++++++++---------- .../CompoundAssignment/GateAttendance.cs | 28 +++++++-------- 2 files changed, 31 insertions(+), 33 deletions(-) diff --git a/docs/csharp/whats-new/tutorials/compound-assignment-operators.md b/docs/csharp/whats-new/tutorials/compound-assignment-operators.md index 9acdf139a1292..955de7263a12a 100644 --- a/docs/csharp/whats-new/tutorials/compound-assignment-operators.md +++ b/docs/csharp/whats-new/tutorials/compound-assignment-operators.md @@ -1,16 +1,14 @@ --- title: Explore user defined instance compound assignment operators -description: "C# 14 enables user defined instance compound assignment operators. These can provide better performance by minimizing allocations or copy operations. Learn how to create these operators." +description: "C# 14 enables user defined instance compound assignment operators. These operators can provide better performance by minimizing allocations or copy operations. Learn how to create these operators." author: billwagner ms.author: wiwagn ms.service: dotnet-csharp ms.topic: tutorial ms.date: 09/15/2025 - +ai-usage:ai-assisted #customer intent: As a C# developer, I want implement user defined instance compound assignment operators so that my algorithms are more efficient. - --- - # Tutorial: Create compound assignment operators C#14.0 adds *user defined compound assignment operators* that enable mutating a data structure in place, rather than creating a new instance. In previous versions of C#, the expression: @@ -19,16 +17,16 @@ C#14.0 adds *user defined compound assignment operators* that enable mutating a a += b; ``` -was expanded to the following code: +Was expanded to the following code: ```csharp var tmp = a + b; a = tmp; ``` -Depending on the type of `a`, this leads to excessive allocations to create new instances, or copying the values of several properties to set values on the copy. Adding a user defined operator for `+=` indicates a type can do a better job by updating the destination object in place. +Depending on the type of `a`, this expansion leads to excessive allocations to create new instances, or copying the values of several properties to set values on the copy. Adding a user defined operator for `+=` indicates a type can do a better job by updating the destination object in place. -C# still supports the existing expansion, but it uses it only when a compound user defined operator isn't available. +C# supports the existing expansion, but it uses it only when a compound user defined operator isn't available. In this tutorial, you: @@ -45,7 +43,7 @@ In this tutorial, you: ## Analyze the starting sample -Run the app to understand what it does. The sample application simulates concert attendance tracking at a theater venue. The simulation models realistic arrival patterns throughout the evening, from early patrons to the main rush before showtime. This simulation demonstrates the performance impact of object allocations when using traditional operators versus the efficiency gains possible with user-defined compound assignment operators. +Run the starter application. You can get it from [the `dotnet/docs` GitHub repository](https://github.com/dotnet/docs/blob/main/docs/csharp/whats-new/tutorials/snippets/CompoundAssignment). The sample application simulates concert attendance tracking at a theater venue. The simulation models realistic arrival patterns throughout the evening, from early attendees to the main rush before showtime. This simulation demonstrates the object allocations when using traditional operators versus the efficiency gains possible with user-defined compound assignment operators. The app tracks attendance through multiple theater gates (main floor and balcony sections) as concert-goers arrive. Each gate maintains a count of attendees using a `GateAttendance` record. Throughout the simulation, the code frequently updates these counts using increment (`++`) and addition (`+=`) operations. The following code shows a portion of that simulation: @@ -53,7 +51,7 @@ The app tracks attendance through multiple theater gates (main floor and balcony With traditional operators, each operation creates a new `GateAttendance` instance due to the immutable nature of records, leading to significant memory allocations. -When you run the simulation, you'll see detailed output showing: +When you run the simulation, you see detailed output showing: - Gate-by-gate attendance numbers during different arrival periods. - Total attendance tracking across all gates. @@ -95,13 +93,13 @@ Examine the starter `GateAttendance` record class: The `InitialImplementation.GateAttendance` record demonstrates the traditional approach to operator overloading in C#. Notice how both the increment operator (`++`) and addition operator (`+`) create entirely new instances of `GateAttendance` using the `with` expression. Each time you write `gate++` or `gate += partySize`, the operators allocate a new record instance with the updated `Count` value, then return that new instance. While this approach maintains immutability and thread safety, it comes at the cost of frequent memory allocations. In scenarios with many operations—like our concert simulation with hundreds of attendance updates—these allocations accumulate quickly, potentially impacting performance and increasing garbage collection pressure. -To see this allocation behavior in action, try running the [.NET Object Allocation tracking tool](~/visualstudio/profiling/dotnet-alloc-tool) in Visual Studio. When you profile the current implementation during the concert simulation, you'll discover that it allocates 134 `GateAttendance` objects to complete the relatively small simulation. Each operator call creates a new instance, demonstrating how quickly allocations can accumulate in real-world scenarios. This measurement provides a concrete baseline for comparing the performance improvements you'll achieve with compound assignment operators. +To see this allocation behavior in action, try running the [.NET Object Allocation tracking tool](~/visualstudio/profiling/dotnet-alloc-tool) in Visual Studio. When you profile the current implementation during the concert simulation, you discover that it allocates 134 `GateAttendance` objects to complete the relatively small simulation. Each operator call creates a new instance, demonstrating how quickly allocations can accumulate in real-world scenarios. This measurement provides a concrete baseline for comparing the performance improvements you achieve with compound assignment operators. ## Implement compound assignment operators C# 14 introduces user-defined compound assignment operators that enable in-place mutations instead of creating new instances. These operators provide a more efficient alternative to the traditional pattern while maintaining the familiar compound assignment syntax. -Compound assignment operators use a new syntax that declares `void` return methods with the `operator` keyword: +Compound assignment operators use a new syntax that declares `void` return methods with the `operator` keyword. Add the following operators to the `GateAttendance` class: ```csharp public void operator +=(int value) => this.property += value; @@ -110,9 +108,9 @@ public void operator ++() => this.property++; The key differences from traditional operators are: -- **Return type**: Compound assignment operators return `void`, not the type itself - **Mutation**: They modify the current instance directly using `this` - **No new instances**: Unlike traditional operators that return new objects, compound operators modify existing ones +- **Return type**: Compound assignment operators return `void`, not the type itself When the compiler encounters compound assignment expressions like `a += b` or `++a`, it follows this resolution order: @@ -134,11 +132,11 @@ The new compound assignment operators are shown in the following code: ## Analyze finished sample -Now that you've implemented the compound assignment operators, it's time to measure the performance improvement. Run the [.NET Object Allocation tracking tool](~/visualstudio/profiling/dotnet-alloc-tool) again on the updated code to see the dramatic difference in memory allocations. +Now that you implemented the compound assignment operators, it's time to measure the performance improvement. To see the dramatic difference in memory allocations, run the [.NET Object Allocation tracking tool](/visualstudio/profiling/dotnet-alloc-tool) again on the updated code. -When you profile the application with the compound assignment operators enabled, you'll observe a remarkable reduction: only **10 `GateAttendance` objects** are allocated during the entire concert simulation, compared to the previous 134 allocations. This represents a 92% reduction in object allocations! +When you profile the application with the compound assignment operators enabled, you observe a remarkable reduction: only **10 `GateAttendance` objects** are allocated during the entire concert simulation, compared to the previous 134 allocations. This update represents a 92% reduction in object allocations! -The remaining 10 allocations come from the initial creation of the `GateAttendance` instances for each theater gate (4 main floor gates + 2 balcony gates = 6 initial instances), plus a few additional allocations from other parts of the simulation that don't use the compound operators. +The remaining 10 allocations come from the initial creation of the `GateAttendance` instances for each theater gate (four main floor gates + two balcony gates = six initial instances), plus a few more allocations from other parts of the simulation that don't use the compound operators. This allocation reduction translates to real performance benefits: @@ -149,12 +147,12 @@ This allocation reduction translates to real performance benefits: The performance improvement becomes even more significant in production applications where similar patterns occur at much larger scales—imagine tracking millions of transactions, updating thousands of counters, or processing high-frequency data streams. -Try identifying other opportunities for compound assignment operators in the codebase. Look for patterns where you see traditional assignment operations like `gates.MainFloorGates[1] = gates.MainFloorGates[1] + 4` and consider whether they could benefit from compound assignment syntax. While some of these are already using `+=` in the simulation code, the principle applies to any scenario where you're repeatedly modifying objects rather than creating new instances. +Try identifying other opportunities for compound assignment operators in the codebase. Look for patterns where you see traditional assignment operations like `gates.MainFloorGates[1] = gates.MainFloorGates[1] + 4` and consider whether they could benefit from compound assignment syntax. While some of these operations are already using `+=` in the simulation code, the principle applies to any scenario where you're repeatedly modifying objects rather than creating new instances. -As a final experiment, change the `GateAttendance` type from a `record class` to a `record struct`. It's a different optimization, and it works in this simulation because the struct has a small memory footprint. Copying a `GateAttendance` struct isn't an expensive operation. Even so, you'll see small improvements. +As a final experiment, change the `GateAttendance` type from a `record class` to a `record struct`. It's a different optimization, and it works in this simulation because the struct has a small memory footprint. Copying a `GateAttendance` struct isn't an expensive operation. Even so, you see small improvements. ## Related content - [What's new in C# 14](../csharp-14.md) -- [Operator overloading - define unary, arithmetic, equality and comparison operators](../../language-reference/operators/operator-overloading.md) -- [Analyze memory usage by using the .NET Object Allocation tool - Visual Studio](~/visualstudio/profiling/dotnet-alloc-tool) +- [Operator overloading - define unary, arithmetic, equality, and comparison operators](../../language-reference/operators/operator-overloading.md) +- [Analyze memory usage by using the .NET Object Allocation tool - Visual Studio](/visualstudio/profiling/dotnet-alloc-tool) diff --git a/docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/GateAttendance.cs b/docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/GateAttendance.cs index b113ff8e832a0..c6490b5773d20 100644 --- a/docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/GateAttendance.cs +++ b/docs/csharp/whats-new/tutorials/snippets/CompoundAssignment/GateAttendance.cs @@ -1,6 +1,7 @@ -public record class GateAttendance(string GateId) +// +public record class GateAttendance(string GateId) { - public int Count { get; private set; } + public int Count { get; init; } public static GateAttendance operator ++(GateAttendance gate) { @@ -13,20 +14,14 @@ GateAttendance updateGate = gate with { Count = gate.Count + partySize }; return updateGate; } +} +// - // - public void operator ++() => Count++; - - public void operator +=(int partySize) => Count += partySize; - // -} - -namespace InitialImplementation +namespace FinalImplementation { - // public record class GateAttendance(string GateId) { - public int Count { get; init; } + public int Count { get; private set; } public static GateAttendance operator ++(GateAttendance gate) { @@ -39,6 +34,11 @@ public record class GateAttendance(string GateId) GateAttendance updateGate = gate with { Count = gate.Count + partySize }; return updateGate; } - } - // + + // + public void operator ++() => Count++; + + public void operator +=(int partySize) => Count += partySize; + // + } } From f5cfcb0b53cf933576eb87887b6babd30ac04828 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Mon, 15 Sep 2025 16:37:00 -0400 Subject: [PATCH 4/5] build warnings --- .../whats-new/tutorials/compound-assignment-operators.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/csharp/whats-new/tutorials/compound-assignment-operators.md b/docs/csharp/whats-new/tutorials/compound-assignment-operators.md index 955de7263a12a..832d9f1a71d2c 100644 --- a/docs/csharp/whats-new/tutorials/compound-assignment-operators.md +++ b/docs/csharp/whats-new/tutorials/compound-assignment-operators.md @@ -6,7 +6,7 @@ ms.author: wiwagn ms.service: dotnet-csharp ms.topic: tutorial ms.date: 09/15/2025 -ai-usage:ai-assisted +ai-usage: ai-assisted #customer intent: As a C# developer, I want implement user defined instance compound assignment operators so that my algorithms are more efficient. --- # Tutorial: Create compound assignment operators @@ -31,6 +31,7 @@ C# supports the existing expansion, but it uses it only when a compound user def In this tutorial, you: > [!div class="checklist"] +> > * Install prerequisites > * Analyze the starting sample > * Implement compound assignment operators From 4a7689d8db48cad031aad3eaabc8f8f81899f867 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Mon, 15 Sep 2025 16:45:48 -0400 Subject: [PATCH 5/5] warnings, again --- .../whats-new/tutorials/compound-assignment-operators.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/csharp/whats-new/tutorials/compound-assignment-operators.md b/docs/csharp/whats-new/tutorials/compound-assignment-operators.md index 832d9f1a71d2c..fa31fcac696f4 100644 --- a/docs/csharp/whats-new/tutorials/compound-assignment-operators.md +++ b/docs/csharp/whats-new/tutorials/compound-assignment-operators.md @@ -31,7 +31,7 @@ C# supports the existing expansion, but it uses it only when a compound user def In this tutorial, you: > [!div class="checklist"] -> +> > * Install prerequisites > * Analyze the starting sample > * Implement compound assignment operators @@ -94,7 +94,7 @@ Examine the starter `GateAttendance` record class: The `InitialImplementation.GateAttendance` record demonstrates the traditional approach to operator overloading in C#. Notice how both the increment operator (`++`) and addition operator (`+`) create entirely new instances of `GateAttendance` using the `with` expression. Each time you write `gate++` or `gate += partySize`, the operators allocate a new record instance with the updated `Count` value, then return that new instance. While this approach maintains immutability and thread safety, it comes at the cost of frequent memory allocations. In scenarios with many operations—like our concert simulation with hundreds of attendance updates—these allocations accumulate quickly, potentially impacting performance and increasing garbage collection pressure. -To see this allocation behavior in action, try running the [.NET Object Allocation tracking tool](~/visualstudio/profiling/dotnet-alloc-tool) in Visual Studio. When you profile the current implementation during the concert simulation, you discover that it allocates 134 `GateAttendance` objects to complete the relatively small simulation. Each operator call creates a new instance, demonstrating how quickly allocations can accumulate in real-world scenarios. This measurement provides a concrete baseline for comparing the performance improvements you achieve with compound assignment operators. +To see this allocation behavior in action, try running the [.NET Object Allocation tracking tool](/visualstudio/profiling/dotnet-alloc-tool) in Visual Studio. When you profile the current implementation during the concert simulation, you discover that it allocates 134 `GateAttendance` objects to complete the relatively small simulation. Each operator call creates a new instance, demonstrating how quickly allocations can accumulate in real-world scenarios. This measurement provides a concrete baseline for comparing the performance improvements you achieve with compound assignment operators. ## Implement compound assignment operators