# Generics

Source: https://codewiki.com/csharp/generics/

> - **what**: Generics use type parameters to preserve type relationships across inputs, outputs, and storage, while one declaration supports many concrete types.
> - **trap**: Constraints expose compile-time capabilities but don't validate runtime business data. Generic classes are invariant, and `out` and `in` apply only to eligible interfaces and delegates.
> - **fix**: Express only the operations an algorithm needs as constraints, handle values through generic protocols such as `EqualityComparer.Default`, and validate `null`, empty collections, and business invariants at API boundaries.

## What it is and why it exists

A generic declaration leaves one or more types as parameters. Classes, structs, interfaces, delegates, and methods can declare a type parameter, conventionally named `T`, `TKey`, or `TResult`. Once a caller supplies type arguments, a constructed type such as `List<string>` or `Dictionary<string, int>` is fully specified.

Generics reuse type relationships, not merely lines of code. `T Find(IEnumerable source)` says that the result and the sequence element have the same type. Changing it to `object Find(IEnumerable source)` loses that relationship, forcing callers to cast and accept runtime failure.

Passing a value type through `object` usually causes boxing, followed by unboxing when the value comes back. `List<int>` stores and retrieves `int` elements directly, preserving compile-time checking without first converting every element to `object`. This doesn't mean all generic code allocates nothing; it avoids this specific conversion path.

Generics appear throughout collections, LINQ, delegates, asynchronous APIs, dependency injection, and serialization libraries. In `IEnumerable`, the type between angle brackets isn't documentation. It is part of the contract used by both the compiler and runtime.

Generics fit algorithms that apply to several types and depend on a small, explicit set of capabilities. If each type needs a different business flow, a stack of type tests and `typeof(T)` branches usually hides several implementations. Interfaces, polymorphism, or separate services state that design more clearly.

## How it works

### Declaration and construction are separate stages

`Cache<TKey, TValue>` declares two type parameters. `Cache<string, Product>` supplies two type arguments and forms a closed constructed type; arity, order, and constraints must all match. `Cache<,>` is an unbound generic type used only for metadata operations such as `typeof` and reflection. You can't instantiate it directly.

When one type parameter occurs in several positions, the compiler keeps those positions equal. The following signature requires `item`, `fallback`, and the result to use one `T`. A caller can't mix unrelated types in one call.

```csharp
static T Choose<T>(bool useItem, T item, T fallback) =>
    useItem ? item : fallback;
```

A generic method owns its type parameters even when its containing type isn't generic. For `Choose(true, 10, 20)`, the compiler infers `T` as `int` from the arguments. Method type inference doesn't infer `T` from the return value. If inference fails or its result violates a constraint, the caller must change the arguments or write the type argument explicitly.

### Constraints expose capabilities

An unconstrained `T` supports only operations available to every type, such as assignment, `object` members, and `EqualityComparer.Default`. A generic constraint in a `where` clause narrows the accepted types and lets the implementation use members guaranteed by that constraint.

| Constraint | Types a caller can supply | Capability available to the implementation |
| --- | --- | --- |
| `where T : class` | Non-nullable reference types | Analyze `T` as a non-nullable reference type |
| `where T : class?` | Nullable or non-nullable reference types | Analyze `T` as a nullable reference type |
| `where T : struct` | Non-nullable value types | Apply value-type rules; a parameterless constructor is implicit |
| `where T : unmanaged` | Non-nullable value types without managed references | Obtain size or pointers when unsafe code is enabled |
| `where T : notnull` | Non-nullable value or reference types | Diagnose nullable type arguments |
| `where T : BaseType` | Types derived from the named base class | Use public base-class members |
| `where T : IContract` | Types implementing the named interface | Use interface members, including static abstract members |
| `where T : new()` | Non-abstract types with a public parameterless constructor | Execute `new T()` |

Constraints are compile-time rules, not business validators. `where T : class` doesn't stop older code, reflection, or callers with nullable analysis disabled from passing `null`. `where T : new()` doesn't prove that the new object satisfies domain invariants. Public boundaries must still check actual values.

Multiple constraints follow a prescribed order: a primary constraint such as a reference-type, value-type, or `unmanaged` constraint comes first, followed by base-class or interface constraints, with `new()` near the end. The `allows ref struct` anti-constraint states that the implementation obeys `ref struct` safety rules. It comes after other constraints and prevents the implementation from assuming that `T` can be boxed or stored across `await`.

