Immutability in C#
Rainer Stropek | @rstropek
Introduction
Rainer Stropek
- Passionate software developers for 25+ years
Β - Microsoft MVP, Regional Director
Β - Trainer, Teacher, Mentor
Β - π community
Why?
Core Concepts
- Methods can have side effects
- Mutate global state, static state, or parameters
- Methods with side effects require synchronizationβ in multi-threaded contexts
- Methods without don't
- π Simpler, more robust code by making side effects impossible using immutables
- Other languages: Instances are immutable by default if not otherwise stated (e.g. Rust)
- C#: Buiding immutable classes/structs has been made easier
Concepts
Mutable Objects
Mutable Objects (aka read/write)
- (Optionally) Initialize state when constructing the object
- Constructor
- Initializers
- Change state in-place during object lifetime
- Pros π
- Object representing things that can change
- Well-suited for data binding in UI apps
- Cons π
- Not thread-safe by nature
Immutable Objects
Immutable Objects
- Initialize state when constructing the object
- Constructor
- Initializers
- Cannot change state in-place during object lifetime
- Often: Get a changed instance by cloning
- Well-known example in C#: Strings
- Shallow immutable vs. deep immutableΒ
- Pros π
- Thread-safe by nature
- Cons π
- Need to copy object in case of changes
- Not well-suited for data binding in UI apps
Feezables
Freezables
- Mutable until some point
- Can be frozen
- Becomes immutable
- Pros π
- Mutable as long as necessary (e.g. during manipulation in UI)
- Thread-safe by nature after freezing
- Cons π
- No specific support in C#
- No base class in BCL (only in WPF: System.Windows.Freezable)
- Requires quite a lot of boilerplate code (source generators?)
Read-Only
Read-Only
- A reference to a type that does not permit mutation of underlying data
- Mutation might be possible through other type
- Examples in .NET: IEnumerable<T>, ReadOnlyCollection<T>
- Thread-safe to a certain extent (depends on the implementation) π
Immutable Classes and Structs
Demo
Time!
Immutable Collections
When to use?
- To enhance readability/maintainability
- Question "where is this collection altered" is easier to answer
- Consumer has a stable shapshot of data structure, does not need to worry about ongoing changes
- In multi-threaded programs
- Immutable collections are thread-safe by nature
- Alternative: Concurrent collections (e.g. ConcurrentStack)
- Changes are immediately seen by all consumers of the collection
- Immutable collections support change batches
- Immutable collections implement common interfaces like IEnumerable, IList, ICollection
- Methods operating on those interfaces can consume immutable collections without changes
Immutable Stack
- Simplest immutable collection
- Implemented as a linked list π
var stack = ImmutableStack<int>.Empty;
stack = stack.Push(0);
stack = stack.Push(1);
Assert.Equal(2, stack.Count());
Assert.Equal(1, stack.Peek());
stack = stack.Pop(out var top);
Assert.Equal(1, top);
Assert.Single(stack);
Immutable Queue
- Implemented with immutable stacks in the background
var queue = ImmutableQueue<int>.Empty;
queue = queue.Enqueue(0);
queue = queue.Enqueue(1);
queue = queue.Enqueue(2);
Assert.Equal(3, queue.Count());
Assert.Equal(0, queue.Peek());
queue = queue.Dequeue(out var first);
Assert.Equal(0, first);
queue = queue.Dequeue(out var _);
Assert.Single(queue);
Immutable Array
- Thin wrapper around T[]
- If you change it, copy of whole array is made
- Builder-pattern to efficiently implement large number of changes
var l1 = ImmutableList<string>.Empty;
var l2 = l1.Add("1");
var l3 = l2.Add("2");
var l4 = l3.Add("3");
var l5 = l4.Replace("2", "4");
var builder = ImmutableList<string>.Empty.ToBuilder();
builder.AddRange(Enumerable.Range(1, 7).Select(n => n.ToString()));
var l1 = builder.ToImmutable();
var l2 = l1.Replace("4", "99");
Immutable Dictionary, HashSet
var d1 = ImmutableDictionary<string, string>.Empty;
var d2 = d1.SetItem("1", "One");
var d3 = d2.SetItem("2", "Two");
var d4 = d3.SetItem("2", "Zwei");
Assert.Equal("Zwei", d4["2"]);
var builder = ImmutableDictionary<string, string>
.Empty.ToBuilder();
builder.AddRange(Enumerable.Range(1, 5).Select(
n => new KeyValuePair<string, string>(
n.ToString(), "FooBar")));
var d1 = builder.ToImmutable();
var d2 = d1.SetItem("2", "Zwei");
Immutable Sorted Set
- Implemented as a binary tree
- Sorting based on default comparer or custom comparer
- Implemented also List-related interfaces (IReadOnlyList, IList)
- Contains set functions like intersect, union, etc.
- Builder-pattern to efficiently implement large number of changes
var s = ImmutableSortedSet<Person>.Empty.WithComparer(new PersonComparer());
s = s.Add(new("B", "B"));
s = s.Add(new("C", "C"));
s = s.Add(new("A", "A"));
Assert.Equal(0, s.IndexOf(new("A", "A")));
Immutable Sorted Dictionary
- Balanced binary tree for dictionary
- Dictionary key used as sort key (see also Node π)
- Builder pattern similar to ImmutableListβ
var d1 = ImmutableSortedDictionary<string, string>.Empty;
var d2 = d1.SetItem("1", "One");
var d3 = d2.SetItem("2", "Two");
var d4 = d3.SetItem("2", "Zwei");
Assert.Equal("Zwei", d4["2"]);
var builder = ImmutableSortedDictionary<string, string>.Empty.ToBuilder();
builder.AddRange(Enumerable.Range(1, 5).Select(
n => new KeyValuePair<string, string>(n.ToString(), "FooBar")));
var d1 = builder.ToImmutable();
var d2 = d1.SetItem("2", "Zwei");
ImmutableInterlocked
- Methods for mutating immutable collections in-place
- Relevant for multi-threaded apps operating on a single collection
- E.g. ASP.NET API where all requests access the same collection
var stack = ImmutableStack<int>.Empty;
ImmutableInterlocked.Push(ref stack, 0);
ImmutableInterlocked.Push(ref stack, 1);
ImmutableInterlocked.Push(ref stack, 2);
Assert.Equal(3, stack.Count());
var d = ImmutableDictionary<string, string>.Empty;
ImmutableInterlocked.AddOrUpdate(ref d, "1", "One", (_, _) => "One");
ImmutableInterlocked.AddOrUpdate(ref d, "2", "Two", (_, _) => "Two");
ImmutableInterlocked.AddOrUpdate(ref d, "1", "Eins", (_, _) => "Eins");
Assert.Equal("Eins", d["1"]);
Assert.Equal("Two", d["2"]);
Immutables...
- ...can make your code more robust
- ...make your code thread-safe
- ...are sometimes slower than mutable counterparts (collections)
- Do perf testing
- Consider alternatives (mutable or concurrent collections)
- ...are not a silver bullet
Resources
Thank you!
Rainer Stropek | @rstropek
Immutability in C#
By Rainer Stropek
Immutability in C#
- 786