← Open in the full interactive course (progress tracking, search & more)

For as long as C# has supported operator overloading, a += b on a custom type has quietly meant "build a whole new value and reassign it." C# 14 finally lets a type say otherwise.

Continuing this Part's tour of C# 14 additions — partial constructors (329) closed a gap for source generators. This lesson is unrelated to generators entirely; it's about performance, and specifically about what actually happens when your code writes a += b on a type you defined yourself.

Lesson 011 introduced operator overloading — defining what +, ==, and similar operators mean for your own types. What lesson 011 didn't cover, because it wasn't true until C# 14, is that a type can now also define its own dedicated behavior for a compound assignment operator like += — one that mutates an existing instance directly, instead of always being sugar for "compute a new value with operator+, then reassign it."

What Is It?

The Simple Explanation

Before C# 14, writing a += b on your own type always meant exactly the same thing as writing a = a + b — the compiler expanded it for you, every time, whether or not that was the cheapest way to get the job done. C# 14 lets a type define its own += directly, as an instance operator that updates a in place — no new value constructed, no reassignment.

The Technical Definition

C# 14 introduces user-defined compound assignment operators — instance operator declarations (no static, returning void) that give a type explicit control over what a compound assignment like +=, -=, *=, or similar actually does, separately from the type's ordinary binary operator+ overload. When a type defines an instance operator +=, the compiler calls it directly on assignment; when it doesn't, a += b continues to desugar to a = a + b exactly as it always has — this is a fully backward-compatible, opt-in addition.

Before C# 14 — Always Sugar

C# 14 — Optionally Direct

Why Does It Exist?

The Problem — Sugar That Can't Tell You Have a Better Option

The old desugaring rule is simple, predictable, and exactly the problem: a += b reconstructing a brand-new value every single time, even in a hot loop where a is a mutable struct or a reference type you fully control and could safely update in place. For a large mutable struct, that means the whole struct gets rebuilt and copied back on every iteration — real, avoidable work — instead of just the one or two fields that actually changed. For a mutable reference type, it's worse: a = a + b means operator+ allocates a brand-new object on the heap, every single time, purely so it can be immediately reassigned over the old one — exactly the kind of avoidable allocation lesson 174's memory allocation coverage and lesson 231's zero-allocation programming techniques spend real effort eliminating elsewhere.

The Solution — Let the Type Author Decide

C# 14 gives the type author an escape hatch: define an instance operator += and the compiler calls it directly, in place, whenever it can prove the target is genuinely being compound-assigned (not just read). The type's author — the one person who actually knows whether "update in place" is safe and correct for this type — gets to opt in specifically where it pays off, while every type that doesn't define one keeps behaving exactly as it always has.

Big Picture

TWO INDEPENDENT OPERATORS, TWO INDEPENDENT JOBS
operator+
static, returns a new value — used for plain a + b expressions where the original must stay untouched
operator +=
instance, returns void, mutates this — used specifically for a += b, where reassigning a was always the goal anyway

The two operators can coexist on the same type, each doing what's cheapest for its own situation — a plain a + b genuinely needs a new value (the original a has to remain unchanged), while a += b was always going to end with a holding the new result anyway, so mutating it directly loses nothing.

How It Works

WHAT HAPPENS WHEN THE COMPILER SEES a += b
1. DOES a's TYPE DEFINE AN INSTANCE operator +=?
2A. YES — CALL IT DIRECTLY, MUTATING a IN PLACE
2B. NO — FALL BACK TO THE ORIGINAL DESUGARING
3. EITHER WAY, THE OBSERVABLE RESULT SHOULD BE THE SAME

Simple Example