### Equality, comparison, and operators need protocols

Using `==` with an unknown `T` doesn't always compile and may not express the domain's equality rule. Ordinary equality searches should use `EqualityComparer.Default`; sorting can accept an `IComparer`; add `IComparable` only when the implementation calls `CompareTo`. Value types, reference types, and caller-supplied comparison policies then follow one explicit path.

C# interfaces can declare static abstract members. Generic math interfaces such as `INumber` therefore let constrained code use `T.Zero` and `+` without falling back to `dynamic`. This is a real capability constraint: callers can supply only types implementing the required static protocol.

Don't add `new()`, `class`, or a broad interface just for convenience. Constraints become compatibility boundaries in a public API. A constraint the algorithm doesn't use excludes otherwise valid types and may need to be removed later as a design correction.

### Variance controls reference-conversion direction

Variance describes how existing reference conversions propagate through generic interfaces or delegates. `IEnumerable<out T>` is covariant, so `IEnumerable<string>` can be assigned to `IEnumerable<object>`. It only produces `T` through that interface, preventing the consumer from inserting an arbitrary `object`.

`IComparer<in T>` is contravariant. A comparer that handles any `Animal` can also compare two `Dog` values, so `IComparer` can be used where `IComparer` is required. The direction looks reversed because `T` flows only into the interface.

Only interface and delegate type parameters can declare `out` or `in`; generic classes remain invariant. Even when `Dog` derives from `Animal`, `List` isn't assignable to `List`, because the receiver could then insert an `Animal` that isn't a `Dog`. Variance conversions also apply only to reference-type arguments. They don't convert `IEnumerable<int>` to `IEnumerable<object>`.

## Examples

These four examples cover type relationships, static capabilities supplied by constraints, variance conversions, and runtime type identity. This environment has no .NET SDK or C# compiler, so each block carries the repository's non-execution marker and no output is fabricated.

### Preserve the result type in a search

This search method binds the element, target, and comparer to the same `T`. The caller gets `string?` inference from the `string?[]` argument and never converts elements to `object`.

<!-- quick -->

```csharp
// file: GenericFind.cs
// # not executed here: the .NET SDK and C# compilers are unavailable
using System;
using System.Collections.Generic;

string?[] states = ["queued", null, "sent", "sent"];

Console.WriteLine(FindIndex(states, null));
Console.WriteLine(FindIndex(states, "sent"));
Console.WriteLine(FindIndex(states, "missing"));

static int FindIndex<T>(IReadOnlyList<T> items, T target)
{
    EqualityComparer<T> comparer = EqualityComparer<T>.Default;

    for (int index = 0; index < items.Count; index++)
    {
        if (comparer.Equals(items[index], target))
            return index;
    }

    return -1;
}
```

```text
# not executed here: the .NET SDK and C# compilers are unavailable
```


<!-- /quick -->

`EqualityComparer.Default` handles the two `null` operands in the example and follows the concrete type's default equality rules. The method returns an index instead of `default(T)`, so “not found” can't be confused with a valid default value of the element type.

A real API must also decide whether `items` itself can be `null`. Nullable annotations express intent and produce warnings. Across reflection, deserialization, or code compiled without nullable analysis, a contract that rejects `null` still needs a runtime check.

### Sum through static abstract members

`INumber` guarantees that `T.Zero` and addition are available. The implementation neither branches on concrete numeric types nor defers operator binding to `dynamic`.

```csharp
// file: GenericSum.cs
// # not executed here: the .NET SDK and C# compilers are unavailable
using System;
using System.Collections.Generic;
using System.Numerics;

int[] units = [2, 3, 5];
decimal[] prices = [1.25m, 2.50m, 4.00m];

Console.WriteLine(Sum(units));
Console.WriteLine(Sum(prices));

static T Sum<T>(IEnumerable<T> values) where T : INumber<T>
{
    T total = T.Zero;

    foreach (T value in values)
        total += value;

    return total;
}
```

```text
# not executed here: the .NET SDK and C# compilers are unavailable
```

An empty sequence returns `T.Zero`, the additive identity deliberately chosen by this method. That result doesn't automatically fit averages, maxima, or business validation for money. Those operations need their own empty-input contracts rather than copying sum's behavior.

