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
ImmutableList<T> from System.Collections.Immutable. Modifications return new instances. Or wrap with new ReadOnlyCollection<T>(list).Array.AsReadOnly(array) for true protection.