Skip to Content
Course content

233: With-Expressions for Non-Destructive Mutation

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've spent a lot of time debugging issues where a piece of state changed in one part of a system, and suddenly a completely unrelated module started crashing because it was holding a reference to that same object. In the old days of C#, the instinct was either to mutate the object directly—which is fast but dangerous—or to write a custom Clone() method. Both paths usually lead to headaches.

When we're working with records, we're embracing value semantics. The goal isn't to change an existing object, but to derive a new state from an old one. This is what we call "non-destructive mutation."

The boilerplate burden of manual copies

Imagine you're building a system to manage user preferences for a complex dashboard. You have a record with several properties, and you want to allow the user to toggle a single setting without affecting the original configuration object (perhaps so you can implement an "Undo" feature or a "Preview" mode).

public record UserPreferences(
    string Theme, 
    int FontSize, 
    bool NotificationsEnabled, 
    string Language, 
    bool HighContrastMode
);

// Naive approach: Manual copying
var currentPrefs = new UserPreferences("Dark", 12, true, "English", false);

// I just want to change the theme to "Light", but I need a new object
var updatedPrefs = currentPrefs with { Theme = "Light" }; // Wait, I'm skipping to the solution.

Before with expressions existed, you had to do this. It's tedious, but look at how fragile it is:

var updatedPrefs = new UserPreferences(
    "Light", 
    currentPrefs.FontSize, 
    currentPrefs.NotificationsEnabled, 
    currentPrefs.Language, 
    currentPrefs.HighContrastMode
);

This is a maintenance nightmare. If I add a Timezone property to the UserPreferences record tomorrow, this line of code will break. I'll have to hunt down every single place I manually copied the record and add the new property to the constructor call. It's a lot of noise for a very simple intent: "I want this object, but with one thing changed."

Letting the compiler handle the clone

The with expression is essentially a shorthand that tells the compiler: "Create a shallow copy of this record, and then apply these specific changes to the new instance."

var updatedPrefs = currentPrefs with { Theme = "Light" };

Under the hood, C# is doing the heavy lifting. It handles the copying of all the other properties for you. If you add ten more properties to the record, this line of code remains exactly the same and continues to work perfectly. It's clean, it's expressive, and it completely removes the risk of forgetting to copy a field.

You can also change multiple properties at once, which is where this really starts to shine in business logic:

var accessibilityPrefs = currentPrefs with 
{ 
    FontSize = 18, 
    HighContrastMode = true 
};

Why non-destructive mutation is a safer bet

You might be wondering, "Why not just make the properties set; instead of init; and change the value directly?"

When you mutate an object in place, you lose the history of that object. If another part of your app is currently reading currentPrefs while you're changing the Theme on a different thread, you've just introduced a race condition. By using with, you create a new snapshot. The original currentPrefs remains untouched and immutable.

This pattern is a lifesaver when implementing features like Redux-style state management or simple "Cancel" buttons in a settings menu. You keep the old state, generate the new state, and if the user hits cancel, you just discard the new record. No need to manually "revert" five different properties back to their original values.




📋 Practical Task

Implementing a Game Character Status Modifier

You are building a combat system for an RPG. Characters have a CharacterStats record that tracks their health, mana, and strength. Instead of mutating the character directly when they pick up a temporary power-up, you need to generate a new version of their stats using a with expression.

Your Task:

  • Define a record named CharacterStats with the following properties: string Name, int Health, int Mana, and int Strength.
  • In your main program, create an instance of CharacterStats for a character named "Valerius" with 100 Health, 50 Mana, and 15 Strength.
  • Create a second variable called poweredUpStats that uses a with expression to increase Valerius's Strength to 25 and Mana to 80, while keeping the Name and Health the same.
  • Print both the original stats and the powered-up stats to the console to verify that the original object remained unchanged.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.