100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
C#

Collection Interfaces

Survey the layered hierarchy of IEnumerable, ICollection, IList, and their read-only and generic variants that unify how .NET collections are consumed.

Collections & GenericsIntermediate9 min readJul 9, 2026
Analogies

Collection Interfaces

.NET's collection types — List<T>, Dictionary<TKey,TValue>, arrays, HashSet<T>, and many others — all implement a shared hierarchy of interfaces that describe increasing levels of capability. Understanding this hierarchy matters because it tells you what a method parameter actually needs: a method that only iterates its input should accept IEnumerable<T>, not List<T>, so it can work with arrays, LINQ query results, database cursors, or any other sequence without forcing callers to materialize a concrete list first. Programming against the narrowest interface that satisfies your needs is one of the most impactful habits for writing flexible, reusable C# APIs.

🏏

Cricket analogy: A coach writing a fielding drill should ask for 'anyone who can catch' (IEnumerable<T>) rather than demanding a specific XI like the Mumbai Indians squad list, so the drill works whether the players come from a training group or a full team roster.

The core hierarchy

IEnumerable<T> sits at the root: it exposes a single method, GetEnumerator(), and represents anything that can be iterated once, forward-only, via foreach. ICollection<T> extends it with Count, Add, Remove, Contains, and Clear — a modifiable collection of known size but no positional/indexed access. IList<T> extends ICollection<T> further with indexed access (this[int index]) and positional Insert/RemoveAt. IDictionary<TKey,TValue> is a parallel branch representing keyed access, extending ICollection<KeyValuePair<TKey,TValue>> with this[TKey], Keys, Values, and TryGetValue.

🏏

Cricket analogy: GetEnumerator is like an umpire simply signaling 'next ball' one delivery at a time, while a full scorecard with Add and Remove capability, like adjusting a squad list before a match, needs the richer ICollection powers; IList adds numbered batting positions you can insert into directly.

csharp
// Accepting the narrowest interface that satisfies the method's needs.
static double Average(IEnumerable<double> readings)
{
    double sum = 0;
    int count = 0;
    foreach (var reading in readings)
    {
        sum += reading;
        count++;
    }
    return count == 0 ? 0 : sum / count;
}

static void AddDefaultEntries(ICollection<string> destination)
{
    destination.Add("pending");
    destination.Add("active");
}

// Works with an array, a List<T>, a HashSet<T> projection — anything enumerable.
var fromArray = Average(new[] { 3.5, 4.0, 2.75 });
var fromList = Average(new List<double> { 10.0, 12.5 });

var tags = new List<string>();
AddDefaultEntries(tags); // List<T> satisfies ICollection<T>

// IReadOnlyList<T> exposes indexed read access without mutation methods.
IReadOnlyList<string> snapshot = tags.AsReadOnly();
Console.WriteLine(snapshot[0]);

Read-only interfaces

Alongside the mutable hierarchy, .NET defines IReadOnlyCollection<T>, IReadOnlyList<T>, and IReadOnlyDictionary<TKey,TValue>, which expose only non-mutating members (Count, indexers, enumeration) with no Add/Remove. These are the correct return type for a public API that wants to expose a collection for reading without letting callers mutate the underlying storage — a much safer choice than returning a concrete List<T> or even ICollection<T>, both of which expose mutation methods that callers could invoke against your internal state.

🏏

Cricket analogy: Handing a journalist an IReadOnlyList<T> of the scorecard is like giving them a printed sheet of runs and overs to report on without letting them scribble changes into the official team ledger itself.

IEnumerable<T> inherits from the non-generic IEnumerable, and similarly ICollection<T>/IList<T> inherit from their non-generic .NET Framework 1.x predecessors. This dual hierarchy exists purely for backward compatibility from before generics existed in C# 2.0 — in modern code you should essentially always use the generic versions, since they avoid boxing for value types and provide compile-time type checking.

Calling .Count() (the LINQ extension method) on a type that already implements ICollection<T>, such as List<T>, is a subtle inefficiency trap if done carelessly through a variable typed as plain IEnumerable<T> — LINQ's Count() actually checks for ICollection<T>/ICollection at runtime and uses the O(1) Count property when available, so it's usually fine, but it's still clearer and marginally faster to use the .Count property directly whenever the static type already guarantees ICollection<T> or IReadOnlyCollection<T>.

Choosing interfaces for method signatures

A practical rule of thumb: accept the least capable interface that lets your method do its job (often IEnumerable<T> for read-only iteration, or ICollection<T> if you need to know the count or add items), and return the most capable and informative type reasonable for the caller — often a concrete type like List<T> or T[] from an internal implementation, or a read-only interface like IReadOnlyList<T> from a public API that wants to hide mutability. This asymmetry — liberal in what you accept, precise in what you return — keeps APIs both flexible and safe.

🏏

Cricket analogy: A net-bowling coach should be happy to bowl to 'any batter handed to him' but should hand back a full, specific report on that batter's technique, not just a vague 'they can bat'; accept broadly, return precisely.

  • IEnumerable<T> is the root interface, exposing only forward-only iteration via GetEnumerator().
  • ICollection<T> adds Count, Add, Remove, Contains, Clear; IList<T> further adds indexed access and positional Insert/RemoveAt.
  • IDictionary<TKey,TValue> parallels this hierarchy for keyed access, extending ICollection<KeyValuePair<TKey,TValue>>.
  • IReadOnlyList<T>/IReadOnlyCollection<T>/IReadOnlyDictionary expose read-only views with no mutation members, ideal for public API return types.
  • Accept the narrowest interface a method needs as a parameter type; return the most useful/precise type for callers.
  • Non-generic collection interfaces (IEnumerable, ICollection, IList) predate generics and should generally be avoided in modern code.

Practice what you learned

Was this page helpful?

Topics covered

#CStudyNotes#Programming#CollectionInterfaces#Collection#Interfaces#Core#Hierarchy#StudyNotes#SkillVeris#ExamPrep