Generics: Constraints, Variance & Advanced Use Cases
Lesson 20 โข Advanced Track
What You'll Learn
- Apply type constraints (where T : struct, class, new(), interface) to generic types
- Build generic interfaces like IRepository<T> for reusable data patterns
- Understand covariance (out T) โ why IEnumerable<Dog> can be IEnumerable<Animal>
- Understand contravariance (in T) โ why Action<Animal> can be Action<Dog>
- Design generic classes and methods that are type-safe and flexible
- Apply generics to real-world patterns like the Repository pattern
๐ก Real-World Analogy
Constraints are like job requirements โ "this position requires someone who can drive (IComparable) and has a clean record (struct)." Covariance is like a delivery service: if you can deliver German Shepherds, you can fill an order for "deliver any dog." Contravariance is the opposite: if a vet treats all animals, they can certainly treat dogs โ the more general skill applies to specific cases.
๐ Constraint Quick Reference
| Constraint | Meaning | Example |
|---|---|---|
| where T : struct | Value type only | int, double, DateTime |
| where T : class | Reference type only | string, List, custom classes |
| where T : new() | Has parameterless constructor | Can use new T() |
| where T : IComparable<T> | Implements interface | Can call CompareTo() |
| where T : BaseClass | Inherits from class | Guarantees base members |
| out T (covariant) | Can substitute derived | IEnumerable<out T> |
| in T (contravariant) | Can substitute base | Action<in T> |
1. Type Constraints
Constraints restrict what types can be used as generic arguments. Without constraints, T is treated as object โ you can't call CompareTo or use new T(). Add constraints to unlock specific capabilities while keeping type safety.
Generic Constraints
Apply struct, class, new(), and interface constraints to generic methods.
using System;
// Generic method with constraints
class DataProcessor
{
// where T : struct โ value types only (int, double, DateTime)
static T Max<T>(T a, T b) where T : struct, IComparable<T>
=> a.CompareTo(b) >= 0 ? a : b;
// where T : class โ reference types only
static void PrintIfNotNull<T>(T? item) where T : class
=> Console.WriteLine(item?.ToString() ?? "(null)");
// where T : new() โ must have a parameterless constructor
static T CreateAndLog<T>
...2. Generic Interfaces & Repository Pattern
Generic interfaces define contracts that work with any type. The Repository pattern is a classic example: IRepository<T> provides Add, GetById, and GetAll for any entity type. One implementation, infinite reuse.
Generic Repository Pattern
Build a reusable IRepository<T> interface with InMemoryRepository.
using System;
using System.Collections.Generic;
// Generic repository pattern
interface IRepository<T> where T : class
{
void Add(T entity);
T? GetById(int id);
IEnumerable<T> GetAll();
int Count { get; }
}
class InMemoryRepository<T> : IRepository<T> where T : class
{
private readonly List<T> _items = new();
private int _nextId = 1;
public void Add(T entity)
{
_items.Add(entity);
Console.WriteLine($" Added {typeof(T).Name} #{_nextId++}");
...3. Covariance & Contravariance
Covariance (out T) means a producer of Dogs can be treated as a producer of Animals. Contravariance (in T) means a consumer of Animals can be treated as a consumer of Dogs. These enable flexible type substitution while maintaining safety.
Covariance & Contravariance
Use out T and in T for flexible type substitution.
using System;
using System.Collections.Generic;
// Covariance (out T) โ can return T but not accept T as parameter
interface IProducer<out T>
{
T Produce();
}
// Contravariance (in T) โ can accept T but not return T
interface IConsumer<in T>
{
void Consume(T item);
}
class Animal { public virtual string Sound => "..."; }
class Dog : Animal { public override string Sound => "Woof!"; }
class Cat : Animal { public override string Sound => "Meow!"; }
class DogFactory : IProducer<Dog>
{
...Pro Tips
- ๐ก Prefer interface constraints over class constraints:
where T : IComparable<T>is more flexible thanwhere T : MyBaseClass. - ๐ก Remember the mnemonic:
out= output position (return types) = covariant.in= input position (parameters) = contravariant. - ๐ก Variance only works on interfaces and delegates: You can't make
List<Dog>covariant โ useIEnumerable<Dog>instead. - ๐ก Use multiple constraints:
where T : class, IDisposable, new()โ all constraints must be satisfied.
Common Mistakes
- List<Dog> to List<Animal>: This is NOT allowed โ
Listis invariant. If it were allowed, you could add a Cat to a Dog list. UseIEnumerable<Animal>instead. - Missing new() constraint:
new T()won't compile withoutwhere T : new(). The compiler can't guarantee T has a parameterless constructor. - Constraint order matters:
classorstructmust come first, then interfaces, thennew()last. - Over-constraining: Adding too many constraints makes the generic less reusable. Only constrain what you actually need.
๐ Lesson Complete
- โ
Constraints restrict generic types:
struct,class,new(), interfaces, base classes - โ
Generic interfaces like
IRepository<T>enable reusable patterns - โ
Covariance (
out T):IProducer<Dog>โIProducer<Animal> - โ
Contravariance (
in T):IConsumer<Animal>โIConsumer<Dog> - โ Variance only applies to interfaces and delegates, not classes
- โ Next lesson: Asynchronous Programming Internals
Sign up for free to track which lessons you've completed and get learning reminders.