4 min read

What's New in C# 14: User-Defined Compound Assignments

C# 14, coming with .NET 10, introduces user-defined compound assignments. It allows for the creation of more efficient assignment operators, preventing unnecessary object creation. Discover how to implement and use this new feature, and learn about the potential pitfalls to watch out for.

.NET 10 is scheduled to be released later this year and, with it, comes the next iteration of C#. C# 14 has several cool features planned, such as extension members and field-backed properties. However, one, perhaps lesser-known, feature is the new user-defined compound assignment.

Prerequisites

The code snippets in this article are for C# 14, which is still in development. This means that the syntax might change slightly before the final release of .NET 10. However, I will endeavor to keep this article updated if it does.

To run the code snippets yourself, you need to download and install the latest .NET 10 preview SDK. Next, create a new C# project (using a Console project in this article) and ensure it targets .NET 10. You'll also need to enable preview language features by opening your *.csproj file and adding the <LangVersion>preview</LangVersion> tag:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <!-- Add the LangVersion tag below. -->
    <LangVersion>preview</LangVersion>
  </PropertyGroup>
</Project>

That's it! You'll now be able to run the snippets.

Compound Assignments in C# 13 and Earlier

To understand how user-defined compound assignments will work, you should understand how compound assignments currently work in C# 13 and below. Traditionally, compound assignments like x += y are simply shorthand for x = x + y. Classes and structs can override the + operator, and this override is then automatically used when using a compound assignment:

public record struct Vector3(decimal X, decimal Y, decimal Z)
{
    public static Vector3 operator +(Vector3 left, Vector3 right)
    {
        return new Vector3(
            left.X + right.X,
            left.Y + right.Y,
            left.Z + right.Z
        );
    }
}

var one = new Vector3(1, 2, 3);
var two = new Vector3(4, 5, 6);

// Uses the operator overload
one = one + two;

// Also uses the + operator overload
one += two;

Using the + operator implementation makes sense for the compound assignment; however, it does have a slight performance impact.

The Hidden Costs of Compound Assignments

Each time the + operator is called, a new object is created within the override and returned. This means that even if you write: one += two, the operator method creates a third object containing the result of one + two and returns it, which is then assigned back to one. The previous object assigned to one is then marked for garbage collection.

This performance issue is usually minor. However, it can still cause noticeable effects in certain situations. Luckily, C# 14 addresses this by allowing you to create a self-assignment operator method.

A More Efficient Compound Assignment in C# 14

C# 14 introduces a new feature that lets you define a += operator override. This lets you define a more efficient, in-place compound assignment operator than the default + operator

Below is an example of a class with both the + and += operators overridden:

public record struct Vector3(decimal X, decimal Y, decimal Z)
{
    // Static + operator overload that returns a new instance
    public static Vector3 operator +(Vector3 left, Vector3 right)
    {
        return new Vector3(
            left.X + right.X,
            left.Y + right.Y,
            left.Z + right.Z
        );
    }

    // More efficient compound operator overload that does in-place
    // addition
    public void operator +=(Vector3 right)
    {
        X += right.X;
        Y += right.Y;
        Z += right.Z;
    }
}

var one = new Vector3(1, 2, 3);
var two = new Vector3(4, 5, 6);

// Uses the compound assignment operator
one += two;

// Uses the addition operator
var three = one + two;

Notice how the new operator overload is an instance method on the class instead of a static method. The return type is also void because no object needs to be created and returned. Instead, you can see how the right object's properties are added to the current object's properties.

You can also have a + operator override and a += compound assignment override for the same class. C# will automatically pick the most appropriate method when performing operations on the class. This lets you retain existing functionality exposed by an operator override while implementing a more efficient implementation for self-assignment operators.

Tips and Gotchas

While this can be really useful, there are a few things to consider when defining your compound assignment methods.

Maintain Logical Consistency

When defining both + and += overrides, it's best to ensure the logic remains consistent between the two methods. While you can intentionally make them different, doing so is a terrible idea and would confuse developers using the operators.

The purpose of the compound assignment override is to provide an efficient, self-assignment override that still matches the behaviour of the regular operator.

Know When to Use It

The new compound operator is designed to make self-assignment operations on objects faster and more efficient. You will see the most benefits from this feature when using it in classes or structs that handle a lot of data. It helps you avoid unnecessary memory use and copying entire objects.

In other situations, you might not notice much difference in performance. If that’s the case, it is probably easier to just implement the operator overload and let C# manage compound assignments for you.

Beware Fallbacks

You don't just have to write operator overrides for objects of the same type. C# allows you to implement multiple overloads for operators that enable operations between different types. The same applies to compound assignment overrides.

However, be careful that you don't inadvertently call an operator override because of a missing compound assignment override for that particular type. For example, the class below implements a compound assignment and operator override for int. However, only the operator method is defined for decimal.

public record struct Vector3(decimal X, decimal Y, decimal Z)
{
    public void operator *=(int right)
    {
        // ...
    }

    public static Vector3 operator *(Vector3 left, int right)
    {
        // ...
    }

    public static Vector3 operator *(Vector3 left, decimal right)
    {
        // ...
    }
}

var one = new Vector3(1, 2, 3);
var two = new Vector3(4, 5, 6);

// Uses the compound assignment method
one *= 2;

// Uses the operator override
two *= 3.4m;

When you call one *= 2, the compound assignment method for int is used correctly. However, two *= 3.4m will fall back to the operator override because no compound assignment operator is defined for decimal.

Conclusion

User-defined compound assignments are a small but powerful feature. If you have large data classes or structs and want to support operations, or if you're a library author aiming for efficiency, this is definitely worth considering. However, since you might be duplicating logic, consider how important the performance benefits are compared to the cost of duplicated code.