9.0 What This Teaches
- Generic methods and why they exist
- Generic classes and structs
- Type constraints:
where T : constraint - Generic interfaces
- Multiple type parameters
- Common generic types from the BCL
9.1 Generic Methods
static T Identity<T>(T value) => value;
static void Swap<T>(ref T a, ref T b) { T tmp = a; a = b; b = tmp; }
static T[] Repeat<T>(T item, int count) => Enumerable.Repeat(item, count).ToArray();
Console.WriteLine(Identity(42)); // type inferred as int
Console.WriteLine(Identity("hello")); // type inferred as string
int x = 3, y = 7;
Swap(ref x, ref y);
Console.WriteLine($"{x} {y}"); // 7 3
var arr = Repeat("ok", 3);
Console.WriteLine(string.Join(", ", arr)); // ok, ok, ok
9.2 Generic Classes
class Box<T>
{
public T Value { get; set; }
public Box(T value) { Value = value; }
public override string ToString() => $"Box<{typeof(T).Name}>({Value})";
}
class Pair<TFirst, TSecond>
{
public TFirst First { get; }
public TSecond Second { get; }
public Pair(TFirst first, TSecond second) { First = first; Second = second; }
}
var intBox = new Box<int>(42);
var strBox = new Box<string>("hello");
Console.WriteLine(intBox); // Box<Int32>(42)
Console.WriteLine(strBox); // Box<String>(hello)
var pair = new Pair<string, int>("age", 30);
Console.WriteLine($"{pair.First}: {pair.Second}");
9.3 Type Constraints
T and unlock
type-specific operations inside the generic body:
static T Max<T>(T a, T b) where T : IComparable<T>
=> a.CompareTo(b) >= 0 ? a : b;
static T CreateNew<T>() where T : new()
=> new T();
Console.WriteLine(Max(3, 7)); // 7
Console.WriteLine(Max("apple", "fig")); // fig
| Constraint | Meaning |
|---|---|
where T : class | T must be a reference type |
where T : struct | T must be a value type |
where T : new() | T must have a public parameterless constructor |
where T : BaseClass | T must inherit from BaseClass |
where T : IInterface | T must implement IInterface |
9.4 Generic Interfaces
interface IRepository<T, TId>
{
T? GetById(TId id);
void Save(T item);
IEnumerable<T> GetAll();
}
class InMemoryRepo<T, TId> : IRepository<T, TId>
{
private readonly Dictionary<TId, T> _store = new();
public T? GetById(TId id) =>
_store.TryGetValue(id!, out var v) ? v : default;
public void Save(T item) { /* simplified */ }
public IEnumerable<T> GetAll() => _store.Values;
}
9.5 Common Generic Types from the BCL
| Type | Purpose |
|---|---|
List<T> | Dynamic array |
Dictionary<TKey, TValue> | Hash map |
HashSet<T> | Unordered set; no duplicates |
Stack<T> | LIFO collection |
Queue<T> | FIFO collection |
Nullable<T> / T? | Value type that can be null |
Task<T> | Async operation returning T |
IEnumerable<T> | Sequence that can be iterated |
Func<T, TResult> | Delegate taking T, returning TResult |
Action<T> | Delegate taking T, returning void |
9.6 Example - All Together
// Generics - generic stack with constrained Peek helper.
var stack = new TypedStack<int>();
stack.Push(1); stack.Push(2); stack.Push(3);
Console.WriteLine(stack.Pop()); // 3
Console.WriteLine(stack.Peek()); // 2
Console.WriteLine(stack.Count); // 2
Console.WriteLine(MaxOf(stack)); // 2 (max of remaining items)
static T MaxOf<T>(IEnumerable<T> items) where T : IComparable<T>
{
T? best = default;
foreach (var item in items)
if (best is null || item.CompareTo(best) > 0)
best = item;
return best!;
}
class TypedStack<T>
{
private readonly List<T> _data = new();
public int Count => _data.Count;
public void Push(T item) => _data.Add(item);
public T Pop() { var v = _data[^1]; _data.RemoveAt(_data.Count - 1); return v; }
public T Peek() => _data[^1];
public IEnumerator<T> GetEnumerator() => _data.GetEnumerator();
}
3
2
2
2
9.7 Exercise
Exercise
- Write a generic
Clamp<T>method with awhere T : IComparable<T>constraint. Test it withint,double, andchar. - Write a generic
Result<T>class withIsSuccess,Value, andErrorMessageproperties. Return it from aDivide(double a, double b)method that fails on division by zero. - Use
List<T>and a genericFilter<T>method accepting aFunc<T, bool>predicate to filter a list of integers to only even values.
9.8 Common Mistakes
Calling type-specific operations without a constraint
static T Add<T>(T a, T b) => a + b; // compile error: + not defined for T
// Fix: use a constraint or the INumber<T> interface (.NET 7+)
Using object instead of a generic
object loses type information and requires
casting. A generic method keeps the type through the call chain without boxing
value types.
Not specifying the type parameter when it can't be inferred
var result = CreateNew(); // error: cannot infer T
var result = CreateNew<MyClass>(); // correct: specify explicitly
9.9 Key Terms
| Term | Meaning |
|---|---|
| generic | Type or method parameterized by one or more type arguments |
| type parameter | Placeholder (T, TKey, TValue) filled in at the call or construction site |
| type argument | The actual type supplied for a type parameter: List<int> → int |
| constraint (where) | Restricts which types are valid for a type parameter |
| type inference | Compiler deducing type arguments from context, avoiding explicit brackets |
| open type | A generic type with unspecified type parameters: List<T> |
| closed type | A generic type with all type parameters specified: List<int> |
| BCL | Base Class Library - the standard .NET collection of generic types |