Introduction to Generics
Generics enable you to define classes, interfaces, and methods with a placeholder for the type of data they store or manipulate. This provides type safety without sacrificing performance.
Why use Generics?
- Strongly‑typed collections
- Reduced code duplication
- Improved runtime performance (no boxing/unboxing for value types)
- Better tooling support (intellisense, refactoring)
Basic Syntax
public class List<T> : IList<T>
{
private T[] _items;
public void Add(T item) { /*...*/ }
public T this[int index] => _items[index];
}
Common Generic Types
List<T>– resizable arrayDictionary<TKey,TValue>– key/value mapNullable<T>– value types that can be nullIEnumerable<T>,IQueryable<T>– queryable sequences
Generic Type Definition
A generic type can declare one or more type parameters. These are listed in angle brackets after the type name.
public class Cache<TKey,TValue> where TKey : notnull
{
private readonly Dictionary<TKey,TValue> _store = new();
public void Set(TKey key, TValue value) => _store[key] = value;
public TValue Get(TKey key) => _store[key];
}
Generic Constraints
Constraints restrict the types that can be used as arguments for a generic parameter.
public class Repository<TEntity>
where TEntity : class, IEntity, new()
{
public TEntity Create() => new TEntity();
}
Read more about type constraints.
Variance
Variance allows you to use a more derived or less derived type than originally specified for generic interfaces and delegates.
// Covariant interface (out)
public interface IProducer<out T>
{
T Produce();
}
// Contravariant interface (in)
public interface IConsumer<in T>
{
void Consume(T item);
}
Read more about generic variance.
Best Practices
- Prefer generic collections over non‑generic ones.
- Use constraints to express the capabilities required by your code.
- Avoid over‑genericizing; keep APIs simple.
- Leverage variance when designing reusable libraries.