If the code only needs addition and zero, it can use narrower operator interfaces instead of all of `INumber`. Constraints should follow the members the implementation actually calls, which makes the API available to more custom numeric types.

### Separate producers from consumers

The producer only returns `T`, so it declares `out T`; the consumer only accepts `T`, so it declares `in T`. Assignment direction follows these interface positions without changing any concrete object's runtime type.

```csharp
// file: Variance.cs
// # not executed here: the .NET SDK and C# compilers are unavailable
using System;

IProducer<Dog> dogSource = new DogSource();
IProducer<Animal> animalSource = dogSource;

IConsumer<Animal> animalSink = new AnimalSink();
IConsumer<Dog> dogSink = animalSink;

Animal animal = animalSource.Create();
dogSink.Save(new Dog("Rex"));
Console.WriteLine(animal.Name);

public interface IProducer<out T>
{
    T Create();
}

public interface IConsumer<in T>
{
    void Save(T value);
}

public sealed class DogSource : IProducer<Dog>
{
    public Dog Create() => new("Milo");
}

public sealed class AnimalSink : IConsumer<Animal>
{
    public void Save(Animal value) =>
        Console.WriteLine($"saved: {value.Name}");
}

public record Animal(string Name);
public sealed record Dog(string Name) : Animal(Name);
```

```text
# not executed here: the .NET SDK and C# compilers are unavailable
```

The static type of `animalSource` promises only an `Animal`, but the object remains a `DogSource` and the produced instance remains a `Dog`. `dogSink` can only be called with a `Dog`, while the underlying `AnimalSink` already knows how to handle that input.

An interface that both accepts and returns the same `T` usually can't mark that parameter covariant or contravariant. Splitting reads and writes into narrow interfaces can enable safe variance conversions, but only when that split matches the domain contract.

### Observe constructed types at runtime

The runtime retains generic type arguments. This example also shows that a generic static class has separate fields for each closed constructed type.

```csharp
// file: GenericRuntime.cs
// # not executed here: the .NET SDK and C# compilers are unavailable
using System;
using System.Collections.Generic;

Type integers = typeof(List<int>);
Type strings = typeof(List<string>);

Console.WriteLine(integers == strings);
Console.WriteLine(integers.GetGenericTypeDefinition() == typeof(List<>));
Console.WriteLine(string.Join(", ", integers.GetGenericArguments()));

TypeSlot<int>.Label = "quantities";
TypeSlot<string>.Label = "names";

Console.WriteLine(TypeSlot<int>.Label);
Console.WriteLine(TypeSlot<string>.Label);

public static class TypeSlot<T>
{
    public static string Label { get; set; } = typeof(T).Name;
}
```

```text
# not executed here: the .NET SDK and C# compilers are unavailable
```

`List<int>` and `List<string>` are distinct constructed types, but both have `List<>` as their generic type definition. Reflection code must distinguish a definition from a closed type before it chooses `GetGenericArguments()`, `MakeGenericType()`, or instance creation.

`TypeSlot<int>.Label` and `TypeSlot<string>.Label` don't share storage. This behavior can cache metadata per type, but it still needs capacity and lifetime design. When untrusted dynamic input determines how many types appear, generic static fields aren't an automatically bounded cache.

## Pitfalls

### Replacing a design problem with `object` or `dynamic`

> **Pitfall:** Generated code often changes `T` to `object`, or uses `dynamic` for comparison and arithmetic, when a generic expression fails to compile. The result may compile while losing input-output relationships and postponing missing-member, conversion, and operator failures until runtime.

**Fix:** write down the smallest protocol the algorithm needs. Use `IEqualityComparer` for equality, `IComparer` for ordering, and an interface with static abstract members for static operations. Use polymorphism or separate implementations for genuinely different business behavior instead of hiding it behind `dynamic`.

### Treating `default(T)` as not found

> **Pitfall:** `default(T)` may be valid data: `0`, `false`, an all-zero struct value, and `null` can all occur in input. Returning it for failure prevents the caller from distinguishing “found the default value” from “no result.”

**Fix:** return an index, a Boolean plus `out T`, a discriminated result type, or a nullable value when that fits the contract. The choice must account for value types, reference types, and the actual domain meaning.

