13. Generics
Generics let you write 'code that takes a type as a parameter.' Learn how collections like List<T> are built, and how to define your own generic classes, methods, and constraints.
What you'll learn
- 1Define and use generic classes and generic methods
- 2Know why and how to apply **constraints (`where`)** to type parameters
- 3Get a one-line intuition for covariance (`out`) / contravariance (`in`)
Overview
Anything written with `<T>` β `List<int>`, `Dictionary<string, int>` β is a **generic** type. Generics are a powerful mechanism: "works for any type, but pinned to one type at use." You gain code reuse and type safety at the same time.
Core Concepts
1) Generic class
`<T>` is a placeholder for "a type to be determined later." It's decided at use.
public class Box<T>
{
public T? Value { get; set; }
}
var intBox = new Box<int> { Value = 42 };
var strBox = new Box<string> { Value = "Hi" };Using `T` instead of `object` means **no boxing / casting, and the type is checked at compile time**.
2) Generic method
public static T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) >= 0 ? a : b;
}
int m = Max(3, 7); // T inferred as int
string s = Max("a", "z");Most of the time the compiler infers `T`. To be explicit: `Max<int>(3, 7)`.
3) Constraints (`where`)
| Constraint | Meaning |
|---|---|
| `where T : class` | Only reference types (`string`, classes) |
| `where T : struct` | Only value types (`int`, `bool`, structs) |
| `where T : new()` | Parameterless constructor required |
| `where T : IComparable<T>` | Must implement the given interface |
| `where T : SomeBase` | `SomeBase` or its descendants |
Combine: `where T : class, new()` (reference type + default constructor).
4) Covariance (`out`) / contravariance (`in`) overview
In short, options that **allow conversions between generic types**.
- `IEnumerable<out T>`: use `IEnumerable<Dog>` as `IEnumerable<Animal>` (**covariant**, output-only).
- `Action<in T>`: use `Action<Animal>` as `Action<Dog>` (**contravariant**, input-only).
For now, just remember "they exist." Detailed usage is an advanced topic.
Examples
Example 1 β `GenericClass`: define and use `Box<T>`
var intBox = new Box<int> { Value = 42 };
var strBox = new Box<string> { Value = "Hi" };
Console.WriteLine($"intBox: {intBox.Value}");
Console.WriteLine($"strBox: {strBox.Value}");
public class Box<T>
{
public T? Value { get; set; }
public void Show() => Console.WriteLine($"Value in box: {Value}");
}**Output**
intBox: 42
strBox: Hi**Note:** One `Box<T>` definition handles any type β no duplication. The `?` in `T?` says `Value` may be initially `null`.
Example 2 β `GenericMethod`: `Max<T>` and type inference
Console.WriteLine(Max(3, 7));
Console.WriteLine(Max("apple", "banana"));
Console.WriteLine(Max(3.14, 2.71));
static T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) >= 0 ? a : b;
}**Output**
7
banana
3.14**Note:** `int`, `string`, `double` all implement `IComparable<T>` and satisfy the constraint. `CompareTo` returning positive means a is greater.
Example 3 β `Constraint`: comparing `where` constraints
// class + new() constraint β can construct
static T Create<T>() where T : class, new() => new T();
// struct constraint β value types only
static T DoubleIt<T>(T x) where T : struct
{
Console.WriteLine($"received: {x}");
return x;
}
Person p = Create<Person>();
p.Name = "Jisoo";
Console.WriteLine($"new person: {p.Name}");
DoubleIt(42);
DoubleIt(3.14);
// DoubleIt("a string"); β compile error: string is not a struct
public class Person
{
public string Name { get; set; } = "";
}**Output**
new person: Jisoo
received: 42
received: 3.14**Note:** The `new()` constraint enables `new T()`. The `struct` constraint blocks invalid types at compile time.
Example 4 β `Variance`: covariance/contravariance at a glance
// IEnumerable<out T> β covariant (use a Dog collection as an Animal collection)
IEnumerable<Dog> dogs = [new Dog("Baduk"), new Dog("Bbobbi")];
IEnumerable<Animal> animals = dogs; // OK thanks to 'out'
foreach (Animal a in animals)
{
Console.WriteLine($"animal: {a.Name}");
}
// Action<in T> β contravariant (use an Animal handler on a Dog)
Action<Animal> describe = a => Console.WriteLine($"name is {a.Name}");
Action<Dog> describeDog = describe; // OK thanks to 'in'
describeDog(new Dog("Choco"));
public class Animal(string name)
{
public string Name { get; } = name;
}
public class Dog(string name) : Animal(name);**Output**
animal: Baduk
animal: Bbobbi
name is Choco**Note:** Output-only (`out`) can be widened to a more general type, input-only (`in`) can be narrowed to a more specific type β safely. The classes here use **primary constructor** syntax.
Full example code (src/)
src/Constraint/Constraint.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Coll13</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/Constraint/Program.cs
#nullable enable
// new() constraint β can call new T()
Person p = Create<Person>();
p.Name = "Jisoo";
Console.WriteLine($"new person: {p.Name}");
// struct constraint β value types only
DoubleIt(42);
DoubleIt(3.14);
// DoubleIt("a string"); // compile error: string is not a struct
static T Create<T>() where T : class, new() => new T();
static T DoubleIt<T>(T x) where T : struct
{
Console.WriteLine($"received: {x}");
return x;
}
public class Person
{
public string Name { get; set; } = "";
}
src/GenericClass/GenericClass.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Coll13</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/GenericClass/Program.cs
#nullable enable
// One Box<T> definition handles any type
var intBox = new Box<int> { Value = 42 };
var strBox = new Box<string> { Value = "Hi" };
Console.WriteLine($"intBox: {intBox.Value}");
Console.WriteLine($"strBox: {strBox.Value}");
intBox.Show();
strBox.Show();
public class Box<T>
{
public T? Value { get; set; }
public void Show() => Console.WriteLine($"Value in box: {Value}");
}
src/GenericMethod/GenericMethod.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Coll13</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/GenericMethod/Program.cs
#nullable enable
// T is inferred at call site
Console.WriteLine(Max(3, 7));
Console.WriteLine(Max("apple", "banana"));
Console.WriteLine(Max(3.14, 2.71));
static T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) >= 0 ? a : b;
}
src/Variance/Program.cs
#nullable enable
// IEnumerable<out T> is covariant β a Dog collection can be used as an Animal collection
IEnumerable<Dog> dogs = [new Dog("Baduk"), new Dog("Bbobbi")];
IEnumerable<Animal> animals = dogs; // OK thanks to 'out'
foreach (Animal a in animals)
{
Console.WriteLine($"animal: {a.Name}");
}
// Action<in T> is contravariant β an Animal handler can be used on a Dog
Action<Animal> describe = a => Console.WriteLine($"name is {a.Name}");
Action<Dog> describeDog = describe; // OK thanks to 'in'
describeDog(new Dog("Choco"));
public class Animal(string name)
{
public string Name { get; } = name;
}
public class Dog(string name) : Animal(name);
src/Variance/Variance.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Coll13</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
Common Mistakes
- Trying to call `new T()` without a `new()` constraint β compile error.
- Confusing `where T : IComparable` (non-generic) with `where T : IComparable<T>`.
- Static fields on a generic class are **separate per type** β `Box<int>.X` and `Box<string>.X` are different variables.
- Sprinkling `<T>` everywhere β if there's only one type, concrete types read better.
- Trying to put `out`/`in` on a class β variance applies **only to interfaces and delegates**.
Summary
- Generics make code take a type as a parameter β **reusable + type-safe**.
- `where` constraints scope and guarantee what `T` can do.
- `out` is covariant (output), `in` is contravariant (input) β interfaces and delegates only.
- Standard collections (`List<T>`, `Dictionary<K,V>`, ...) are all generic creations.
Practice
**Practice - 13. Generics**
Problem 1 β Build a generic stack
- Project folder: `Homework01/`
- Key concepts: generic class, internally using `List<T>`
Requirements
- Define a `MyStack<T>` class. Internally use a `List<T>` and provide:
- `void Push(T item)`
- `T Pop()` β throw `InvalidOperationException` if empty
- `T Peek()` β throw `InvalidOperationException` if empty
- `int Count` (property)
- Create both `MyStack<int>` and `MyStack<string>` and exercise them.
Expected output
=== int stack ===
Push: 1, 2, 3
Peek: 3
Pop: 3
Pop: 2
Remaining count: 1
=== string stack ===
Push: A, B
Pop: B
Pop: AHints
- Using/removing the last element of `List<T>` makes it a stack.
- Access the last element with `list[^1]`.
---
Problem 2 β Generic `Swap<T>` method
- Project folder: `Homework02/`
- Key concepts: generic method, `ref` parameters
Requirements
- Write `static void Swap<T>(ref T a, ref T b)`.
- Call it on two `int`s and two `string`s and print the result.
Expected output
Before: a=10, b=20
After: a=20, b=10
Before: s1=hello, s2=world
After: s1=world, s2=helloHints
- Pass `ref` at the call site as well.
- A one-line temporary is enough: `T tmp = a; a = b; b = tmp;`
Check your answer
Try it yourself, then compare against the [`answer/`](./answer/) folder.
Answer (answer/)
homework/answer/Homework01/Homework01.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Coll13</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
homework/answer/Homework01/Program.cs
#nullable enable
Console.WriteLine("=== int stack ===");
MyStack<int> ints = new();
ints.Push(1);
ints.Push(2);
ints.Push(3);
Console.WriteLine("Push: 1, 2, 3");
Console.WriteLine($"Peek: {ints.Peek()}");
Console.WriteLine($"Pop: {ints.Pop()}");
Console.WriteLine($"Pop: {ints.Pop()}");
Console.WriteLine($"Remaining count: {ints.Count}");
Console.WriteLine("=== string stack ===");
MyStack<string> strs = new();
strs.Push("A");
strs.Push("B");
Console.WriteLine("Push: A, B");
Console.WriteLine($"Pop: {strs.Pop()}");
Console.WriteLine($"Pop: {strs.Pop()}");
public class MyStack<T>
{
private readonly List<T> _items = new();
public int Count => _items.Count;
public void Push(T item) => _items.Add(item);
public T Pop()
{
if (_items.Count == 0)
{
throw new InvalidOperationException("Stack is empty.");
}
T top = _items[^1];
_items.RemoveAt(_items.Count - 1);
return top;
}
public T Peek()
{
if (_items.Count == 0)
{
throw new InvalidOperationException("Stack is empty.");
}
return _items[^1];
}
}
homework/answer/Homework02/Homework02.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Coll13</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
homework/answer/Homework02/Program.cs
#nullable enable
int a = 10, b = 20;
Console.WriteLine($"Before: a={a}, b={b}");
Swap(ref a, ref b);
Console.WriteLine($"After: a={a}, b={b}");
string s1 = "hello", s2 = "world";
Console.WriteLine($"Before: s1={s1}, s2={s2}");
Swap(ref s1, ref s2);
Console.WriteLine($"After: s1={s1}, s2={s2}");
static void Swap<T>(ref T a, ref T b)
{
T tmp = a;
a = b;
b = tmp;
}
Try It Yourself
cd src/GenericClass
dotnet run
cd ../GenericMethod
dotnet run
cd ../Constraint
dotnet run
cd ../Variance
dotnet runNext Lecture
[14_LINQ](../14_LINQ/) β Meet LINQ, the elegant query tool for generic collections.
All lecture materials and example code are openly available on GitHub.
View on GitHub β