Go to content|Go to the main menu|Go to search

edhouse-CookieGdpr-Policy-s
3203657
2
/en/gdpr/
320650B6A

Back to Blog

TutorialsEducation

Immutable collections in C#: What should you watch out for?

29.9.2026Filip Kubíček

We encounter immutable collections in C# quite often, but we frequently don't realize all the options available to us, how exactly they differ, and how to choose the right one for a given situation.

Immutable and read-only collections may seem like a simple concept at first glance. Just imagine, for example, a list or dictionary whose elements cannot be added or removed, only read.

But what are they good for?

Answering that question reveals that C# offers several kinds of immutable and read-only collections, and the answer depends on the broader context. If we want to write high-quality code, it is worth understanding the available options and knowing how to choose the right one. Let's take a look.

IReadOnly Interfaces: Read-only on the Surface

This is usually the first group developers encounter. It includes interfaces such as IReadOnlyList<T>, IReadOnlyCollection<T>, and IReadOnlyDictionary<TKey, TValue>.

These interfaces contain a subset of the methods found on traditional collections—specifically, they omit the methods that can modify a collection.

List<string> names = ["Ada", "Grace"];

IReadOnlyList<string> roNames = names;

Code working with the roNames variable cannot call methods such as Add, Remove, or Clear. This is a useful signal for developers and an enforced constraint at the same time.

However, the immutability guarantee is only superficial. Code working with the original names reference can make any changes it likes. Both references point to the same object, so a change is also reflected in roNames.

It is even possible to cast roNames back to List<string> and modify it. Of course, explicitly breaking the contract this way is asking for trouble.

Although the IReadOnly interfaces are read-only in appearance only, they are a very useful mechanism. Using them costs almost nothing, and their main benefits are clearer code and preventing accidental collection mutations. In most everyday cases, that is exactly what we need.

ReadOnly Classes: Still Only a Wrapper

For completeness, the same principle also applies to a similar group of classes, including ReadOnlyCollection<T>, ReadOnlyDictionary<TKey,TValue>, and ReadOnlySet<T>.

ReadOnlyCollection<string> rocNames = names.AsReadOnly();

Like the IReadOnly interfaces, the ReadOnly classes are merely wrappers around the original reference. The collection cannot be modified through the wrapper because the mutating methods are unavailable. But underneath the wrapper lies an ordinary mutable object.

Immutable: True Immutability

What if, instead of a mere read-only reference, we want a genuinely immutable collection?

We might naturally reach for classes such as ImmutableList<T>, ImmutableDictionary<TKey, TValue>, ImmutableHashSet<T>, and others. Instances of these classes really are immutable.

IImmutableList<string> fruits = ImmutableList.Create("kiwi", "mango");	

The resulting object will always contain exactly the strings kiwi and mango, regardless of how many references to it we create or what we do with them.

What benefits does this guarantee bring? Here, we need to be careful about what the Immutable classes are really designed for.

When working with immutable collections, the point is not that the data never changes at all. It must not change within a particular object. If we want a collection that contains, for example, one additional element, we need to create a new collection as a copy of the original with our change applied:

IImmutableList<string> fruitsAndVegetables = fruits.Add("carrot");

The key property of Immutable classes is that this does not involve copying the entire original collection. Behind the scenes, they use shared memory, allowing the new object to efficiently reuse the immutable data from the original collection.

This brings important memory savings when the original collection is large. Changes can also be chained naturally.

IImmutableList<string> fruitsAndVegetablesAndMeat = fruitsAndVegetables.Add("beef");

Because of this mechanism, reading from these immutable collections can be slower. This trade-off has its place. Efficiently creating a modified copy while preserving the immutability of the original object is essential for some programming styles—especially functional programming—and can effectively reduce the complexity of concurrent programs.

Frozen: Create Once, Read Many Times

If we want an immutable collection truly optimized for reading, starting with .NET 8 we can use FrozenDictionary<TKey,TValue> and FrozenSet<T>.

FrozenSet<string> colors = new HashSet<string> { "yellow", "green" }.ToFrozenSet();

The resulting frozen collection offers fast read operations. This is useful for a large collection that is accessed frequently. The price of this optimization is slower creation of the collection. During creation, the collection's data is analyzed and a structure enabling fast reads is computed.

One Final Catch

Immutable collections have one more catch: their guarantee applies only to the structure of the collection. If the collection contains mutable objects, their data can still change. The solution is clear—if that matters, immutable objects must be used.

Which One Should You Choose?

To recap, here are the options C# gives us:

  • We commonly use IReadOnly interfaces in code when we want to express that a given collection should not be modified.
  • Immutable classes are useful when we need to make changes without modifying the original collection instance.
  • Frozen classes are a good fit when we want to optimize collection reads at the cost of slower creation and the inability to modify it.

Personally, I often use IReadOnly interfaces in my code. In contrast, Immutable and Frozen classes are better suited to more specific situations. Still, it is useful to know their properties not only for the cases where they are the right choice, but also to avoid using them incorrectly.

Share article

Author

Filip Kubíček

Filip KubíčekBackend developer primarily in .NET, also interested in Go and DevOps. In my free time, I enjoy cycling, water sports, and tinkering with tech.

Edhouse newsletter

Get the latest updates from the world of Edhouse – news, events, and current software and hardware trends.

By signing up, you agree to our Privacy Policy.

Thank you for your interest in subscribing to our newsletter! To complete your registration you need to confirm your subscription. We have just sent you a confirmation link to the email address you provided. Please click on this link to complete your registration. If you do not find the email, please check your spam or "Promotions" folder.