Generics
Type parameters, constraints, and generic collections
Introduction
In this lesson, you'll learn about generics in C#. Coming from Python, you already have a foundation for understanding this concept. We'll build on that knowledge while highlighting the key differences.
In Python, you're familiar with type parameters, constraints, and generic collections.
C# has its own approach to type parameters, constraints, and generic collections, which we'll explore step by step.
The C# Way
Let's see how C# handles this concept. Here's a typical example:
// Generic method
T First<T>(IList<T> items) => items[0];
// Generic class
public class Stack<T>
{
private List<T> _items = new();
public void Push(T item) => _items.Add(item);
public T Pop()
{
var v = _items[^1];
_items.RemoveAt(_items.Count - 1);
return v;
}
public int Count => _items.Count;
}
// Constraints
T Max<T>(T a, T b) where T : IComparable<T>
=> a.CompareTo(b) >= 0 ? a : b;
// Multiple constraints
void Save<T>(T entity)
where T : class, IEntity, new()
{ }
// Generic interface
public interface IRepository<T> where T : class
{
T? GetById(int id);
void Save(T entity);
IEnumerable<T> GetAll();
}
// Usage
var s = new Stack<int>();
s.Push(1);
Console.WriteLine(s.Pop()); // 1
Max(3, 7); // 7Comparing to Python
Here's how you might have written similar code in Python:
from typing import TypeVar, Generic, List
T = TypeVar("T")
# Generic function
def first(items: List[T]) -> T:
return items[0]
# Generic class
class Stack(Generic[T]):
def __init__(self) -> None:
self._items: List[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
# Bounded TypeVar
from typing import SupportsFloat
N = TypeVar("N", bound=SupportsFloat)
def total(items: List[N]) -> float:
return sum(float(x) for x in items)
s: Stack[int] = Stack()
s.push(1)
print(s.pop()) # 1You may be used to different syntax or behavior.
C# generics are reified — typeof(T) works at runtime (unlike Java's type erasure)
You may be used to different syntax or behavior.
Constraints use 'where T : ...' — similar to Python's TypeVar(bound=...)
You may be used to different syntax or behavior.
where T : class = reference type; where T : struct = value type
You may be used to different syntax or behavior.
where T : new() requires parameterless constructor — Python has no equivalent
You may be used to different syntax or behavior.
C# infers T from method arguments — rarely need to specify explicitly
Step-by-Step Breakdown
1. Generic Methods
Declare <T> after the method name. C# infers T from arguments automatically in most cases.
T = TypeVar("T")
def wrap(v: T) -> dict[str, T]:
return {"value": v}Dictionary<string,T> Wrap<T>(T v)
=> new() { ["value"] = v };
// Caller: Wrap("hello") — T inferred as string2. Generic Classes
Generic classes are parameterized at instantiation. C# generics are reified — the runtime knows the actual type T.
class Box(Generic[T]):
def __init__(self, v: T): self.value = vclass Box<T> {
public T Value { get; }
public Box(T v) => Value = v;
}
var b = new Box<string>("hi");
Console.WriteLine(b.Value.GetType()); // String3. Type Constraints
Constraints let you call interface methods on T. 'where T : IComparable<T>' allows using CompareTo().
N = TypeVar("N", bound=SupportsFloat)
def sum_all(items: list[N]) -> float: ...double SumAll<T>(IList<T> items)
where T : IConvertible
=> items.Sum(x => x.ToDouble(null));4. Generic Interfaces
Generic interfaces define contracts that work for any type. IRepository<T> is a common pattern for data access.
from typing import Protocol
class Repository(Protocol[T]):
def get(self, id: int) -> T: ...interface IRepo<T> where T : class {
T? GetById(int id);
void Save(T entity);
IEnumerable<T> GetAll();
}Common Mistakes
When coming from Python, developers often make these mistakes:
- C# generics are reified — typeof(T) works at runtime (unlike Java's type erasure)
- Constraints use 'where T : ...' — similar to Python's TypeVar(bound=...)
- where T : class = reference type; where T : struct = value type
Key Takeaways
- Generic methods: T Method<T>(T arg); classes: class Foo<T>
- Constraints (where): IComparable<T>, class, struct, new() enable operations on T
- C# generics are reified — typeof(T) and GetType() work at runtime
- Generic interfaces (IRepository<T>) are the foundation of clean architecture patterns