public struct Accumulator { public int Total { get; private set; } // The ordinary operator, from lesson 011 — static, returns a NEW value public static Accumulator operator +(Accumulator left, int amount) => new Accumulator { Total = left.Total + amount }; // C# 14 — instance compound-assignment operator: mutates THIS in place public void operator +=(int amount) { Total += amount; } } var acc = new Accumulator(); var next = acc + 5; // uses operator+ — builds a NEW Accumulator, acc is untouched acc += 5; // uses operator+= — mutates acc.Total directly, no new instance

Meaning: acc + 5 and acc += 5 both conceptually "add 5," but they now take genuinely different paths to get there. The first has to build a new Accumulator because acc is expected to remain unchanged. The second was always going to end with acc holding the new total anyway — C# 14 lets that be a direct mutation instead of a full reconstruction.

Real-World Example — a Hot-Loop Statistics Accumulator

Picture a running-statistics type updated potentially millions of times inside a tight loop — a game's per-frame physics accumulator, or a metrics counter processing a high-volume stream. This is exactly the kind of hot-path scenario lesson 231's zero-allocation programming coverage cares about, and it's where the old desugaring rule stops being a minor inefficiency and starts being a measurable one:

public struct RunningStats { public double Sum { get; private set; } public long Count { get; private set; } public readonly double Average => Count == 0 ? 0 : Sum / Count; public static RunningStats operator +(RunningStats left, double sample) => new RunningStats { Sum = left.Sum + sample, Count = left.Count + 1 }; // C# 14 — update the two fields directly; no new RunningStats value // has to be constructed and copied back over "this" on every sample public void operator +=(double sample) { Sum += sample; Count += 1; } } var stats = new RunningStats(); foreach (var sample in incomingSensorReadings) // potentially millions of samples { stats += sample; // in-place field updates, every iteration } Console.WriteLine($"Average: {stats.Average}");

For a struct like this, the old a = a + b path meant constructing an entire new RunningStats value and copying it back over stats on every single sample — real, repeated work scaling with every field the struct carries. The instance operator += updates exactly the two fields that changed and nothing else. The effect is even more direct for a mutable reference type doing the equivalent job: without a defined operator +=, stats = stats + sample would mean operator+ allocating a brand-new object on the managed heap on every single iteration, purely to be thrown away and replaced a moment later — exactly the churn lesson 174's coverage of memory allocation warns against in a hot path.

Analogy

Repainting a Wall Instead of Rebuilding the House

Imagine that every time you wanted to repaint one wall of your house a slightly different color, the only process available was: demolish the entire house, build an identical new one next door with the new wall color, then move everything you own into the new house and demolish the old one. That's absurd for a single wall — but it's exactly what a = a + b asks a type to do on every compound assignment: reconstruct the whole value from scratch, just to change what might be one field. A user-defined operator += is the sane alternative: send a painter to repaint just that one wall, in the house that's already standing. The house you end up with looks identical either way — the difference is entirely in how much unnecessary demolition and rebuilding happened to get there.

Under the Hood

A user-defined compound assignment operator is declared as an instance member — no static keyword, unlike lesson 011's binary operator+ — and returns void. It operates on this implicitly, the same way any other ordinary instance method does; there's no return value to reassign because nothing new was ever constructed to assign back. The compiler only routes a compound assignment expression through this operator when it can confirm the left-hand side is a genuine, mutable variable being assigned to — not a read-only expression, and not a value it can't safely mutate in place.

This also explains why the feature naturally has no effect on a readonly struct: a readonly struct's instance members already can't mutate this under any circumstances, by the same rule that's applied to every other instance method on a readonly struct — nothing new to learn there, the existing readonly rule simply continues to apply. And because operator+ and operator += are entirely separate declarations, defining one never implicitly defines or changes the other — a type can have a binary operator+ with no compound counterpart (falling back to the old desugaring), a compound operator with no binary counterpart (making plain a + b unavailable), or both together, each doing its own job.

Common Confusion

1. "Defining operator += changes what a += b computes" — no, only how it's computed

A well-designed compound assignment operator should always produce the same conceptual result as the old a = a + b desugaring would have — this feature is about avoiding unnecessary reconstruction, not about giving += a different meaning than + implies. Making the two diverge in behavior would violate the same operator-overloading intuition lesson 011 already asked you to respect for ordinary operators.

2. "Defining operator += means I don't need operator+ anymore" — no, they serve different expressions

Plain a + b (where a must remain unchanged) still needs the static, value-returning operator+. Defining only operator += speeds up compound assignment specifically — it doesn't give you the ability to write a + b as a standalone expression at all.

Common Mistakes

Mistake 1 — Letting += and + drift into different behavior

Implementing operator += with different rounding, clamping, or validation logic than operator+ uses, so a += b and a = a + b silently produce different results on the same type. Keep both operators computing the same conceptual result — the compound operator should only change the mechanism (mutate vs. reconstruct), never the outcome.

Mistake 2 — Reaching for this on a type that isn't actually in a hot path

Adding a custom operator += to an ordinary business/DTO type used a handful of times per request, purely because it seems like a modern best practice. This feature earns its complexity in measurably hot loops — the kind lesson 231's zero-allocation programming techniques already target. For code that isn't allocation- or copy-sensitive, the plain desugared behavior is simpler and every bit as correct.

Mistake 3 — Expecting it to work on a readonly struct

Trying to define a mutating operator += on a readonly struct and being surprised it can't mutate any state. A readonly struct's instance members can never mutate this, for the same reason any other instance method on one can't — this feature doesn't create an exception to that existing rule.

When Should I Use It?

Rule of thumb: If you wouldn't have reached for lesson 231's zero-allocation techniques on this type, you probably don't need a custom operator += either — this feature is a hot-path optimization, not a general-purpose habit to apply to every operator-overloaded type.

Mental Model

operator+ (static, returns a value) = for a + b, where a must stay unchanged
operator += (instance, void, C# 14) = for a += b, where a was always going to end up holding the new result anyway
No compound operator defined? Nothing changes — a += b still means a = a + b, exactly as before

Remember: same outcome either way — this feature only changes HOW the outcome is reached, never WHAT it is.

Key Takeaway


Check Your Understanding

You've seen how a type can now take control of what += actually does under the hood. Let's confirm it clicked.

1. Before C# 14, what did a += b always mean for a custom type?

Show answer

Correct: B

Why B is correct: This is exactly the old, universal rule the lesson opened with — compound assignment on a custom type was always sugar for the binary operator plus reassignment, with no way for the type to intervene.

Why A is incorrect: No dedicated compound operator was needed or even possible before C# 14 — the desugaring to a = a + b handled it automatically as long as operator+ existed.

Why C is incorrect: The opposite was true — the old desugaring meant constructing a new value and reassigning it, never an automatic in-place mutation.

Why D is incorrect: The old desugaring applied uniformly to both classes and structs — it wasn't restricted to one category of type.

Reinforcement: "Always sugar for a = a + b" is the baseline behavior this whole feature is a deliberate, opt-in exception to.

2. A type defines only a static operator+ and no instance operator +=. What happens when code writes a += b on that type in C# 14?

Show answer

Correct: B

Why B is correct: The feature is fully backward-compatible and opt-in — a type with no compound operator defined behaves exactly as it always has, falling back to the familiar a = a + b desugaring.

Why A is incorrect: Defining a compound operator is entirely optional; existing operator-overloaded types keep compiling and behaving exactly as before with no changes required.

Why C is incorrect: The compiler never auto-synthesizes a mutation from operator+ — mutation only happens when the type explicitly defines its own operator +=.

Why D is incorrect: There's no runtime exception involved — this is a purely compile-time choice between two well-defined, always-successful code paths.

Reinforcement: No compound operator defined means no behavior change at all — this feature only does something when a type opts in.

3. Why can't a readonly struct meaningfully define a mutating operator +=?

Show answer

Correct: B

Why B is correct: This is a consequence of an existing rule, not a new restriction invented for this feature — a readonly struct's instance members were already unable to mutate this, and a mutating compound operator is just another instance member subject to that same rule.

Why A is incorrect: Non-readonly structs can define compound assignment operators without issue — the restriction is specific to the readonly modifier, not structs in general.

Why C is incorrect: The lesson's own primary examples (Accumulator, RunningStats) are structs defining compound assignment operators successfully — mutable structs are actually the type category this feature helps the most.

Why D is incorrect: Readonly structs support ordinary operator overloading (like operator+) just fine — only mutation-based members, compound assignment included, are off-limits.

Reinforcement: This feature doesn't create a new exception to the readonly struct mutation rule — it's simply subject to the same rule every other instance member already follows.

4. What's the recommended relationship between what operator+ and operator += compute, on a well-designed type?

Show answer

Correct: A

Why A is correct: This is exactly what Common Confusion #1 and Common Mistake #1 both warned about — the compound operator is meant to be a mechanism-level optimization, not an opportunity to give += different semantics than + implies.

Why B is incorrect: This is precisely the anti-pattern Common Mistake #1 called out — letting the two operators drift into different results is exactly what NOT to do.

Why C is incorrect: While the compiler doesn't technically enforce it, the lesson is explicit that a well-designed type keeps the two operators computing the same conceptual outcome — divergence is a design mistake, not a supported use case.

Why D is incorrect: Plain a + b expressions still need operator+ — defining only operator += doesn't give a type the ability to support ordinary addition expressions.

Reinforcement: Same outcome, cheaper mechanism — that's the entire design intent behind this feature.

Last in this Part: modern file-based apps (331) — closing out Part XII with a look at how C# 14 changes not what you write inside a file, but what has to exist around it before you can run it.


dotnetmadeeasy.com — Learn C# and .NET, the right way.