ImmutableList
ReadOnlyCollection
.NET collections
C# programming
software development

Why use ImmutableList over ReadOnlyCollection?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

ReadOnlyCollection<T> and ImmutableList<T> both prevent accidental mutation through a particular API surface, but they do not provide the same guarantee. The real choice is between a read-only view over mutable state and a truly immutable value that cannot change after creation.

The Core Semantic Difference

ReadOnlyCollection<T> is a wrapper around another collection. It blocks mutation through the wrapper, but if some other code still holds the original list, that other code can modify it and every reader of the wrapper will observe the change.

ImmutableList<T> is different. Operations such as Add, Remove, or Replace do not mutate the existing instance. They return a new list, while the original list remains unchanged.

That means the guarantees are fundamentally different:

  • 'ReadOnlyCollection<T>: do not mutate through this reference'
  • 'ImmutableList<T>: this value will never change'

A Simple Example

csharp
1using System;
2using System.Collections.Generic;
3using System.Collections.ObjectModel;
4
5var source = new List<string> { "a", "b" };
6var readOnly = new ReadOnlyCollection<string>(source);
7
8Console.WriteLine(readOnly.Count); // 2
9source.Add("c");
10Console.WriteLine(readOnly.Count); // 3

The wrapper was read-only, but the data was still mutable.

Now compare that with ImmutableList<T>.

csharp
1using System;
2using System.Collections.Immutable;
3
4var original = ImmutableList.Create("a", "b");
5var updated = original.Add("c");
6
7Console.WriteLine(original.Count); // 2
8Console.WriteLine(updated.Count);  // 3

The original list remains a stable snapshot.

Why Immutability Is Valuable

That snapshot behavior matters whenever data is shared, cached, logged, or passed across layers. It makes code easier to reason about because the value you received will still mean the same thing later.

This is especially useful in:

  • concurrent code
  • event-sourced or reducer-style state updates
  • public APIs with strong correctness expectations
  • history, undo, or snapshot features

With ReadOnlyCollection<T>, you still rely on discipline around the hidden backing collection.

Concurrency Is a Good Example

An immutable collection is naturally easier to share across threads because readers do not need to defend against in-place modification.

csharp
1using System.Collections.Immutable;
2using System.Threading.Tasks;
3
4var values = ImmutableList.Create(1, 2, 3);
5
6Parallel.ForEach(values, x =>
7{
8    Console.WriteLine(x);
9});

This does not solve every concurrency problem, but it removes the problem of another thread unexpectedly changing the collection underneath you.

A read-only wrapper cannot offer that guarantee if the backing list is still mutable elsewhere.

Why ReadOnlyCollection<T> Still Exists

It is lighter and cheaper when you only need API-level protection. If your code owns the mutable list internally and just wants consumers to stop mutating it directly, a read-only wrapper can be sufficient.

That is reasonable when:

  • internal mutation is intentional
  • performance overhead matters more than snapshot semantics
  • you only need to restrict one access path

So ImmutableList<T> is not automatically better everywhere. It is better when you need stronger guarantees.

Structural Sharing Makes Immutable Lists Practical

A common misconception is that every change to ImmutableList<T> copies the whole list. In practice, immutable collection implementations reuse a lot of internal structure, so updates are cheaper than a naive full copy.

You still pay overhead compared with a thin wrapper, but you are paying for semantics: stability, safety, and easier reasoning.

Common Pitfalls

The biggest mistake is believing ReadOnlyCollection<T> makes the underlying data immutable. It does not.

Another mistake is using ImmutableList<T> everywhere without thinking about update frequency and allocation cost.

A third issue is returning a read-only wrapper over a list that other parts of the class mutate constantly, then being surprised when callers observe changing contents.

Finally, immutable collections require a different programming style. Methods return new instances, so callers must keep the returned value instead of assuming in-place mutation.

Summary

  • 'ReadOnlyCollection<T> is a read-only view over potentially mutable data.'
  • 'ImmutableList<T> is an immutable value that never changes in place.'
  • Use ImmutableList<T> when stable snapshots and safe sharing matter.
  • Use ReadOnlyCollection<T> when you only need to block mutation through one API surface.
  • The real advantage of ImmutableList<T> is stronger semantics, not just syntax.
  • Choose based on required guarantees, not on naming alone.

Course illustration
Course illustration

All Rights Reserved.