IReadOnly
3 min readIReadOnly
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
π₯ 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
π₯ 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
π‘ 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
IReadOnlyList
No. It prevents modification through the interface, but underlying data can still change. If the backing List
Yes. List
ReadOnlyCollection
If Count and indexed access are useful to callers and data is already materialized (not lazy), yes. IReadOnlyList
Use ImmutableList<T> from System.Collections.Immutable. Modifications return new instances. Or wrap with new ReadOnlyCollection<T>(list).
Yes, arrays implement IReadOnlyListArray.AsReadOnly(array) for true protection.
Identical. IReadOnlyList
Use arrays or List
When Count is useful to callers and data is already materialized. IEnumerable