1. What is immutability?
Let's start with an analogy: imagine you're an accountant who makes a sales report every day. Like a real pro, you don't change last week's report, you make a new one—based on the old one, but with updated data. It's the same with immutable objects in programming: after you create such an object, it never changes, and any "change" means making a new copy.
Immutability (immutability) is a property of an object to not change after initialization. All its properties become "frozen": if you want a different value—make a new object.
Why do we need this?
- It keeps things safe: if nobody can accidentally change your object, your data won't suddenly get messed up. This happens a lot in multithreaded programs, when several threads try to change stuff at the same time.
- Makes debugging easier: if the object doesn't change, you know exactly what happened after it was created.
- Super handy for passing data around, especially in distributed systems where copies can drift apart.
- Lets you make "snapshots" (snapshots)—the history of changes becomes obvious.
2. Immutability in record types
When you declare a regular class, its properties are mutable (mutable) by default. Example:
public class UserProfile
{
public string Name { get; set; }
public int Age { get; set; }
}
var user = new UserProfile { Name = "Ivan", Age = 25 };
user.Age = 26; // All good—regular class: changing age "on the fly"
With record it's different by default: they're for storing immutable data.
public record UserProfile(string Name, int Age);
// Creating an object:
var user = new UserProfile("Ivan", 25);
// Trying to change age:
user.Age = 26; // Compilation error: property is read-only!
Properties of a positional record are declared read-only (init-only). You can't change them after creation, but you can use a with-expression to make a new copy with a changed property.
Mutable classes vs immutable records
| Class | Record-positional |
|---|---|
Properties by default |
Immutable |
| How to change Access the property |
Only by creating a new copy |
| Object comparison By reference (ReferenceEquals) |
By value (Equals) |
| Handy for passing data Not always |
Yep |
3. with-expressions
You might be thinking—"record is cool, but how do I live if I can't change them?" That's where the magic of with-expressions comes in!
with is a special syntax that lets you create a new copy of a record, changing only the properties you want.
So: "Take this object, make a copy, but tweak a couple properties here."
Simple example
var user1 = new UserProfile("Anna", 30);
// ... but life moves on, and Anna got older
var user2 = user1 with { Age = 31 };
// user1 stayed the same, user2 is a copy but a year older
Console.WriteLine(user1); // UserProfile { Name = Anna, Age = 30 }
Console.WriteLine(user2); // UserProfile { Name = Anna, Age = 31 }
Under the hood
It's not a mutant clone, it's a new object created with a special auto-generated Clone() method that makes a copy and plugs in new values.
If with-expressions existed in real life, you could wake up in the morning not in your "old tired body", but in a copy of yourself with a tuned-up mood and bigger muscles (but only if you were a record).
4. A bit about nesting and copying
If a record contains other records—you're good:
public record Address(string City, string Street);
public record Student(string Name, int Age, string Email, Address Home);
var a1 = new Address("Moskva", "Tverskaya");
var s1 = new Student("Lena", 21, "lena@mail.ru", a1);
var s2 = s1 with { Home = a1 with { Street = "Arbat" } };
Here everything will work truly immutably, because the nested Address is also a record.
5. Last gotchas
Positional records = compactness
You can declare a record in the “short” form (positional syntax). Then all properties automatically get init-only.
public record Course(string Name, int Credits);
var c1 = new Course("C#", 5);
var c2 = c1 with { Credits = 6 };
Analogy with read-only (init-only) properties
In a record, you can explicitly declare properties like this:
public record Student
{
public string Name { get; init; }
public int Age { get; init; }
}
You can only change these properties during initialization (or with with).
6. Practice: demo app
Let's write our training "Online School". Let's say we already have a record for a student:
public record Student(string Name, int Age, string Email);
Classic: someone made a typo in the address, but the student already created an account. How do you "update" the email? Of course, with with!
var student = new Student("Ekaterina", 19, "kate@school.com");
var updatedStudent = student with { Email = "ekaterina@school.com" };
// Let's check the objects:
Console.WriteLine(student); // Student { Name = Ekaterina, Age = 19, Email = kate@school.com }
Console.WriteLine(updatedStudent); // Student { Name = Ekaterina, Age = 19, Email = ekaterina@school.com }
7. Typical mistakes and pitfalls
Now a bit about the pain—where students most often mess up when playing with immutable records.
- First, a lot of folks think with changes the original object. Actually, the original object stays the same, and a new one is created with the changed fields. Sometimes this can trip you up, losing the new values.
- Second, remember: if your record has nested mutable objects (like an array or List), the with-expression does NOT do a deep copy! Your collection will be the same for both copies.
public record Student(string Name, int Age, List<string> Subjects);
var s1 = new Student("Oleg", 22, new List<string> { "Math", "Physics" });
var s2 = s1 with { };
s1.Subjects.Add("C#"); // Oops, now s2.Subjects also includes "C#"
That's why for truly immutable state it's better to use only simple types or collections that are themselves immutable (ImmutableList<T> and others from System.Collections.Immutable).
If you want to guarantee real immutability, use these collections or do manual deep copying.
GO TO FULL VERSION