### Using `new()` as an object-creation contract

> **Pitfall:** `where T : new()` guarantees only a public parameterless constructor. It doesn't inject dependencies, supply required data, or validate the created object. Adding it only to enable `new T()` often forces domain types to expose an invalid empty state.

**Fix:** accept a `Func` or domain factory, or constrain the parameter to an interface that represents the real creation protocol. Use `new()` only when an empty object is valid and the generic implementation truly owns creation.

### Assuming every generic collection is covariant

> **Pitfall:** A conversion from `IEnumerable` to `IEnumerable` doesn't imply a conversion from `List` to `List`. If a writable collection allowed it, the receiver could insert another `Animal` that isn't a `Dog`.

**Fix:** inspect the actual interface declaration for `out` or `in`, and check that the type arguments are reference types. Read-only consumers can accept a covariant interface. Code that mutates elements must preserve the exact element type or explicitly copy into a new target collection.

### Assuming nullable constraints checked the value

> **Pitfall:** `where T : notnull` and `where T : class` mainly affect type arguments and nullable analysis. They don't generate a `null` check at method entry or block every call path that escaped nullable analysis.

**Fix:** express type-level intent with constraints and protect real boundaries with runtime checks such as `ArgumentNullException.ThrowIfNull`. Test inputs arriving through reflection, deserialization, older assemblies, and nullable-disabled contexts.

### Adding constraints the algorithm doesn't use

> **Pitfall:** More constraints don't make an API safer. An unused `class`, `IComparable`, or `new()` rejects types that would otherwise work and may falsely suggest that the implementation needs those capabilities.

**Fix:** map each constrained capability to an actual operation in the implementation. Remove constraints with no such operation. Put business data rules in parameter validation or domain types instead of pretending they are type constraints.

<!-- deep -->

## Constraint boundaries and nullability

`class`, `class?`, `notnull`, and `struct` describe different sets. With nullable analysis enabled, `class` requires a non-nullable reference type and `class?` accepts nullable reference types. `notnull` accepts both non-nullable reference types and non-nullable value types. `struct` accepts non-nullable value types but not `Nullable`. Calling all of them non-null constraints hides these differences.

Nullable diagnostics also depend on the caller's nullable context. Violating `notnull` generally produces a compiler warning rather than a runtime exception, so a library can't treat it as a trust boundary. The meaning of `T?` also depends on the constraints. Public APIs may need annotation attributes and runtime validation to state the full contract.

The `default` constraint is limited to overrides and explicit interface implementations, where it states that a type parameter has neither the `class` nor `struct` primary constraint. It isn't a zero-value constraint that can be added anywhere. `allows ref struct` widens the accepted set while requiring the generic implementation to obey stack-safety rules. Because it works opposite to an ordinary narrowing constraint, the documentation calls it an anti-constraint.

Some constraints are mutually exclusive. `struct` already guarantees a parameterless constructor and can't be combined with `new()`; a base-class constraint also can't be combined arbitrarily with every primary constraint. Don't arrange a complex clause from memory. Compile the smallest declaration and retain only the parts the public signature uses.

## Constructed generics at runtime

The .NET runtime retains type arguments for closed generic types. `typeof(List<int>)` can report `int`, and reflection distinguishes `List<int>` from `List<string>`. This differs from a model that erases every type argument into one runtime type. Serializers, dependency injection containers, and reflection factories rely on this metadata.

Retaining type identity doesn't mean every construction duplicates all machine code. The runtime can usually share generic implementation code across several reference-type arguments, while value-type constructions generally need code suited to their layout. This is a runtime strategy, not evidence for an unmeasured performance claim.

Static fields on a generic type belong to each closed constructed type. `Registry` and `Registry` have separate static state. That enables per-type storage but can quietly create several long-lived caches. Review the actual set of constructed types and define cleanup, capacity, and concurrency rules.

In reflection, `typeof(Dictionary<,>)` is a generic type definition with unbound parameters, while `typeof(Dictionary<string, int>)` is a closed constructed type. `MakeGenericType()` validates arity and constraints at runtime and throws for invalid arguments. External input should map to an allowlist instead of choosing type definitions or arguments directly.

## Exact variance boundaries

