Constraints

A where clause narrows what a type parameter may be, and that is exactly what lets the method body do something with it.

Syntaxclass Repo<T> where T : Entity, IAuditable, new() { ... }

A type parameter with no constraint is nearly useless inside the method body. The compiler knows nothing about T, so all it allows is storing it, passing it along, and calling the few members every type has, such as ToString.

A constraint is a promise about T, written with the where keyword. It narrows what callers may supply, and in exchange it widens what the body may do. where T : IComparable<T> promises that T has a CompareTo method, and that promise is what lets the body sort.

The kinds

ConstraintWhat it promises
where T : classA reference type, and a non-nullable one. Write class? to allow nullable ones too.
where T : structA value type, so a T is never null.
where T : notnullNever a nullable reference type. A violation is a warning, not an error.
where T : new()A non-abstract type with a public parameterless constructor, so new T() compiles. A merely accessible one, such as an internal or protected parameterless constructor, does not satisfy it.
where T : SomeClassThat class, or something derived from it.
where T : ISomeInterfaceEvery member of that interface.
where T : UAnother type parameter, or something derived from it.

There is also unmanaged for low-level interop, and allows ref struct, added in C# 13, which widens rather than narrows: it lets a ref struct be the type argument.

Order and combinations

Several constraints on one parameter are separated by commas, and each type parameter gets its own where clause. The order is fixed: base class, then interfaces, then new(), then allows ref struct. new() cannot be combined with struct or unmanaged, which already guarantee it.

What trips people up

An overriding method must not restate constraints; they are inherited, and repeating them is error CS0460. A derived generic class is the opposite: it has to restate every constraint its base declares.

Example

Example · csharp
int[] numbers = [3, 17, 8];
string[] words = ["pear", "apple", "plum"];

// Without a constraint the body could not call CompareTo at all.
Console.WriteLine(Largest(numbers));
Console.WriteLine(Largest(words));

// new() promises a constructor taking no arguments, so new T() compiles.
Console.WriteLine(Fresh<Counter>().Describe());

static T Largest<T>(T[] items) where T : IComparable<T>
{
    T best = items[0];
    foreach (T item in items)
    {
        // CompareTo returns a number above zero when item sorts after best.
        if (item.CompareTo(best) > 0)
        {
            best = item;
        }
    }

    return best;
}

static T Fresh<T>() where T : new()
{
    return new T();
}

class Counter
{
    public int Value { get; set; }

    public string Describe() => $"Counter at {Value}";
}

// Output:
// 17
// plum
// Counter at 0

When to use it

  • Let a Largest or Clamp helper accept any sortable type by requiring IComparable<T>.
  • Create instances of T inside a factory method by requiring new().
  • Require notnull on a type parameter you intend to use as a dictionary key.
  • Accept only reference types so the body can compare a value against null.

More examples

notnull, and why Dictionary demands it

Constraints travel. The moment you use T as a dictionary key you inherit the dictionary's own requirement, and the compiler says so. notnull is the mildest constraint: it rules out nullable reference types but still allows both value types and reference types, which is what a key needs. Violating it is a warning rather than an error, and only in a file where nullable reference types are enabled.

Example · csharp
string[] words = ["red", "blue", "red"];
Dictionary<string, int> counts = Tally(words);

// A dictionary has no order of its own, so sort the keys before printing.
List<string> keys = new List<string>(counts.Keys);
keys.Sort();
foreach (string key in keys)
{
    Console.WriteLine($"{key}: {counts[key]}");
}

// Dictionary<TKey, TValue> declares where TKey : notnull. Passing T through
// as the key type means T has to carry the same promise.
static Dictionary<T, int> Tally<T>(T[] items) where T : notnull
{
    Dictionary<T, int> counts = new Dictionary<T, int>();
    foreach (T item in items)
    {
        // When the key is absent, TryGetValue leaves existing at 0.
        counts.TryGetValue(item, out int existing);
        counts[item] = existing + 1;
    }

    return counts;
}

// Output:
// blue: 1
// red: 2

Base class, interface and new() together

Each line of the body is unlocked by one constraint, which is the clearest way to see what constraints are for. Note what new() does not promise: a constructor taking no arguments and nothing else, so a type whose only constructor needs arguments will not satisfy it. That is why a factory interface is often a better design than new() once construction needs real input.

Example · csharp
// The order is fixed: base class, then interfaces, then new().
Console.WriteLine(Build<SquareShape>(4).Area());

static TShape Build<TShape>(int size) where TShape : Shape, INamed, new()
{
    TShape shape = new TShape();   // allowed by new()
    shape.Size = size;             // allowed by the Shape constraint
    Console.WriteLine(shape.Name); // allowed by the INamed constraint
    return shape;
}

interface INamed
{
    string Name { get; }
}

abstract class Shape
{
    public int Size { get; set; }

    public abstract int Area();
}

class SquareShape : Shape, INamed
{
    public string Name => "square";

    public override int Area() => Size * Size;
}

// Output:
// square
// 16

A derived generic class must restate the constraint

Constraints are not inherited by a derived type. SortedBag<T> hands its own T to Bag<T>, so it has to prove that T satisfies what Bag<T> asks for, and the only way to prove it is to declare it. The opposite rule applies to overriding a method: there you must not repeat the constraints, or you get error CS0460.

Example · csharp
SortedBag<int> bag = new SortedBag<int>();
bag.Add(5);
bag.Add(1);
Console.WriteLine(string.Join(", ", bag.Sorted()));

class Bag<T> where T : IComparable<T>
{
    protected List<T> Items { get; } = new List<T>();

    public void Add(T item) => Items.Add(item);
}

// The constraint has to be written again. Without it the compiler rejects
// Bag<T> on this line, because T would not be known to be comparable.
class SortedBag<T> : Bag<T> where T : IComparable<T>
{
    public List<T> Sorted()
    {
        List<T> copy = new List<T>(Items);
        copy.Sort();   // the default comparer uses CompareTo
        return copy;
    }
}

// Output:
// 1, 5

Discussion

  • Be the first to comment on this lesson.