C# Generics — Type Parameters, Constraints, Covariance, and Contravariance
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 ListIEnumerable<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
- What are the benefits of generics over using
objecttype? - What constraints can you place on a generic type parameter?
- Explain the difference between covariance and contravariance.
- Why would you use a generic method instead of a non-generic method?
- 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
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