IReadOnly

3 min read
Mid-level2 min read
Rapid overview

IReadOnly

TL;DR

IReadOnlyCollection<T> and IReadOnlyList<T> expose Count (and indexed access in the latter) without surfacing Add, Remove, or Clear, so callers can read and iterate but can't mutate your internal state. List<T> already implements both interfaces, so the cast is free β€” no defensive .ToList() copy needed. They don't guarantee true immutability though: wrap with ReadOnlyCollection<T> or use ImmutableList<T> when the backing store must never change.

How it works

IReadOnlyCollection & IReadOnlyList β€” Immutable Access

"Use IReadOnly* interfaces to expose collections without allowing modification."

❌ Bad example:

public class Portfolio
{
    private List<Position> _positions = new();

    public List<Position> Positions => _positions; // exposes internal list
}

// Caller can mutate internal state
var portfolio = new Portfolio();
portfolio.Positions.Add(new Position()); // breaks encapsulation
portfolio.Positions.Clear(); // disaster!

Exposing mutable collections lets callers break invariants and bypass validation.

βœ… Good example:

public class Portfolio
{
    private List<Position> _positions = new();

    public IReadOnlyCollection<Position> Positions => _positions;

    public void AddPosition(Position position)
    {
        ValidatePosition(position);
        _positions.Add(position);
    }
}

// Caller can only read
var portfolio = new Portfolio();
int count = portfolio.Positions.Count; // βœ… allowed
// portfolio.Positions.Add(...); // ❌ compiler error

πŸ‘‰ IReadOnlyCollection provides Count and iteration, but no Add/Remove.

πŸ”₯ Using IReadOnlyList for indexed access:

public class TradingDay
{
    private List<Trade> _trades = new();

    public IReadOnlyList<Trade> Trades => _trades;

    public void RecordTrade(Trade trade)
    {
        _trades.Add(trade);
    }
}

// Caller can access by index, but not modify
var day = new TradingDay();
var firstTrade = day.Trades[0]; // βœ… indexed access
int count = day.Trades.Count;   // βœ… count
// day.Trades.Add(...);          // ❌ compiler error

πŸ‘‰ IReadOnlyList adds indexed access without exposing mutability.

πŸ”₯ Avoiding defensive copies:

// ❌ Bad: creates unnecessary copy
public IEnumerable<Order> GetOrders()
{
    return _orders.ToList(); // allocates new list every call
}

// βœ… Good: exposes read-only view without copying
public IReadOnlyCollection<Order> GetOrders()
{
    return _orders; // no allocation, just interface cast
}

πŸ‘‰ List implements IReadOnlyCollection, so casting is free.

πŸ’‘ In trading systems:

  • Expose position snapshots as IReadOnlyCollection to prevent accidental modifications.
  • Return IReadOnlyList for price history where indexed access is useful.
  • Prevent invariant violations by hiding Add/Remove while keeping data accessible.

Quick recall Q&A

Q: What's the difference between IReadOnlyCollection and IReadOnlyList? A: IReadOnlyList extends IReadOnlyCollection and adds indexed access (this[int]). Use IReadOnlyList when callers need random access without mutation.
Q: Does IReadOnly* guarantee immutability? A: No. It prevents modification through the interface, but underlying data can still change. If the backing List is modified, IReadOnlyCollection reflects changes. Use ReadOnlyCollection for true immutability.
Q: Can I cast List to IReadOnlyCollection? A: Yes. List implements IReadOnlyCollection and IReadOnlyList. Casting is a zero-cost abstractionβ€”no allocation or copying.
Q: What's ReadOnlyCollection vs IReadOnlyCollection? A: ReadOnlyCollection is a wrapper class that prevents modification entirely, even if you have the underlying list. IReadOnlyCollection is an interface; if you cast back to List, you can mutate.
Q: Should I return IReadOnlyList instead of IEnumerable? A: If Count and indexed access are useful to callers and data is already materialized (not lazy), yes. IReadOnlyList is more informative without exposing mutability.
Q: How do I create a truly immutable collection? A: Use ImmutableList<T> from System.Collections.Immutable. Modifications return new instances. Or wrap with new ReadOnlyCollection<T>(list).
Q: Can I expose arrays as IReadOnlyList? A: Yes, arrays implement IReadOnlyList. But callers can cast back to T[] and mutate. Use Array.AsReadOnly(array) for true protection.
Q: What's the performance of IReadOnlyList vs List? A: Identical. IReadOnlyList is just an interface restriction. Indexing and iteration have the same performance as the underlying collection.
Q: How do I mock IReadOnlyCollection in tests? A: Use arrays or List directly. Most mocking frameworks can stub IReadOnlyCollection, but real collections are often simpler in tests.
Q: When should I use IReadOnlyCollection over IEnumerable? A: When Count is useful to callers and data is already materialized. IEnumerable is better for lazy sequences or when hiding collection semantics.

See also