← Back to C# series
πŸ“š
Collections & LINQ
Collections Β· Prerequisite: collection basics

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.

C#.NET 8collectionsgenerics
Duration
⏱ ~1-1.5 hours
Level
πŸ“Š Intermediate
Prerequisite
🎯 List·Dictionary·HashSet
OUTCOME
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.

csharp
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

csharp
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`)

ConstraintMeaning
`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>`

csharp
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**

text
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

csharp
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**

text
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

csharp
// 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**

text
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

csharp
// 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**

text
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

xml
<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

csharp
#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

xml
<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

csharp
#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

xml
<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

csharp
#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

csharp
#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

xml
<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

  1. Trying to call `new T()` without a `new()` constraint β€” compile error.
  2. Confusing `where T : IComparable` (non-generic) with `where T : IComparable<T>`.
  3. Static fields on a generic class are **separate per type** β€” `Box<int>.X` and `Box<string>.X` are different variables.
  4. Sprinkling `<T>` everywhere β€” if there's only one type, concrete types read better.
  5. 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

text
=== int stack ===
Push: 1, 2, 3
Peek: 3
Pop: 3
Pop: 2
Remaining count: 1
=== string stack ===
Push: A, B
Pop: B
Pop: A

Hints

  • 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

text
Before: a=10, b=20
After: a=20, b=10
Before: s1=hello, s2=world
After: s1=world, s2=hello

Hints

  • 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

xml
<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

csharp
#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

xml
<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

csharp
#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

bash
cd src/GenericClass
dotnet run

cd ../GenericMethod
dotnet run

cd ../Constraint
dotnet run

cd ../Variance
dotnet run

Next Lecture

[14_LINQ](../14_LINQ/) β€” Meet LINQ, the elegant query tool for generic collections.

Example code / lecture materials

All lecture materials and example code are openly available on GitHub.

View on GitHub β†—