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

  • Balanced binary tree for dictionary
    • Hash code used as key (see also SortedInt32KeyNode πŸ”—)
    • Every node (HashBucket πŸ”—) contains an ImmutableList,
      • which is again a balanced binary tree
  • Builder pattern similar to ImmutableList​
  • Much slower than regular dictionary/hashset
    • Do performance testing!
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

  • Code samples on GitHub πŸ”—
  • Read only, frozen, and immutable collections πŸ”—
  • System.Collections.Immutable Namespace πŸ”—
  • Immutable Collections πŸ”—
  • Immutable collections with mutable performance πŸ”—

Thank you!

Rainer Stropek | @rstropek

Immutability in C#

By Rainer Stropek

Immutability in C#

  • 786