Site

Generics — C# Generic Programming

Tutorial 9.0  •  C# / Learn

9.0 What This Teaches

This tutorial covers C# generic programming: C++ templates are expanded at compile time per type; C# generics are compiled once with type erasure for reference types and specialized for value types. Both provide type-safe code reuse without duplication.

9.1 Generic Methods

A generic method declares one or more type parameters in angle brackets. The compiler infers the type from the arguments in most cases:
static T Identity<T>(T value) => value;
static void Swap<T>(ref T a, ref T b) { T tmp = a; a = b; b = tmp; }
static T[] Repeat<T>(T item, int count) => Enumerable.Repeat(item, count).ToArray();

Console.WriteLine(Identity(42));       // type inferred as int
Console.WriteLine(Identity("hello"));  // type inferred as string

int x = 3, y = 7;
Swap(ref x, ref y);
Console.WriteLine($"{x} {y}");  // 7 3

var arr = Repeat("ok", 3);
Console.WriteLine(string.Join(", ", arr));  // ok, ok, ok

9.2 Generic Classes

A generic class carries one or more type parameters that apply to fields, methods, and properties throughout the class body:
class Box<T>
{
    public T Value { get; set; }
    public Box(T value) { Value = value; }
    public override string ToString() => $"Box<{typeof(T).Name}>({Value})";
}

class Pair<TFirst, TSecond>
{
    public TFirst First { get; }
    public TSecond Second { get; }
    public Pair(TFirst first, TSecond second) { First = first; Second = second; }
}

var intBox = new Box<int>(42);
var strBox = new Box<string>("hello");
Console.WriteLine(intBox);   // Box<Int32>(42)
Console.WriteLine(strBox);   // Box<String>(hello)

var pair = new Pair<string, int>("age", 30);
Console.WriteLine($"{pair.First}: {pair.Second}");

9.3 Type Constraints

Constraints limit which types can be substituted for T and unlock type-specific operations inside the generic body:
static T Max<T>(T a, T b) where T : IComparable<T>
    => a.CompareTo(b) >= 0 ? a : b;

static T CreateNew<T>() where T : new()
    => new T();

Console.WriteLine(Max(3, 7));        // 7
Console.WriteLine(Max("apple", "fig"));  // fig
Common constraints:
ConstraintMeaning
where T : classT must be a reference type
where T : structT must be a value type
where T : new()T must have a public parameterless constructor
where T : BaseClassT must inherit from BaseClass
where T : IInterfaceT must implement IInterface

9.4 Generic Interfaces

interface IRepository<T, TId>
{
    T? GetById(TId id);
    void Save(T item);
    IEnumerable<T> GetAll();
}

class InMemoryRepo<T, TId> : IRepository<T, TId>
{
    private readonly Dictionary<TId, T> _store = new();

    public T? GetById(TId id) =>
        _store.TryGetValue(id!, out var v) ? v : default;

    public void Save(T item) { /* simplified */ }
    public IEnumerable<T> GetAll() => _store.Values;
}
Generic interfaces let you write algorithms and data structures that work with any type satisfying the contract, without losing type information.

9.5 Common Generic Types from the BCL

TypePurpose
List<T>Dynamic array
Dictionary<TKey, TValue>Hash map
HashSet<T>Unordered set; no duplicates
Stack<T>LIFO collection
Queue<T>FIFO collection
Nullable<T> / T?Value type that can be null
Task<T>Async operation returning T
IEnumerable<T>Sequence that can be iterated
Func<T, TResult>Delegate taking T, returning TResult
Action<T>Delegate taking T, returning void

9.6 Example - All Together

// Generics - generic stack with constrained Peek helper.

var stack = new TypedStack<int>();
stack.Push(1); stack.Push(2); stack.Push(3);
Console.WriteLine(stack.Pop());    // 3
Console.WriteLine(stack.Peek());   // 2
Console.WriteLine(stack.Count);    // 2

Console.WriteLine(MaxOf(stack));   // 2 (max of remaining items)

static T MaxOf<T>(IEnumerable<T> items) where T : IComparable<T>
{
    T? best = default;
    foreach (var item in items)
        if (best is null || item.CompareTo(best) > 0)
            best = item;
    return best!;
}

class TypedStack<T>
{
    private readonly List<T> _data = new();
    public int Count => _data.Count;
    public void Push(T item) => _data.Add(item);
    public T Pop()  { var v = _data[^1]; _data.RemoveAt(_data.Count - 1); return v; }
    public T Peek() => _data[^1];
    public IEnumerator<T> GetEnumerator() => _data.GetEnumerator();
}
Expected output:
3
2
2
2

9.7 Exercise

Exercise
  • Write a generic Clamp<T> method with a where T : IComparable<T> constraint. Test it with int, double, and char.
  • Write a generic Result<T> class with IsSuccess, Value, and ErrorMessage properties. Return it from a Divide(double a, double b) method that fails on division by zero.
  • Use List<T> and a generic Filter<T> method accepting a Func<T, bool> predicate to filter a list of integers to only even values.

9.8 Common Mistakes

Calling type-specific operations without a constraint

static T Add<T>(T a, T b) => a + b;  // compile error: + not defined for T
// Fix: use a constraint or the INumber<T> interface (.NET 7+)

Using object instead of a generic

Returning or accepting object loses type information and requires casting. A generic method keeps the type through the call chain without boxing value types.

Not specifying the type parameter when it can't be inferred

var result = CreateNew();          // error: cannot infer T
var result = CreateNew<MyClass>(); // correct: specify explicitly

9.9 Key Terms

TermMeaning
genericType or method parameterized by one or more type arguments
type parameterPlaceholder (T, TKey, TValue) filled in at the call or construction site
type argumentThe actual type supplied for a type parameter: List<int> → int
constraint (where)Restricts which types are valid for a type parameter
type inferenceCompiler deducing type arguments from context, avoiding explicit brackets
open typeA generic type with unspecified type parameters: List<T>
closed typeA generic type with all type parameters specified: List<int>
BCLBase Class Library - the standard .NET collection of generic types