Skip to content

C# Generics — Type Parameters, Constraints, Covariance, and Contravariance

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about C# Generics. We cover key concepts, practical examples, and best practices to help you master this topic.

C# generics allow classes, methods, and interfaces to work with any data type while maintaining type safety, eliminating the need for boxing and runtime type checks.

What You'll Learn

You will master generics in C#: generic classes and methods with type parameters, constraints that restrict allowed types, covariance and contravariance for type compatibility, generic interfaces and delegates, and how .NET implements generics with efficient code generation.

Why It Matters

Generics are the foundation of reusable, type-safe code in C#. Before generics, collections like ArrayList stored object references, requiring casting and enabling runtime type errors. Generic List eliminates these problems and avoids boxing overhead for value types. Modern .NET frameworks use generics everywhere: IEnumerable<T>, Task<T>, Func<T>, ILogger<T>.

Real-World Use

ASP.NET Core uses generic ILogger<T> for typed logging. Entity Framework Core provides generic DbSet<T> for entity access. Repository patterns use IRepository<T> for type-safe data access. Dependency injection uses generic IService<T>. LINQ is built on generic IEnumerable<T>.

Learning Path

graph LR
    A["18: Arrays & Collections"] --> B["19: Generics"]
    B --> C["20: Exception Handling"]
    C --> D["21: LINQ"]
    D --> E["22: LINQ Advanced"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style E fill:#4a90d9,stroke:#2c5f8a,color:#fff

Generic Class

public class Box<T>
{
    private T _value;

    public Box(T value)
    {
        _value = value;
    }

    public T GetValue() => _value;
    public void SetValue(T value) => _value = value;

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

// Usage: type inference
var intBox = new Box<int>(42);
var stringBox = new Box<string>("Hello");
var dateBox = new Box<DateTime>(DateTime.UtcNow);

Console.WriteLine(intBox.GetValue());    // 42
Console.WriteLine(stringBox.GetValue()); // Hello

Generic Method

public static class Utils
{
    public static T Max<T>(T a, T b) where T : IComparable<T>
    {
        return a.CompareTo(b) > 0 ? a : b;
    }

    public static void Swap<T>(ref T a, ref T b)
    {
        (a, b) = (b, a);
    }

    public static T? DefaultIfNull<T>(T? value, T defaultValue) where T : struct
    {
        return value ?? defaultValue;
    }
}

int max = Utils.Max(5, 10);  // 10
string maxStr = Utils.Max("apple", "banana");  // "banana"

int x = 1, y = 2;
Utils.Swap(ref x, ref y);
Console.WriteLine($"x={x}, y={y}");  // x=2, y=1

Generic Constraints

// where T : constraint1, constraint2, ...

// Type must be a reference type
public class RefOnly<T> where T : class { }

// Type must be a value type
public class ValOnly<T> where T : struct { }

// Type must have a parameterless constructor
public class Creatable<T> where T : new() { }

// Type must implement an interface
public class Repository<T> where T : IEntity { }

// Type must inherit from a base class
public class EntityHandler<T> where T : Entity { }

// Type must be or inherit from another type parameter
public class Mapper<TInput, TOutput>
    where TOutput : TInput { }

public interface IEntity
{
    int Id { get; }
}

public class Repository<T> where T : IEntity
{
    private readonly Dictionary<int, T> _items = new();

    public void Add(T item) => _items[item.Id] = item;
    public T? GetById(int id) =>
        _items.TryGetValue(id, out var item) ? item : default;
}

Default Values with Generics

public T? GetDefault<T>()
{
    return default(T);  // null for ref types, 0 for value types
}

Console.WriteLine(GetDefault<int>());       // 0
Console.WriteLine(GetDefault<string>());    // (null)
Console.WriteLine(GetDefault<bool>());      // False

Covariance (out)

Covariance allows a generic type to use a more derived type than specified. Use out for return types:

// Covariant interface: T only appears in output positions
public interface IProducer<out T>
{
    T Produce();
}

public class StringProducer : IProducer<string>
{
    public string Produce() => "Hello";
}

// Covariance: IProducer<string> can be used as IProducer<object>
IProducer<string> stringProd = new StringProducer();
IProducer<object> objectProd = stringProd;  // Covariant conversion

Console.WriteLine(objectProd.Produce());  // Hello

// IEnumerable<T> is covariant
IEnumerable<string> strings = new[] { "a", "b", "c" };
IEnumerable<object> objects = strings;

Contravariance (in)

Contravariance allows a generic type to use a less derived type than specified. Use in for input parameters:

// Contravariant interface: T only appears in input positions
public interface IConsumer<in T>
{
    void Consume(T item);
}

public class ConsoleConsumer : IConsumer<object>
{
    public void Consume(object item) =>
        Console.WriteLine($"Consumed: {item}");
}

// Contravariance: IConsumer<object> can be used as IConsumer<string>
IConsumer<object> objectConsumer = new ConsoleConsumer();
IConsumer<string> stringConsumer = objectConsumer;  // Contravariant conversion

stringConsumer.Consume("Hello");  // Consumed: Hello

// IComparer<T> is contravariant
IComparer<object> baseComparer = Comparer<object>.Default;
IComparer<string> stringComparer = baseComparer;  // Contravariant conversion

Generic Delegates

// Built-in generic delegates
Func<int, int, int> add = (a, b) => a + b;
Action<string> log = msg => Console.WriteLine(msg);
Predicate<int> isPositive = x => x > 0;

// Custom generic delegate
public delegate T Transformer<T>(T input);

Transformer<int> square = x => x * x;
Transformer<string> upper = s => s.ToUpper();

Console.WriteLine(square(5));    // 25
Console.WriteLine(upper("hi"));  // HI

Generic Collections

All of these are generic:

List<int> ints = new();
Dictionary<string, int> scores = new();
HashSet<double> values = new();
Queue<Task> tasks = new();
Stack<string> history = new();
LinkedList<int> linked = new();
SortedList<string, int> sorted = new();
SortedDictionary<string, int> sortedDict = new();

Generic Factory Patternory" >}} Pattern

public class Factory<T> where T : class, new()
{
    public T Create() => new T();

    public T CreateWith(Action<T> initializer)
    {
        var instance = new T();
        initializer(instance);
        return instance;
    }
}

var factory = new Factory<StringBuilder>();
var sb = factory.CreateWith(b => b.Append("Hello"));
Console.WriteLine(sb.ToString());  // Hello

Common Mistakes

Mistake 1: Not Using Constraints When Needed

Without constraints, the compiler restricts you to operations available on object. Use constraints to access members of specific types.

Mistake 2: Using new() Constraint with Parameters

The parameterless constructor constraint only works for parameterless constructors. For parameterized construction, use a factory or Activator.CreateInstance.

Mistake 3: Confusing Covariance and Contravariance

Covariance (out) is for output (return values). Contravariance (in) is for input (parameters). Mixing them up causes compilation errors.

Mistake 4: Boxing Value Types via Interface

When a generic type with a value type argument is assigned to a non-generic interface, boxing occurs. Prefer generic interfaces.

Mistake 5: Over-Generifying Code

Not everything needs to be generic. If a method only works with integers, use int, not T. Generics add complexity.

Mistake 6: Forgetting That Static Members Are Per-Closed-Type

List<int>.Empty and List<string>.Empty are different static fields. Each closed generic type has its own static state.

Practice Questions

  1. What are the benefits of generics over using object type?
  2. What constraints can you place on a generic type parameter?
  3. Explain the difference between covariance and contravariance.
  4. Why would you use a generic method instead of a non-generic method?
  5. Write a generic Stack<T> implementation with push, pop, and peek operations.

Challenge

Create a generic EventHandler<TEventArgs> pattern where TEventArgs inherits from EventArgs. Implement a simple event system demonstrating both event subscription and invocation.

FAQ

Are generics in C# the same as C++ templates?

No. C# generics are reified (preserve type info at runtime) and compiled once for all reference types. C++ templates are compile-time expanded per type. C# generics are more efficient but less powerful than templates.

Can I use nullable value types as generic type arguments?

Yes. T? where T : struct creates Nullable<T>. For reference types, T? uses nullable reference type annotations (C# 8+).

What is the difference between `typeof(T)` and `typeof(MyClass)`?

typeof(T) gets the runtime type of the type argument. typeof(MyClass<T>) gets the open generic type definition. They are different at runtime.

Can I create generic properties?

No. Properties cannot be generic in C#. Use generic methods instead: public T GetProperty<T>(string name).

Can I use enum types as generic constraints?

No direct enum constraint exists. Workaround: where T : struct, Enum (C# 7.3+). Similarly, use where T : Delegate for delegates.

Mini Project

Create a generic event bus:

public interface IEvent { }

public record UserRegistered(string Email, string Name) : IEvent;
public record OrderPlaced(int OrderId, decimal Total) : IEvent;

public class EventBus
{
    private readonly Dictionary<Type, List<Delegate>> _handlers = new();

    public void Subscribe<T>(Action<T> handler) where T : IEvent
    {
        var type = typeof(T);
        if (!_handlers.ContainsKey(type))
            _handlers[type] = new List<Delegate>();
        _handlers[type].Add(handler);
    }

    public void Publish<T>(T eventData) where T : IEvent
    {
        if (!_handlers.TryGetValue(typeof(T), out var handlers)) return;
        foreach (Action<T> handler in handlers)
            handler(eventData);
    }
}

var bus = new EventBus();

bus.Subscribe<UserRegistered>(e =>
    Console.WriteLine($"Email to {e.Email}: Welcome {e.Name}!"));

bus.Subscribe<OrderPlaced>(e =>
    Console.WriteLine($"Order #{e.OrderId} for ${e.Total} received"));

bus.Publish(new UserRegistered("alice@example.com", "Alice"));
bus.Publish(new OrderPlaced(1001, 59.99m));

Expected output:

Email to alice@example.com: Welcome Alice!
Order #1001 for $59.99 received

What's Next

You have mastered generics in C#. The next lesson covers Exception Handling: try/catch/finally, custom exceptions, and when filters.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro