# Advanced Generics

Course: [C# Advanced](https://www.devtutorials.ai/learn/advanced-csharp) · Section: Modern C# Features
Source: https://www.devtutorials.ai/learn/advanced-csharp/modern-csharp/advanced-generics

> Generic constraints with where: new(), interface and base class constraints, multiple constraints, generic math with INumber<T>, and generic performance.

## Introduction to Advanced Generics

Think about a lunch box with a label: "Sandwiches only".

A plain box can hold anything: soup, sand, a sandwich. A labelled box makes a promise. Everyone who opens it knows exactly what's inside and what they can do with it.

You already know **generics**: `List<int>`, `Dictionary<string, decimal>`, or your own `Box<T>`. The `T` is a placeholder for "any type".

But sometimes you need to limit which types T can be. If your method needs to **compare** two values, or **create** a new one, or call `.Save()`, then not every type will work. **Generic constraints** are like the label on the box: they limit which types are allowed, and in return they let you use those types' abilities.

**In simple words:** a constraint is a rule for `T`. It limits which types can be used, and unlocks what you can do with them.

**Easy Way to Remember:** without a constraint, `T` can only do what **every** object can do. A constraint is a **promise** that unlocks more.

## What You Will Learn

1. What generic constraints are, and the `where` keyword.
2. The `new()` constraint, for creating objects.
3. Interface constraints and base class constraints.
4. Combining several constraints.
5. Generic math with `INumber<T>`.
6. How generics affect performance.

All examples in this lesson run in your browser. Generic math needs .NET 7 or later; the playground uses .NET 10.

## Start With a Working Example

Press **Run**.

```csharp runnable title="Find the largest item of any comparable type"
using System;

Console.WriteLine(Largest(new[] { 4, 19, 7 }));
Console.WriteLine(Largest(new[] { "pear", "apple", "zebra", "mango" }));
Console.WriteLine(Largest(new[] { 2.5, 9.75, 3.0 }));

static T Largest<T>(T[] items) where T : IComparable<T>
{
    T best = items[0];
    foreach (T item in items)
    {
        if (item.CompareTo(best) > 0)
            best = item;
    }
    return best;
}
```

One method works for numbers, text and decimals. `where T : IComparable<T>` promises that every `T` has a `CompareTo` method, so the method is allowed to call it.

### Try This

Remove `where T : IComparable<T>` and run it again. The compiler now refuses `item.CompareTo(best)`, because it can no longer be sure that `T` knows how to compare itself.

## Generic Constraints

A **constraint** is written with the **`where`** keyword after the method or class signature:

```text
static void Save<T>(T item) where T : ISaveable
class Repository<T> where T : class, IEntity, new()
```

Here are all the common constraints:

| Constraint | `T` must be | Unlocks |
|---|---|---|
| `where T : class` | A reference type: a class, interface, delegate, or array. These are types that are stored on the heap with a memory address. | Using `null` checks and reference semantics |
| `where T : class?` | A reference type, but nullable types are also allowed | Same as above, with support for nullable types |
| `where T : struct` | A non-nullable value type, like `int`, `DateTime`, or your own struct. Value types are stored directly on the stack. | Value-type behavior: the type is copied when passed to a method or assigned to a variable |
| `where T : notnull` | Any type that cannot be null | Safe to use as a dictionary key |
| `where T : unmanaged` | A value type with no references inside it | Low-level memory operations and unsafe code |
| `where T : new()` | A type with a public parameterless constructor | Using `new T()` to create objects |
| `where T : SomeBaseClass` | That class or a class derived from it | Calling the base class's methods and properties |
| `where T : ISomeInterface` | A type that implements the interface | Calling all the interface's methods and properties |
| `where T : U` | The same as, or derived from, another type parameter `U` | Converting `T` to `U` |

```csharp runnable title="class and struct constraints"
using System;

Console.WriteLine(Describe("hello"));      // string is a class
Console.WriteLine(Describe(new int[3]));  // arrays are classes
Console.WriteLine(Twice(21));              // int is a struct
Console.WriteLine(Twice(DateTime.MinValue.Year));

static string Describe<T>(T? item) where T : class =>
    item is null ? "nothing" : $"a {typeof(T).Name}";

static string Twice<T>(T value) where T : struct =>
    $"{value} and {value}";
```

If you call `Describe(5)`, the compiler stops you: `int` is not a class. Constraints catch mistakes **at compile time**, before the program ever runs.

## new() Constraint

The **`new()`** constraint says: "`T` must have a public constructor with **no parameters**". In return, you can write `new T()` inside the generic code.

It is useful for factories and for code that needs to create fresh objects of a type it does not know in advance.

```csharp runnable title="A generic factory with new()"
using System;
using System.Collections.Generic;

List<Customer> customers = CreateMany<Customer>(3);
List<Order> orders = CreateMany<Order>(2);

Console.WriteLine($"Made {customers.Count} customers, first is '{customers[0].Name}'");
Console.WriteLine($"Made {orders.Count} orders, status '{orders[0].Status}'");

static List<T> CreateMany<T>(int count) where T : new()
{
    var list = new List<T>();
    for (int i = 0; i < count; i++)
        list.Add(new T());   // allowed because of new()
    return list;
}

class Customer
{
    public string Name { get; set; } = "New customer";
}

class Order
{
    public string Status { get; set; } = "Pending";
}
```

A class whose only constructor takes parameters, such as `class Point(int x, int y)`, cannot be used here. The compiler would reject `CreateMany<Point>(2)`.

## Interface Constraints

An **interface constraint** says: "`T` must implement this interface". Inside the generic code you can then call every member of that interface.

This is the most common kind of constraint. It lets one method work with many different classes, as long as they all follow the same contract.

```csharp runnable title="Print a total for anything that has a price"
using System;
using System.Collections.Generic;

var books = new List<Book> { new("C# Basics", 450m), new("Async Deep Dive", 700m) };
var courses = new List<Course> { new("Generics", 1200m) };

Console.WriteLine($"Books:   {Total(books)}");
Console.WriteLine($"Courses: {Total(courses)}");

static decimal Total<T>(IEnumerable<T> items) where T : IPriced
{
    decimal sum = 0;
    foreach (T item in items)
        sum += item.Price;     // IPriced guarantees Price exists
    return sum;
}

interface IPriced
{
    decimal Price { get; }
}

record Book(string Title, decimal Price) : IPriced;
record Course(string Name, decimal Price) : IPriced;
```

`Book` and `Course` are unrelated types, but both implement `IPriced`, so `Total` works with both.

## Base Class Constraints

A **base class constraint** says: "`T` must be this class, or a class that inherits from it". You can then use everything the base class offers.

```csharp runnable title="A pay report for any kind of employee"
using System;
using System.Collections.Generic;

var developers = new List<Developer> { new("Asha", 90_000), new("Ravi", 85_000) };
var managers = new List<Manager> { new("Meena", 120_000) };

PrintPay(developers);
PrintPay(managers);

static void PrintPay<T>(List<T> staff) where T : Employee
{
    foreach (T person in staff)
        Console.WriteLine($"{person.Name,-6} {person.Role,-10} {person.MonthlyPay():0}");
}

abstract class Employee(string name, decimal yearlySalary)
{
    public string Name { get; } = name;
    public abstract string Role { get; }
    public virtual decimal MonthlyPay() => yearlySalary / 12;
}

class Developer(string name, decimal salary) : Employee(name, salary)
{
    public override string Role => "Developer";
}

class Manager(string name, decimal salary) : Employee(name, salary)
{
    public override string Role => "Manager";
    public override decimal MonthlyPay() => base.MonthlyPay() + 1000; // bonus
}
```

| | Interface constraint | Base class constraint |
|---|---|---|
| How many per `T` | As many as you like | Only one |
| Shares code | No, only a contract | Yes, the base class's code |
| Typical use | Behavior unrelated types share (`IComparable`, `IPriced`) | A family of related classes (`Employee`, `Shape`) |

## Multiple Constraints

One type parameter can have **several** constraints, separated by commas. They must come in this order:

1. `class`, `struct`, `notnull` or `unmanaged` (only one of these)
2. a base class (only one)
3. any number of interfaces
4. `new()` (always last)

Each type parameter gets its own `where` clause.

```text
class Repository<TEntity, TKey>
    where TEntity : class, IEntity<TKey>, new()
    where TKey : notnull, IComparable<TKey>
```

```csharp runnable title="A small repository with several constraints"
using System;
using System.Collections.Generic;
using System.Linq;

var repo = new Repository<Product, int>();
repo.Add(new Product { Id = 3, Name = "Lamp" });
repo.Add(new Product { Id = 1, Name = "Desk" });
Product blank = repo.CreateBlank();

Console.WriteLine($"Blank product: '{blank.Name}'");
foreach (Product p in repo.AllSorted())
    Console.WriteLine($"{p.Id}: {p.Name}");

class Repository<TEntity, TKey>
    where TEntity : class, IEntity<TKey>, new()
    where TKey : notnull, IComparable<TKey>
{
    private readonly Dictionary<TKey, TEntity> _items = new();

    public void Add(TEntity entity) => _items[entity.Id] = entity;   // TKey is notnull: safe as a key
    public TEntity CreateBlank() => new TEntity();                   // thanks to new()
    public IEnumerable<TEntity> AllSorted() =>
        _items.Values.OrderBy(e => e.Id);                             // TKey is IComparable
}

interface IEntity<TKey>
{
    TKey Id { get; }
}

class Product : IEntity<int>
{
    public int Id { get; set; }
    public string Name { get; set; } = "Unnamed";
}
```

Every constraint earns its place: `class` because entities are reference types, `IEntity<TKey>` for `Id`, `new()` for `CreateBlank`, `notnull` for the dictionary key, `IComparable<TKey>` for sorting.

Add only the constraints your code really uses. Each extra constraint shuts out some types for no reason, which makes the generic code less reusable.

## Generic Math

Before .NET 7, you could not write one `Sum<T>` method for `int`, `double` **and** `decimal`. There was no constraint that meant "a number", so `a + b` did not compile for a `T`.

**Generic math** fixes this. .NET 7 added interfaces such as **`INumber<T>`** (in `System.Numerics`) that every built-in number type implements. They provide operators like `+`, `-`, `*`, `/`, `<`, and values like `T.Zero` and `T.One`.

```csharp runnable title="One Sum and Average for every number type"
using System;
using System.Numerics;

int[] marks = { 72, 85, 91 };
double[] heights = { 1.62, 1.75, 1.80 };
decimal[] prices = { 19.99m, 5.01m, 75m };

Console.WriteLine($"Sum of marks:   {Sum(marks)}");
Console.WriteLine($"Sum of heights: {Sum(heights)}");
Console.WriteLine($"Sum of prices:  {Sum(prices)}");
Console.WriteLine($"Average price:  {Average(prices):0.00}");

static T Sum<T>(T[] values) where T : INumber<T>
{
    T total = T.Zero;           // the zero of whatever number type T is
    foreach (T v in values)
        total += v;             // + works because of INumber<T>
    return total;
}

static T Average<T>(T[] values) where T : INumber<T> =>
    Sum(values) / T.CreateChecked(values.Length);
```

`T.CreateChecked(values.Length)` turns the `int` length into a `T`, so it can be divided. Some useful generic math interfaces:

| Interface | Gives you |
|---|---|
| `INumber<T>` | Everything for ordinary numbers: `+ - * /`, comparisons, `T.Zero`, `T.One` |
| `IAdditionOperators<T,T,T>` | Only `+` (when that is all you need) |
| `IComparisonOperators<T,T,bool>` | `<`, `>`, `<=`, `>=` |
| `IBinaryInteger<T>` | Whole-number types only: `%`, bit operations |
| `IFloatingPoint<T>` | `double`, `float`, `decimal`: rounding, fractions |

Interfaces like `INumber<T>` use **static abstract** members. This means that `T.Zero` and the `+` operator are static—you call them directly on the type, not on an instance. This feature helps generic math work smoothly across all number types.

## Generic Performance Considerations

Generics are not just convenient. They are usually **faster** than the old non-generic alternatives.

**Boxing.** Storing an `int` in an `object` variable (as old collections like `ArrayList` did) wraps the number in a container object on the heap. To use the number, you must unwrap it. This wrapping and unwrapping costs memory and processing time.

Generic collections like `List<int>` store the `int` directly: no wrapping, less memory, less garbage collection work.

```csharp runnable title="List<int> versus List<object>"
using System;
using System.Collections.Generic;

var typed = new List<int>();
var untyped = new List<object>();

for (int i = 0; i < 5; i++)
{
    typed.Add(i);     // stored as an int
    untyped.Add(i);   // wrapped: stored in a container object
}

int sumTyped = 0;
foreach (int n in typed) sumTyped += n;              // no cast needed

int sumUntyped = 0;
foreach (object o in untyped) sumUntyped += (int)o;  // unwrap (cast) every time

Console.WriteLine($"Typed sum:   {sumTyped}");
Console.WriteLine($"Untyped sum: {sumUntyped}");
Console.WriteLine($"Element type of the typed list: {typed.GetType().GenericTypeArguments[0].Name}");
```

| Point | What it means for you |
|---|---|
| No boxing for value types | `List<int>`, `Dictionary<int, double>` store numbers directly without wrapping them, making them fast and memory-friendly |
| Specialised code per value type | .NET builds separate optimised code for `List<int>` and `List<double>`, so each collection is as fast as possible for its type |
| Shared code for reference types | `List<string>` and `List<Customer>` share one copy of the code, so generic code does not make your program bigger |
| Type safety at compile time | Wrong types are caught before running, with no runtime casts needed |
| Constraints cost nothing at run time | Constraints are checked by the compiler only; they do not slow down your running program |
| Avoid `typeof(T) == typeof(int)` checks in loops | Use method overloads or constraints instead; type checks slow down frequently-called code |

Calling an interface method on a constrained `T` that is a **struct** does not wrap it. But casting that struct to the interface type itself, such as `IComparable c = value;`, does wrap it. Keep the value typed as `T`.

## Common mistakes

| Mistake | Wrong | Correct | Why |
|---|---|---|---|
| Calling a method `T` might not have | `item.CompareTo(x)` with no constraint | `where T : IComparable<T>` | `T` only has `object`'s members |
| `new T()` without `new()` | `T x = new T();` | `where T : new()` | The compiler cannot know a constructor exists |
| `new()` in the wrong place | `where T : new(), IEntity` | `where T : IEntity, new()` | `new()` must come last |
| Two base classes | `where T : Animal, Vehicle` | One base class, many interfaces | C# allows only one base class |
| `+` on a plain `T` | `T total = a + b;` | `where T : INumber<T>` | Operators need generic math |
| Starting a sum at `0` | `T total = 0;` | `T total = T.Zero;` | `0` is an `int`, not a `T` |
| Boxing with `object` lists | `List<object>` for numbers | `List<int>` | Boxing costs memory and time |

## What to Remember

- Generic constraints use `where` to limit which types `T` can be, and unlock what you can do with `T`.
- `class`, `struct`, `notnull` and `unmanaged` describe the kind of type.
- `new()` allows `new T()`; it must be the last constraint.
- An interface constraint gives access to the interface's members; a type can have many.
- A base class constraint gives access to the base class's members; only one is allowed.
- Several constraints go in order: kind, base class, interfaces, `new()`.
- Generic math (`INumber<T>`, `T.Zero`, `T.CreateChecked`) lets one method do maths on every number type.
- Generic collections avoid boxing and catch type mistakes at compile time.

Next: Extension Methods
