Land your first dev job — 100% money-back guarantee.
Continue

Generics in C#

Write reusable type-safe code with generic classes, methods, interfaces, constraints (where T : class/struct/new()/IInterface), covariance, and contravariance.

Why Generics?

Before generics, reusable containers stored everything as `object`. That forced boxing (wrapping value types) and casting (which could fail at runtime). Generics let you write one class or method that works with any type while keeping full compile-time type safety — no casts, no boxing, no surprises.

1. The Problem Without Generics

A stack that holds `object` compiles fine but is unsafe — you can accidentally mix types and only find out at runtime.

2. The Generic Solution

Replace `object` with a type parameter `T`. The compiler enforces type safety and generates efficient code per type — no casts, no boxing.

Generic Classes

A generic class is declared with `<T>` after the class name. You can have multiple type parameters (`<TKey, TValue>`), and each usage of the class is independently type-safe. Generic classes are the basis of all .NET collection types — `List<T>`, `Dictionary<TKey, TValue>`, `Queue<T>`, etc.

1. Generic Class with Multiple Type Parameters

A class can have more than one type parameter. A `Pair<TFirst, TSecond>` can hold any two types together without losing type information.

2. Generic Repository Pattern

A real-world use of generics: a repository class that works with any entity type, avoiding duplication of CRUD logic for each entity.

Generic Methods

A method can be generic even if the class it belongs to is not. The type parameter goes between the method name and the parentheses: `void Swap<T>(ref T a, ref T b)`. The compiler usually infers `T` from the arguments so you rarely need to specify it explicitly.

1. Generic Methods

Type parameters on methods let you write utility functions that work on any type without repeating the logic for each type.

Generic Constraints

Without constraints, `T` can be any type — which limits what you can do with it (you can only call methods defined on `object`). Constraints tell the compiler what `T` is guaranteed to have, unlocking methods, operators, and patterns. C# supports six kinds of constraints that can be combined.

1. Types of Constraints

The six constraint kinds: `class` (reference type), `struct` (value type), `new()` (has a no-arg constructor), interface constraint, base class constraint, and `notnull`.

2. default(T) and typeof(T)

`default(T)` returns the zero value for any type — `null` for reference types, `0` for numbers, `false` for bool. `typeof(T)` returns the `Type` object at compile time.

Generic Interfaces

Generic interfaces work exactly like generic classes — you declare a type parameter and use it in the method signatures. A class can implement the same generic interface multiple times with different type arguments (e.g., `IConverter<string, int>` and `IConverter<int, string>`). The built-in `IEnumerable<T>`, `IComparable<T>`, and `IEquatable<T>` are all generic interfaces.

1. Defining and Implementing Generic Interfaces

A generic interface lets you define a contract that is reusable across types while remaining type-safe.

Covariance & Contravariance

By default, `IEnumerable<Dog>` cannot be assigned to `IEnumerable<Animal>` even though `Dog` is an `Animal` — generic types are invariant. Covariance (`out T`) allows assignment from a more derived type to a base type. Contravariance (`in T`) allows the opposite — a base type where a derived type is expected. These only apply to interfaces and delegates, not classes.

1. Covariance with out

`out T` marks a type parameter as covariant — the interface can only return `T`, never accept it as input. This lets you assign `IProducer<Dog>` to `IProducer<Animal>`.

2. Contravariance with in

`in T` marks a type parameter as contravariant — the interface only accepts `T` as input, never returns it. This lets you assign `IConsumer<Animal>` to `IConsumer<Dog>` — a handler for any animal can handle a dog specifically.