Modern C# · Lesson 21

Covariant read-only views

Read derived values through a base interface without promising writes.

How it works

Covariance allows certain generic interfaces that produce values to be used through a more general reference type. Every Dog is an Animal, so reading dogs through IReadOnlyList<Animal> is safe. The interface promises that an indexed read returns an Animal; the actual Dog satisfies that promise.

The assignment does not copy the list or create new animal objects. Both references view the same underlying collection. The first read still reaches Pip. The useful abstraction is the kind of operation the interface exposes: callers can retrieve items and count them, without receiving a method that inserts any arbitrary Animal.

If the same conversion were allowed for a fully writable List<Dog>, a caller could try to insert a different Animal that is not a Dog. The original list's type guarantee would then be broken. Restricting the available operations is what makes the read-only covariant interface relationship sensible.

Read-only access is not the same as an immutable snapshot. Another reference to the original List<Dog> could still add or remove dogs while the view exists. The Animal objects could also have mutable properties in a different design. Variance answers a typing question; it does not establish exclusive ownership or deep immutability.

Practice by storing the List<Dog> in a separate mutable variable before assigning the read-only view. Add a second dog through that variable and print the count through IReadOnlyList<Animal>. Explain why the count changes even though the read-only interface itself offers no Add method.

Then write a function that accepts IEnumerable<Animal> and returns the names. Call it with the dog collection without building a new list of base objects. This demonstrates a useful API design: accept the weakest read capability the calculation needs, while keeping decisions about copying and concurrent mutation explicit elsewhere.

A working example

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;

IReadOnlyList<Dog> dogs = new List<Dog> { new Dog("Pip") };
IReadOnlyList<Animal> animals = dogs;
Console.WriteLine(animals[0].Name);
class Animal { public string Name { get; } public Animal(string name) { Name = name; } }
class Dog : Animal { public Dog(string name) : base(name) { } }

Expected output

Pip
Try this
A read-only interface can safely expose Dog items as Animal items.