IReadOnlyList and IReadOnlyDictionary in Modern C
This article explores the purpose of
IReadOnlyList<T> and
IReadOnlyDictionary<TKey, TValue> in modern C# API
design. It covers how these interfaces establish clear contracts for
data consumption, distinguish read-only views from immutability, provide
index-based access without mutation risks, and improve API
maintainability compared to older collection interfaces.
Communicating Intent and Encapsulation
Before .NET 4.5, developers often relied on
IEnumerable<T> to expose collections safely or
IList<T> when index-based access was required.
IEnumerable<T> protects data from modification, but
it conceals basic metrics such as Count and forces
sequential enumeration or LINQ allocations (.ToList(),
.Count()) for index-based operations. Conversely,
IList<T> and
IDictionary<TKey, TValue> expose mutating methods
like Add, Remove, and Clear,
forcing developers either to implement runtime exceptions
(NotSupportedException) on read-only wrappers or expose
mutable internals.
IReadOnlyList<T> and
IReadOnlyDictionary<TKey, TValue> solve this problem
by defining an explicit contract: the consumer can read and index the
data, but cannot modify it through the interface. This provides strong
encapsulation without sacrificing structural visibility.
Key Capabilities
These interfaces provide the efficiency of direct access while omitting mutation APIs:
- Direct Lookup:
IReadOnlyList<T>exposes an indexer (this[int index]) and aCountproperty, enabling \(O(1)\) random access. - Key-Based Access:
IReadOnlyDictionary<TKey, TValue>providesContainsKey,TryGetValue,Keys,Values, and a key indexer (this[TKey key]), mirroring the lookup mechanics of standard dictionaries. - Covariance:
IReadOnlyList<T>is covariant (out T), meaning anIReadOnlyList<string>can be implicitly treated as anIReadOnlyList<object>. Standard mutable interfaces likeIList<T>cannot support covariance because allowing writes would violate type safety.
Read-Only View vs. True Immutability
A common misconception is that these interfaces guarantee immutable data. In reality, they represent a read-only view, not an immutable collection.
Standard mutable types like List<T>,
T[], and Dictionary<TKey, TValue>
implement these read-only interfaces directly. If a class exposes a
private List<T> as an
IReadOnlyList<T>, the caller cannot invoke
.Add() directly, but internal methods within the owning
class can still alter the underlying list. Consumers observing the
interface will see those updates. If an API requires guaranteed thread
safety or absolute immutability that cannot change under any
circumstances, types from the System.Collections.Immutable
namespace should be used instead.
Best Practices for Modern API Design
When designing modern C# APIs, collection choices should follow these guidelines:
- Return Types: Use
IReadOnlyList<T>orIReadOnlyDictionary<TKey, TValue>when returning in-memory collections that consumers need to index, count, or query by key, but should not modify. - Avoid Unnecessary Defensive Copies: Because standard collections implement these interfaces, you can return existing collections directly typed as read-only interfaces without allocating new arrays or wrapper classes.
- Parameter Types: Accept the least specific
interface needed. If a method only enumerates, accept
IEnumerable<T>. If it requires index access or count checks without altering data, acceptIReadOnlyList<T>. This allows callers to pass arrays, lists, or custom collections interchangeably.