Transitioning from Classes to Records and Adopting Immutable Collections in C#
The purpose of this article is to advocate for using read-only records and immutable data structures when creating new types in our codebase. Moreover, whenever possible, we should update existing types to adopt these patterns.
In this article, we'll discuss how to update a C# code snippet to use records instead of classes and switch collection types to an immutable list interface. We'll also demonstrate two common implementations of immutable collections and briefly compare their differences.
Original Class Definitions
Initially, we had the following class definitions:
Changes Made
We made the following changes:
Converted
DocumentTypeandDocumentTypesfrom classes to records.Changed the
Itemsproperty inDocumentTypesto useImmutableArray<DocumentType>.
The updated code now looks like this:
Having an immutable array doesn't prevent someone from modifying the contents of an element in the list. To prevent unintended modifications, it's recommended to make the items in the list immutable. Since DocumentType is now a read-only record and its properties are of type string (which are immutable), we don't have to worry about modifications to individual elements.
Demonstrating Immutable Collections
To showcase the flexibility of immutable collections, we implemented two tests using two different concrete implementations: ImmutableArray<T> and ImmutableList<T>. (Note: These examples are generic and do not refer to any specific project.)
Using ImmutableArray<T>
Using ImmutableList<T>
Comparing Immutable Collections
Both ImmutableArray<T> and ImmutableList<T> provide immutable collections, but they have different performance characteristics:
ImmutableArray<T>:
Backed by a simple, contiguous array, it has lower memory overhead and better cache locality, making it ideal for scenarios requiring efficient random access.ImmutableList<T>:
Uses a more complex data structure (such as a balanced tree) that offers better performance for insertions and deletions in the middle of the collection but comes with higher memory usage.
For brevity, the remainder of this discussion will focus solely on the ImmutableArray<T> approach.
Creating Immutable Arrays in C#
The ImmutableArray<T> class provides several methods to create immutable collections, each with its own benefits and trade-offs:
ToImmutableArray():
An extension method available on anyIEnumerable<T>(from theSystem.Collections.Immutablenamespace). It creates anImmutableArray<T>by copying elements into a new array. This is convenient when converting an existing mutable collection.ImmutableArray.Create<T>(params T[] items):
A static method to create anImmutableArray<T>from a fixed set of items. This is useful when you have a small number of items known at compile time.var array = ImmutableArray.Create( new DocumentType { Id = "1", SystemName = "SystemName1" }, new DocumentType { Id = "2", SystemName = "SystemName2" } );ImmutableArray.CreateBuilder<T>():
This method creates a mutable builder for anImmutableArray<T>. You can add or remove items, and then convert it to an immutable array using theToImmutable()method. This approach is more efficient when constructing large collections or when the number of items is unknown. This approach is analogous to usingStringBuilderin C# to efficiently create a string by building it incrementally before finalizing the immutable result.var builder = ImmutableArray.CreateBuilder<DocumentType>(); builder.Add(new DocumentType { Id = "1", SystemName = "SystemName1" }); builder.Add(new DocumentType { Id = "2", SystemName = "SystemName2" }); var array = builder.ToImmutable();
This article illustrates how converting classes to records and implementing immutable collection types not only promotes immutability but also enhances overall design stability. The performance differences between ImmutableArray<T> and ImmutableList<T> arise from their data structure choices rather than their immutable nature.
See Also:
Functional in the Small and Object-Oriented in the Large (immutability)
Defensive Copying in C# Example (related immutability technique)
Utilizing Record Structs for Enhanced Performance in .NET (structs vs classes/records)
Comparing Objects for Equality in C# Using JSON Serialization (mentions record equality)
Self-Validating Value Objects in C# (uses records)
Avoiding Liskov Substitution Principal Violations in Type Hierarchies (uses records)