Skip to content
Happy Programming Guide
Start learning
C#

C# Generics

Generic classes, methods and constraints in C#, why the generic List beats ArrayList, and the variance keywords in and out explained plainly.

Colourful plastic building bricks on a table

Generics let you write one class or method that works with any type while keeping full compile-time checking. In C# they are everywhere — lists, dictionaries, tasks — so even if you never write one, you read them constantly. This guide covers writing your own, constraining them, and the variance keywords that confuse people.

The problem#

CSHARP
// Before generics
var list = new ArrayList();
list.Add("hello");
list.Add(42);                    // no complaint

string s = (string)list[1];      // compiles, throws at run time

Everything went in as object, so nothing was checked and every read needed a cast. Value types were also boxed onto the heap, which cost allocations.

CSHARP
var list = new List<string>();
list.Add("hello");
list.Add(42);                    // compile error - caught immediately

string s = list[0];              // no cast needed

A generic class#

CSHARP
public class Box<T>
{
    private T _value;

    public Box(T value) => _value = value;

    public T Get() => _value;
    public void Set(T value) => _value = value;

    public override string ToString() => $"Box({_value})";
}

var text = new Box<string>("hello");
var number = new Box<int>(42);

More than one type parameter is fine:

CSHARP
public class Pair<TFirst, TSecond>
{
    public TFirst First { get; }
    public TSecond Second { get; }

    public Pair(TFirst first, TSecond second)
    {
        First = first;
        Second = second;
    }

    public Pair<TSecond, TFirst> Swap() => new(Second, First);
}

The conventional names are T for a single type, and descriptive ones such as TKey, TValue or TResult when there are several.

Generic methods#

CSHARP
public static T[] Repeat<T>(T value, int count)
{
    var result = new T[count];
    for (int i = 0; i < count; i++) result[i] = value;
    return result;
}

var zeros = Repeat(0, 5);            // int[] - inferred
var blanks = Repeat("", 3);          // string[]
var explicitly = Repeat<double>(1, 3);

The type is inferred from the arguments, so you rarely write it out. You only need the explicit form when inference cannot decide — usually when the type appears only in the return value.

Constraints#

Without a constraint, T could be anything, so you can only use members that every type has:

CSHARP
public static T Largest<T>(IEnumerable<T> items) where T : IComparable<T>
{
    T best = items.First();
    foreach (T item in items)
        if (item.CompareTo(best) > 0) best = item;
    return best;
}

The full set:

Constraint Means
where T : class a reference type
where T : struct a value type, not nullable
where T : new() has a parameterless constructor
where T : SomeBase inherits from that class
where T : ISomething implements that interface
where T : notnull cannot be null
where T : U derives from another type parameter
CSHARP
public class Factory<T> where T : class, IEntity, new()
{
    public T Create() => new T();     // legal because of new()
}

Constraints are a promise to the compiler and a requirement on the caller. Add the minimum that lets your code compile — each one narrows who can use the type.

default(T)#

CSHARP
public static T FirstOrDefault<T>(IEnumerable<T> items)
{
    foreach (T item in items) return item;
    return default;        // null for classes, 0 for int, false for bool
}

Variance#

This is the part people find confusing, and it has a concrete reason behind it:

CSHARP
List<string> strings = new();
List<object> objects = strings;      // compile error

If that were allowed, you could then add an int through the second reference, and the list of strings would contain a non-string. The compiler prevents it.

But reading is safe. IEnumerable<T> only produces values, never accepts them, so this works:

CSHARP
IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings;      // fine

That is because it is declared IEnumerable<out T>. The keywords say which direction values flow:

  • out — the type only comes out, as return values. Safe to use a more general type.
  • in — the type only goes in, as parameters. Safe to use a more specific type.
CSHARP
public interface IReader<out T>
{
    T Read();                 // T only appears as a return type
}

public interface IWriter<in T>
{
    void Write(T value);      // T only appears as a parameter
}

A list does both, so it can be neither — which is exactly why the first example does not compile.

Generic collections#

CSHARP
List<T>                   // ordered, grows, index access
Dictionary<TKey, TValue>  // key lookup
HashSet<T>                // unique values, fast membership tests
Queue<T>                  // first in, first out
Stack<T>                  // last in, first out
IEnumerable<T>            // anything you can iterate
IReadOnlyList<T>          // read-only view

A useful habit: accept the most general type you can and return the most specific that is useful. A method taking IEnumerable<T> works with arrays, lists and LINQ results alike.

Static members are per constructed type#

CSHARP
public class Counter<T>
{
    public static int Count;
}

Counter<string>.Count = 5;
Counter<int>.Count = 10;

Console.WriteLine(Counter<string>.Count);   // 5 - separate storage

Each closed type gets its own static fields. Occasionally useful, occasionally surprising.

C# does not erase#

CSHARP
var list = new List<string>();
Console.WriteLine(list.GetType().Name);   // List`1

if (obj is List<string> strings) { }      // works - type is known at run time

Java removes generic type information after compiling; C# keeps it. That means reflection sees the real types, new T() is possible with a constraint, and value types are not boxed — a genuine performance difference.

Questions people ask#

Do generics slow things down?

The opposite, usually. The runtime generates specialised code for value types, which avoids the boxing that ArrayList required.

Can I constrain to a numeric type?

Yes, since C# 11: where T : INumber<T> lets you write arithmetic that works for any numeric type. Before that it needed awkward workarounds.

What is the difference between T and object?

object loses type information — you need casts and can put anything in. T preserves it, so the compiler checks every use and no casting is needed.

When should I write my own generic type?

When you have written the same class twice with only the type changed. Write the concrete version first; generalise when the second use appears.

Where to go next#

C# collections, and which to reach forRead next

Keep reading

C#

C# Testing

Writing unit tests in C# with xUnit: arrange-act-assert, parameterised tests, mocking dependencies, and what is actually worth testing.

4 min read

C#

C# Deployment

How to publish a .NET application: framework-dependent versus self-contained builds, single-file output, trimming, Docker and configuration for production.

4 min read

Keep going — pick your next guide

The fastest way to improve is to read one guide, then build the thing it describes. Start with the basics, or jump straight to a project.

Ask a question or share what worked

Your email address will not be published. Required fields are marked *