C#
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
233: With-Expressions for Non-Destructive Mutation
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
CharacterStatswith the following properties:string Name,int Health,int Mana, andint Strength. - In your main program, create an instance of
CharacterStatsfor a character named "Valerius" with 100 Health, 50 Mana, and 15 Strength. - Create a second variable called
poweredUpStatsthat uses awithexpression to increase Valerius'sStrengthto 25 andManato 80, while keeping theNameandHealththe same. - Print both the original stats and the powered-up stats to the console to verify that the original object remained unchanged.
There are no comments for now.