A variance annotation appears on the interface or delegate declaration, not at the use site. A covariant parameter occurs mainly in output positions, while a contravariant parameter occurs mainly in input positions. The compiler checks properties, method parameters, return values, and nested delegate positions, rejecting declarations that could permit an unsafe read or write.

Variance propagates only existing implicit reference conversions. `string` has a reference conversion to `object`, so `IEnumerable<string>` converts to `IEnumerable<object>`. Converting `int` to `object` requires boxing and isn't the same conversion, so the corresponding generic interfaces aren't covariant. User-defined conversions don't automatically become generic variance conversions either.

A generic class may implement a variant interface, but the class remains invariant. A `List<string>` can be used as `IEnumerable<object>` by first viewing the list as `IEnumerable<string>` and then applying interface covariance. This doesn't create a `List<object>` or add the ability to write `object` values into the original list.

Array covariance is a separate, older runtime rule. A `string[]` can be assigned to `object[]`, but storing an incompatible object throws `ArrayTypeMismatchException` at runtime. Don't infer generic collection behavior from arrays. Generic variance exists to reject unsafe directions at compile time.

## Evolving a generic API

Public type-parameter names appear in documentation and reflection. `TKey`, `TValue`, and `TSource` explain relationships better than undifferentiated `T1` and `T2`. Names don't change type identity, but they do affect how callers understand constraints, diagnostics, and generated documentation.

Adding a constraint can stop existing callers from compiling because a formerly valid type argument becomes excluded. Removing a constraint widens the caller set but may force the implementation to stop using a member. Changing `in`, `out`, or a parameter position also changes assignability. All of these are API design changes.

Returning a broad interface doesn't automatically improve a design. `IEnumerable` may defer work, repeat a query, or support only one enumeration. `IReadOnlyList` promises a count and indexing but still doesn't guarantee that the backing data stays unchanged. The type parameter describes element type, while the collection interface describes access capability; both contract layers need to be accurate.

Don't test a generic library with only one reference type. Include a value type to expose boxing and default-value assumptions, a nullable case to validate boundaries, and a type with custom equality or ordering. For variance, write both assignments that should compile and assignments the compiler should reject, and treat the diagnostics as part of the test target.

## Open generics in frameworks

Reflection and dependency injection frameworks often accept open generic definitions. A registration can say that every `IRepository` is implemented by `Repository`, with the container closing both sides over the same argument when a concrete service is requested. This removes repetitive registrations but doesn't remove constructor, lifetime, or constraint checks.

The arity and positions of open parameters must be compatible. A two-parameter definition can't directly implement a one-parameter service, and a constrained implementation can't close over an argument that violates its constraints. A framework may validate during registration, container construction, or first resolution, so startup tests should deliberately resolve important closed types.

| Expression | Type represented | Directly instantiable |
| --- | --- | --- |
| `typeof(Box<>)` | A generic definition with one unbound parameter | No |
| `typeof(Box<int>)` | A closed constructed type with `int` as its argument | Yes |
| `typeof(Box<>).MakeGenericType(typeof(string))` | A closed constructed type created at runtime | Yes |

An open registration doesn't prove that every closed combination makes business sense. `Repository` may satisfy language constraints while violating application rules for aggregate roots, transactions, or authorization. Express such rules through an explicit marker, registration allowlist, or domain interface instead of discovering them on the first request.

Don't pass a client-supplied assembly-qualified name directly into `Type.GetType()` and `MakeGenericType()`. Language constraints may limit the eventual construction, but an attacker could still select application types that were never meant to be exposed or trigger many closed combinations. Map external names to a fixed set of services and type arguments first.

Open-generic diagnostics should record both the definition and final closed type. Seeing only an `IHandler<>` registration doesn't reveal which message type failed; seeing only `IHandler` may hide that it came from a global open registration. Include both types and the selected lifetime in diagnostic output.

<!-- /deep -->

[Checkpoint: csharp/generics](https://codewiki.com/csharp/generics/#checkpoint)

## Further reading

- [C# generics overview](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/generics)
- [Constraints on type parameters](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/constraints-on-type-parameters)
- [Variance in generic interfaces](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/covariance-contravariance/variance-in-generic-interfaces)
- [Generics in the .NET runtime](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/generics-in-the-run-time)
- [Constructed types in the C# language specification](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/types#constructed-types)
