Dataslope logoDataslope

Generic Abstractions

How `IEnumerable<T>` became a universal language for sequences — and how to write your own generic, type-safe pipeline helpers

LINQ is not "a library that works on lists". It's a library that works on any sequence — arrays, lists, dictionaries, file lines, network records, custom iterators — through a single generic interface: IEnumerable<T>.

This unification is what makes the same .Where(...).Select(...) work for everything from in-memory collections to streaming files. It is also a fantastic case study in how generics enable reusable, type-safe abstractions in any language.

The interface that unifies everything

IEnumerable<T> says one thing:

"I can give you a one-direction, one-element-at-a-time iterator over values of type T."

That's it. No indexing, no length, no mutation. The minimum necessary to walk a sequence.

Because so many data sources can satisfy that contract, almost everything in .NET implements it. Arrays. List<T>. HashSet<T>. Dictionary<K,V> (as key-value pairs). string (as char). File.ReadLines(...). Even your own iterator methods.

This is abstraction as superpower: when you write an algorithm against IEnumerable<T>, you've written it for all of those at once.

Generic operators, one definition

Watch how much one type parameter buys you.

Code Block
C# 13

One implementation. Three element types. Zero casts. Zero boxing. The C# compiler synthesizes the right concrete code at each call site.

Building a generic MyWhere

To really see how built-in LINQ works, let's reimplement Where.

Code Block
C# 13

Two short methods, generic over the element type, and they chain. This is essentially all built-in Where and Select do.

The real implementations add micro-optimizations (special cases for arrays/lists) but the shape is exactly this.

Constraints: making generics smarter

Sometimes you need to know something about T. C# generic constraints let you say so.

ConstraintMeaning
where T : structT is a value type
where T : classT is a reference type
where T : new()T has a public parameterless constructor
where T : IComparable<T>T can be ordered
where T : SomeBaseT inherits from SomeBase
where T : notnullT is non-nullable
Code Block
C# 13

The constraint where T : IComparable<T> is what allows CompareTo to be called. Without it, the compiler couldn't guarantee that T supports comparison.

Generic delegates: the lambda type system

LINQ's operator signatures use these tiny, ubiquitous generic delegate types:

DelegateShapeUsed for
Func<T, TResult>T → TResultselectors, projections
Func<T, bool>T → boolpredicates
Func<T1, T2, TResult>(T1, T2) → TResultcombining two values
Action<T>T → voidside-effecting callbacks

Every LINQ method's signature you'll ever read is some combination of these. Master them and the entire API surface becomes self-documenting.

Building a generic key-grouped histogram

A satisfying use of constraints + generics — count by an arbitrary key extracted from any sequence.

Challenge
C# 13

Take a moment to appreciate that the same Histogram<T, TKey> method ran with T = string, TKey = int and T = int, TKey = string without modification. That's the payoff of writing against the generic abstraction.

Why this matters

Generic abstractions are one of the great inventions of strongly typed languages. They let you write each piece of logic once, preserve type safety at every call site, and grow a library that scales as the program grows.

LINQ is the showcase: dozens of operators, each generic, each working on every sequence type, every projection type — all from a single ten-line interface.

QuestionSelect one

When you call new[] { "a", "b", "c" }.MyWhere(s => s.Length == 1), where does the type T come from?

It defaults to object and gets cast back to string later.

The lambda parameter s is declared as string, which is how T is decided.

The compiler infers T = string from the source array's element type, then specializes MyWhere for string.

T is resolved at runtime by reflection.

On this page