.NET
List<T>
IReadOnlyList<T>
.NET 4.5
programming

Why does ListT implement IReadOnlyListT in .NET 4.5?

Master System Design with Codemia

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

Introduction

In .NET 4.5, List<T> was updated to implement IReadOnlyList<T> and IReadOnlyCollection<T>. This seems contradictory — List<T> is clearly mutable, so why does it implement a "read-only" interface? The answer lies in the distinction between read-only views and immutable collections. IReadOnlyList<T> does not make a collection immutable — it provides a read-only contract that consumers can depend on without needing the full mutable interface.

What IReadOnlyList<T> Actually Means

IReadOnlyList<T> provides:

csharp
1public interface IReadOnlyList<out T> : IReadOnlyCollection<T>
2{
3    T this[int index] { get; }
4}
5
6public interface IReadOnlyCollection<out T> : IEnumerable<T>
7{
8    int Count { get; }
9}

It guarantees that consumers can:

  • Read elements by index (this[int index])
  • Get the count (Count)
  • Enumerate the collection

It does not guarantee that the collection cannot be modified by someone else. It is a read-only view, not an immutability guarantee.

Why List<T> Implements It

1. Consumer-Side API Design

The primary motivation is allowing methods to accept IReadOnlyList<T> when they only need to read:

csharp
1// Before .NET 4.5 — callers must pass List<T> or T[]
2public double Average(List<int> numbers) { ... }
3
4// After .NET 4.5 — accepts any read-only list
5public double Average(IReadOnlyList<int> numbers)
6{
7    double sum = 0;
8    for (int i = 0; i < numbers.Count; i++)
9        sum += numbers[i];
10    return sum / numbers.Count;
11}
12
13// Works with List<T>, arrays, ReadOnlyCollection<T>, etc.
14var list = new List<int> { 1, 2, 3 };
15var array = new int[] { 4, 5, 6 };
16Average(list);   // Works
17Average(array);  // Works — arrays also implement IReadOnlyList<T>

2. Covariance

IReadOnlyList<T> is covariant (out T), while IList<T> is invariant. This means:

csharp
1IReadOnlyList<object> objects = new List<string> { "hello", "world" };
2// This works because IReadOnlyList<out T> is covariant
3
4// IList<object> objects = new List<string>(); // DOES NOT COMPILE
5// IList<T> is invariant because it has both get and set operations

Covariance is only safe for read-only interfaces because there is no risk of inserting an incompatible type.

3. Liskov Substitution Principle

List<T> legitimately satisfies the IReadOnlyList<T> contract — it does support indexed access and count. The interface says "I can be read," not "I cannot be written." A mutable list is a superset of a read-only list's capabilities, so implementing it is correct.

Read-Only vs Immutable

This distinction is critical:

ConceptMeaning.NET Type
Read-onlyConsumer cannot modify through this interfaceIReadOnlyList<T>
ImmutableNobody can modify, everImmutableList<T> (System.Collections.Immutable)
csharp
1var list = new List<int> { 1, 2, 3 };
2
3// Read-only view — list can still be modified via the original reference
4IReadOnlyList<int> readOnly = list;
5// readOnly[0] = 99;  // Compile error — no setter
6// readOnly.Add(4);   // Compile error — no Add method
7
8list.Add(4);          // This still works!
9Console.WriteLine(readOnly.Count); // 4 — the change is visible
10
11// True immutability
12var immutable = ImmutableList.Create(1, 2, 3);
13var newList = immutable.Add(4);  // Returns a NEW list
14Console.WriteLine(immutable.Count);  // 3 — unchanged
15Console.WriteLine(newList.Count);    // 4

Practical Usage Patterns

Expose Read-Only, Keep Mutable Internally

csharp
1public class UserRepository
2{
3    private readonly List<User> _users = new();
4
5    // Expose as read-only — callers cannot cast back and modify safely
6    public IReadOnlyList<User> Users => _users;
7
8    public void AddUser(User user) => _users.Add(user);
9}

Prefer IReadOnlyList<T> in Method Parameters

csharp
1// GOOD: Communicates that this method only reads the collection
2public void PrintItems(IReadOnlyList<string> items)
3{
4    for (int i = 0; i < items.Count; i++)
5        Console.WriteLine($"{i}: {items[i]}");
6}
7
8// LESS GOOD: Accepts List<T> but only reads — misleading API
9public void PrintItems(List<string> items) { ... }

AsReadOnly() for Defensive Copies

If you need to prevent casting back to List<T>:

csharp
1private readonly List<int> _data = new() { 1, 2, 3 };
2
3// Callers could cast IReadOnlyList<int> back to List<int>
4public IReadOnlyList<int> UnsafeExpose => _data;
5
6// ReadOnlyCollection<T> wraps the list — no cast-back possible
7public IReadOnlyList<int> SafeExpose => _data.AsReadOnly();

Other Types That Implement IReadOnlyList<T>

TypeSince
List<T>.NET 4.5
T[] (arrays).NET 4.5
ReadOnlyCollection<T>.NET 4.5
ImmutableList<T>NuGet package
ImmutableArray<T>NuGet package

Common Pitfalls

  • Assuming IReadOnlyList<T> means immutable: A List<T> behind an IReadOnlyList<T> reference can still be mutated through the original reference. For true immutability, use ImmutableList<T> from System.Collections.Immutable.
  • Casting back to List<T>: (List<int>)readOnlyRef works at runtime if the underlying object is a List<T>. Use .AsReadOnly() to create a wrapper that cannot be cast back.
  • Thread safety: IReadOnlyList<T> does not imply thread safety. A List<T> exposed as IReadOnlyList<T> can still be modified concurrently by another thread through the mutable reference.
  • Performance of AsReadOnly(): ReadOnlyCollection<T> is a thin wrapper with no memory overhead for the elements. The only cost is one extra object allocation.
  • IEnumerable<T> vs IReadOnlyList<T>: Prefer IReadOnlyList<T> over IEnumerable<T> when you need indexed access or Count without multiple enumeration. IEnumerable<T> may be lazily evaluated and cannot be indexed.

Summary

  • IReadOnlyList<T> is a read-only view, not an immutability guarantee
  • List<T> implements it because it legitimately supports read access by index
  • Use IReadOnlyList<T> in method parameters to communicate read-only intent
  • Use covariance: IReadOnlyList<object> = new List<string>() works because out T
  • For true immutability, use ImmutableList<T> from System.Collections.Immutable
  • Use .AsReadOnly() to prevent consumers from casting back to the mutable type

Course illustration
Course illustration

All Rights Reserved.