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

Why does ListT implement IReadOnlyListT in .NET 4.5?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

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

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.