Constraints

Generics support the ability to define constraints on type parameters. These constraints ensure that the types provided as type arguments conform to various rules. Take, for example, the BinaryTree<T> class shown in Listing 12.19.

Listing 12.19: Declaring a BinaryTree<T> Class with No Constraints
public class BinaryTree<T>
{
    public BinaryTree(T item)
    {
        Item = item;
    }
 
    public T Item { getset; }
    public Pair<BinaryTree<T>> SubItems { getset; }
}

(An interesting side note is that BinaryTree<T> uses Pair<T> internally, which is possible because Pair<T> is simply another type.)

Suppose you want the tree to sort the values within the Pair<T> value as it is assigned to the SubItems property. To achieve the sorting, the SubItems set accessor uses the CompareTo() method of the supplied key, as shown in Listing 12.20.

Listing 12.20: Needing the Type Parameter to Support an Interface
public class BinaryTree<T>
{
    public BinaryTree(T item)
    {
        Item = item;
    }
 
    public T Item { getset; }
    public Pair<BinaryTree<T>> SubItems
    {
        get { return _SubItems; }
        set
        {
            IComparable<T> first;
            // ERROR: Cannot implicitly convert type...
            //first = value.First;  // Explicit cast required
 
            //if(first.CompareTo(value.Second) < 0)
            //{
            //    // first is less than second
            //    //...
            //}
            //else
            //{
            //    // first and second are the same or
            //    // second is less than first
            //    //...
            //}
            _SubItems = value;
        }
    }
    private Pair<BinaryTree<T>> _SubItems;
}

At compile time, the type parameter T is an unconstrained generic. When the code is written as shown in Listing 12.20, the compiler assumes that the only members available on T are those inherited from the base type object, since every type has object as a base class. Only methods such as ToString(), therefore, are available to call on an instance of the type parameter T. As a result, the compiler displays a compilation error because the CompareTo() method is not defined on type object.

You can cast the T parameter to the IComparable<T> interface to access the CompareTo() method, as shown in Listing 12.21.

Listing 12.21: Needing the Type Parameter to Support an Interface or Exception Thrown
public class BinaryTree<T>
{
    public BinaryTree(T item)
    {
        Item = item;
    }
 
    public T Item { getset; }
    public Pair<BinaryTree<T>?>? SubItems
    {
        get { return _SubItems; }
        set
        {
            switch (value)
            {
                // Null handling removed for elucidation
 
                // Using C# 8.0 Pattern Matching. Switch to
                // checking for null prior to C# 8.0
                // ...
                case
                {
                        First: {Item: IComparable<T> first },
                        Second: {Item: T second } }:
                    if (first.CompareTo(second) < 0)
                    {
                        // first is less than second
                    }
                    else
                    {
                        // second is less than or equal to first
                    }
                    break;
                default:
                    throw new InvalidCastException(
                        @$"Unable to sort the items as 
                            typeof(T) } does not support IComparable<T>.");
            };
            _SubItems = value;
        }
    }
private Pair<BinaryTree<T>?>? _SubItems;
}

Unfortunately, if you now declare a BinaryTree<SomeType> class variable but the type argument (SomeType) does not implement the IComparable<SomeType> interface, there is no way to sort the items; in turn, we throw an InvalidCastException, indicating the type doesn’t support the requisite interface. This eliminates a key reason for having generics in the first place: to improve type safety.

To avoid this exception and instead generate a compile-time error if the type argument does not implement the interface, C# allows you to supply an optional list of constraints for each type parameter declared in the generic type. A constraint declares the characteristics that the generic type requires of the type argument supplied for each type parameter. You declare a constraint using the where keyword, followed by a parameter–requirements pair, where the parameter must be one of those declared in the generic type. The requirements describe one of three things: the class or interfaces to which the type argument must be convertible, the presence of a default constructor, or a reference/value type restriction.

Interface Constraints

To ensure that a binary tree has its nodes correctly ordered, you can use the CompareTo() method in the BinaryTree class. To do this most effectively, you should impose a constraint on the T type parameter. That is, you need the T type parameter to implement the IComparable<T> interface. The syntax for declaring this constraint appears in Listing 12.22.

Listing 12.22: Declaring an Interface Constraint
public class BinaryTree<T>
    where T : System.IComparable<T>
{
    public BinaryTree(T item)
    {
        Item = item;
    }
 
    public T Item { getset; }
    public Pair<BinaryTree<T>?> SubItems
    {
        get { return _SubItems; }
        set
        {
            switch (value)
            {
                // Null handling removed for elucidation
 
                // Using C# 8.0 Pattern Matching. Switch to
                // checking for null prior to C# 8.0
                // ...
                case
                {
                    First: { Item: T first },
                    Second: { Item: T second }
                }:
                    if (first.CompareTo(second) < 0)
                    {
                        // first is less than second
                    }
                    else
                    {
                        // second is less than or equal to first
                    }
                    break;
                default:
                    throw new InvalidCastException(
                        @$"Unable to sort the items as {
                            typeof(T) } does not support IComparable<T>.");
            };
            _SubItems = value;
        }
    }
    private Pair<BinaryTree<T>?> _SubItems;
}

While the code change in this example is minor, it moves the error identification to the compiler rather than at runtime, and this is an important difference. When given the interface constraint addition in Listing 12.22, the compiler ensures that each time you use the BinaryTree<T> class, you specify a type parameter that implements the corresponding construction of the IComparable<T> interface. Furthermore, you no longer need to explicitly cast the variable to an IComparable<T> interface before calling the CompareTo() method. Casting is not even required to access members that use explicit interface implementation, which in other contexts would hide the member without a cast. When calling a method on a value typed as a generic type parameter, the compiler checks whether the method matches any method on any of the interfaces declared as constraints.

If you tried to create a BinaryTree<T> variable using System.Text.StringBuilder as the type parameter, you would receive a compiler error because StringBuilder does not implement IComparable<StringBuilder>. The error is similar to the one shown in Output 12.3.

Output 12.3
error CS0311: The type 'System.Text.StringBuilder' cannot be used as type
parameter 'T' in the generic type or method 'BinaryTree<T>'. There is no
implicit reference conversion from 'System.Text.StringBuilder' to
'System.IComparable<System.Text.StringBuilder>'.

To specify an interface for the constraint, you declare an interface type constraint. This constraint even circumvents the need to cast to call an explicit interface member implementation.

Type Parameter Constraints

Sometimes you might want to constrain a type argument to be convertible to a particular type. You do this using a type parameter constraint, as shown in Listing 12.23.

Listing 12.23: Declaring a Class Type Constraint
public class EntityDictionary<TKey, TValue>
    : System.Collections.Generic.Dictionary<TKey, TValue>
    where TKey : notnull
    where TValue : EntityBase
{
    // ...
}

In Listing 12.23, EntityDictionary<TKey, TValue> requires that all type arguments provided for the type parameter TValue be implicitly convertible to the EntityBase class. By requiring the conversion, it becomes possible to use the members of EntityBase on values of type TValue within the generic implementation, because the constraint will ensure that all type arguments can be implicitly converted to the EntityBase class.

The syntax for the class type constraint is the same as that for the interface type constraint, except that class type constraints must appear before any interface type constraints (just as the base class must appear before implemented interfaces in a class declaration). However, unlike interface constraints, multiple base class constraints are not allowed, since it is not possible to derive from multiple unrelated classes. Similarly, base class constraints cannot specify sealed classes or non-class types. For example, C# does not allow a type parameter to be constrained to string or System.Nullable<T> because there would then be only one possible type argument for that type parameter—that’s hardly “generic.” If the type parameter is constrained to a single type, there is no need for the type parameter in the first place; just use that type directly.

Certain “special” types are not legal as class type constraints. See “Advanced Topic: Constraint Limitations,” later in this chapter, for details.

You can use System.Enum as a constraint, thereby ensuring a type parameter is an enum.8 However, you cannot specify type System.Array as a constraint. The latter restriction has minimal impact, however, as other collection types and interfaces are preferable anyway; see Chapter 15.

AdVanced Topic
Delegate Constraints

Using System.Delegate and System.MulticastDelegate allows for combining (using the static Combine() method) and separating (using the static Remove() method) delegates in a type-safe manner.9 There isn’t a strongly typed way for a generic type to invoke a delegate, but the DynamicInvoke() method can accomplish this. Internally, it uses reflection. Even though the generic type can’t invoke the delegate directly (without going through DynamicInvoke()), it is possible via a direct reference to T at compile time. For example, you could invoke a Combine() method and cast to the expected type, as shown with the pattern matching in Listing 12.24.

Listing 12.24: Declaring a Generic with a MulticastDelegate Constraint
static public object? InvokeAll<TDelegate>(
    object?[]? argsparams TDelegate[] delegates)
    // Constraint of type Action/Func not allowed
    where TDelegate : System.MulticastDelegate
{
  switch (Delegate.Combine(delegates))
  {
      case Action action:
          action();
          return null;
      case TDelegate result:
          return result.DynamicInvoke(args);
      default:
          return null;
  };
}

In this example, we attempt to cast to Action before invoking the result. If that effort is not successful, we cast to TDelegate and invoke the result using DynamicInvoke().

Note that outside the generic we would know the type of T, so we could invoke it directly after calling Combine():

Action? result =

     (Action?)Delegate.Combine(actions);

result?.Invoke();

Note the comment in Listing 12.24. While System.Delegate and System.MulticastDelegate are supported, you cannot specify a specific delegate type such as Action, Func<T>, or one of the related types.

unmanaged Constraint

The unmanaged constraint10 limits the type parameter to be of type sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool, an enum, a pointer, or any struct where all fields are unmanaged. This allows you to do things like use the sizeof (or stackalloc, as discussed in Chapter 22) operator on an unmanaged constrained type parameter.

Prior to C# 8.0, the unmanaged constraint restricted type parameters to constructed struct types—that is, value types that were not generic. However, C# 8.0 removed this constraint. You can now declare a variable of type Thing<Thing<int>> even if Thing<T> had an unmanaged constraint for T.

notnull Constraint

In Listing 12.23, there is a second constraint: the non-null constraint using the contextual keyword notnull. This constraint triggers a warning if a nullable type is specified for the type parameter decorated with notnull. In this case, for example, declaring EntityDictionary<string?, EntityBase> will lead to the following warning: Nullability of type argument 'string?' doesn't match 'notnull' constraint.

The notnull keyword cannot be combined with the struct or class constraints, which are not nullable by default (as we describe next).

struct/class Constraints

Another valuable generic constraint is the ability to restrict type arguments to be any non-nullable value type or any reference type. Rather than specifying a class from which T must derive, you simply use the keyword struct or class, as shown in Listing 12.25.

Listing 12.25: Specifying the Type Parameter as a Value Type
public struct Nullable<T> :
    IFormattable, IComparable,
    IComparable<Nullable<T>>, INullable
    where T : struct
{
    // ...
    public static implicit operator Nullable<T>(T value) => new(value);
    public static explicit operator T(Nullable<T> value) => value!.Value;
    // ...
}

Note that the class constraint restricts the type parameter to reference types including interface, delegate, or array types (and not, as the keyword might seem to imply, only class types).

In C# 8.0, a class constraint defaults to not nullable (assuming, of course, that nullable reference types are enabled). Specifying a nullable reference type parameter will trigger a warning by the compiler. You can change the generic type to allow nullable reference types by including the nullable modifier on the class constraint. Consider the WeakReference<T> class introduced in Chapter 10. Since only reference types are garbage collected, this type includes the class constraint as follows:

public sealed partial class WeakReference<T> : ISerializable

         where T : class?

{ ... }

This restricts the type parameter, T, to be a reference type—which may be nullable.

In contrast to the class constraint, the struct constraint does not allow the nullable modifier. Instead, you can specify nullability when using the parameter. In Listing 12.25, for example, the implicit and explicit conversion operators, for example, use T and T? to identify whether a non-nullable or nullable version of T is allowed. As such, the type parameter is constrained when it is used in member declarations, rather than with a type constraint.

Because a class type constraint requires a reference type, using a struct constraint with a class type constraint would be contradictory. Therefore, you cannot combine struct and class constraints.

The struct constraint has one special characteristic: Nullable value types do not satisfy the constraint. Why? Nullable value types are implemented as the generic type Nullable<T>, which itself applies the struct constraint to T. If nullable value types satisfied that constraint, it would be possible to define the nonsense type Nullable<Nullable<int>>. A doubly nullable integer is confusing to the point of being meaningless. (As expected, the shorthand syntax int?? is also disallowed.)

Multiple Constraints

For any given type parameter, you may specify any number of interface type constraints, but no more than one class type constraint (just as a class may implement any number of interfaces but inherit from only one other class). Each new constraint is declared in a comma-delimited list following the generic type parameter and a colon. If there is more than one type parameter, each must be preceded by the where keyword. In Listing 12.26, the generic EntityDictionary class declares two type parameters: TKey and TValue. The TKey type parameter has two interface type constraints, and the TValue type parameter has one class type constraint.

Listing 12.26: Specifying Multiple Constraints
public class EntityDictionary<TKey, TValue>
    : Dictionary<TKey, TValue>
    where TKey : IComparable<TKey>, IFormattable
    where TValue : EntityBase
{
    // ...
}

In this case, there are multiple constraints on TKey itself and an additional constraint on TValue. When specifying multiple constraints on one type parameter, an AND relationship is assumed. If a type C is supplied as the type argument for TKey, C must implement IComparable<C> and IFormattable, for example.

Notice there is no comma between each where clause.

Constructor Constraints

In some cases, it is desirable to create an instance of the type argument’s type inside the generic class. In Listing 12.27, for example, the MakeValue() method for the EntityDictionary<TKey, TValue> class must create an instance of the type argument corresponding to type parameter TValue.

Listing 12.27: Requiring a Default Constructor Constraint
public class EntityBase<TKey>
    where TKey: notnull
{
    public EntityBase(TKey key)
    {
        Key = key;
    }
    
    public TKey Key { getset; }
}
 
public class EntityDictionary<TKey, TValue> :
    Dictionary<TKey, TValue>
    where TKey : IComparable<TKey>, IFormattable
    where TValue : EntityBase<TKey>, new()
{
    public TValue MakeValue(TKey key)
    {
        TValue newEntity = new()
        {
            Key = key
        };
        Add(newEntity.Key, newEntity);
        return newEntity;
    }
 
    // ...
}

Because not all objects are guaranteed to have public default constructors, the compiler does not allow you to call the default constructor on an unconstrained type parameter. To override this compiler restriction, you can add the text new() after all other constraints are specified. This text, called a constructor constraint, requires the type argument corresponding to the constrained type parameter to have a public or internal default constructor. Only the default constructor constraint is available. You cannot specify a constraint that ensures that the type argument supplied provides a constructor that takes formal parameters.

Listing 12.27 includes a constructor constraint that forces the type argument supplied for TValue to provide a public parameterless constructor. There is no constraint to force the type argument to provide a constructor that takes other formal parameters. For example, you might want to constrain TValue so that the type argument provided for it must provide a constructor that takes the type argument provided for TKey, but this is not possible. Listing 12.28 demonstrates the invalid code.

Listing 12.28: Constructor Constraints Can Be Specified Only for Default Constructors
public TValue New(TKey key)
{
    // Error: 'TValue': Cannot provide arguments 
    // when creating an instance of a variable type
    TValue newEntity = null;
    // newEntity = new TValue(key);
    Add(newEntity.Key, newEntity);
    return newEntity;
}

One way to circumvent this restriction is to supply a factory interface that includes a method for instantiating the type. The factory implementing the interface takes responsibility for instantiating the entity rather than the EntityDictionary itself (see Listing 12.29).

Listing 12.29: Using a Factory Interface in Place of a Constructor Constraint
public class EntityBase<TKey>
{
    public EntityBase(TKey key)
    {
        Key = key;
    }
    public TKey Key { getset; }
}
 
public class EntityDictionary<TKey, TValue, TFactory> :
  Dictionary<TKey, TValue>
    where TKey : IComparable<TKey>, IFormattable
    where TValue : EntityBase<TKey>
    where TFactory : IEntityFactory<TKey, TValue>, new()
{
    // ...
 
    public TValue New(TKey key)
    {
        TFactory factory = new();
        TValue newEntity = factory.CreateNew(key);
        Add(newEntity.Key, newEntity);
        return newEntity;
    }
    //...
}
 
public interface IEntityFactory<TKey, TValue>
{
    TValue CreateNew(TKey key);
}

A declaration such as this allows you to pass the new key to a TValue factory method that takes parameters, rather than forcing you to rely on the default constructor. It no longer uses the constructor constraint on TValue because TFactory is responsible for instantiating value. (One modification to the code in Listing 12.29 would be to cache a reference to the factory method—possibly leveraging Lazy<T> if multithreaded support was needed. This would enable you to reuse the factory method instead of reinstantiating it every time.)

A declaration for a variable of type EntityDictionary<TKey, TValue, TFactory> would result in an entity declaration similar to the Order entity in Listing 12.30.

Listing 12.30: Declaring an Entity to Be Used in EntityDictionary<...>
public class Order : EntityBase<Guid>
{
    public Order(Guid key) :
        base(key)
    {
        // ...
    }
}
 
public class OrderFactory : IEntityFactory<Guid, Order>
{
    public Order CreateNew(Guid key)
    {
        return new Order(key);
    }
}
Constraint Inheritance

Neither generic type parameters nor their constraints are inherited by a derived class, because generic type parameters are not members. (Remember, class inheritance is the property that the derived class has all the members of the base class.) It is a common practice to make new generic types that inherit from other generic types. In such a case, because the type parameters of the derived generic type become the type arguments of the generic base class, the type parameters must have constraints equal to (or stronger than) those on the base class. Confused? Consider Listing 12.31.

Listing 12.31: Inherited Constraints Specified Explicitly
public class EntityBase<T> where T : IComparable<T>
{
    // ...
}
 
// ERROR: 
// The type 'U' must be convertible to 'System.IComparable<U>' 
// to use it as parameter 'T' in the generic type or 
// method
// class Entity<U> : EntityBase<U>
// {
//     ...
// }

In Listing 12.31, EntityBase<T> requires that the type argument U supplied for T by the base class specifier EntityBase<U> implement IComparable<U>. Therefore, the Entity<U> class needs to require the same constraint on U. Failure to do so will result in a compile-time error. This pattern increases a programmer’s awareness of the base class’s type constraint in the derived class, avoiding the confusion that might otherwise occur if the programmer uses the derived class and discovers the constraint but does not understand where it comes from.

We have not covered generic methods yet; we’ll get to them later in this chapter. For now, simply recognize that methods may also be generic and may also place constraints on the type arguments supplied for their type parameters. How, then, are constraints handled when a virtual generic method is inherited and overridden? In contrast to the situation with type parameters declared on a generic class, constraints on overriding virtual generic methods (or explicit interface) methods are inherited implicitly and may not be restated (see Listing 12.32).

Listing 12.32: Repeating Inherited Constraints on Virtual Members Is Prohibited
public class EntityBase
{
    public virtual void Method<T>(T t)
        where T : IComparable<T>
    {
        // ...
    }
}
public class Order : EntityBase
{
    public override void Method<T>(T t)
    //    Constraints may not be repeated on overriding
    //    members
    //    where T : IComparable<T>
    {
        // ...
    }
}

In the generic class inheritance case, the type parameter on the derived class can be further constrained by adding not only the constraints on the base class (required), but also other constraints. However, overriding virtual generic methods need to conform exactly to the constraints defined by the base class method. Additional constraints could break polymorphism, so they are not allowed, and the type parameter constraints on the overriding method are implied.

AdVanced Topic
Constraint Limitations

Constraints are appropriately limited to avoid nonsensical code. For example, you cannot combine a class type constraint with a struct or class constraint, or notnull with either type of constraint. Also, you cannot specify constraints that restrict inheritance to special types such as object, arrays, or System.ValueType.11 You cannot have a constraint for a specific delegate type like Action, Func<T>, or related types.

In some cases, constraint limitations are perhaps more desirable, yet they are still not supported. Only having the ability to require a default constructor is perhaps one such limitation. The following subsections provide some additional examples of constraints that are not allowed.

Operator Constraints Are Not Allowed

All generics implicitly allow for == and != comparisons, along with implicit casts to object, because everything is an object. You cannot constrain a type parameter to a type that implements a particular method or operator (other than in the aforementioned cases), except via interface type constraints (for methods) or class type constraints (for methods and operators). Because of this, the generic Add() in Listing 12.33 does not work.

Listing 12.33: Constraint Expressions Cannot Require Operators
public abstract class MathEx<T>
{
    public static T Add(T first, T second)
    {
        // Error: Operator '+' cannot be applied to 
        // operands of type 'T' and 'T'
        // return first + second;
    }
}

In this case, the method assumes that the + operator is available on all types that could be supplied as type arguments for T. But there is no constraint that prevents you from supplying a type argument that does not have an associated addition operator, so an error occurs. Unfortunately, there is no way to specify that an addition operator is required within a constraint, aside from using a class type constraint where the class type implements an addition operator.

More generally, there is no way to constrain a type to have a static method.

OR Criteria Are Not Supported

If you supply multiple interfaces or class constraints for a type parameter, the compiler always assumes an AND relationship between constraints. For example, where T : IComparable<T>, IFormattable requires that both IComparable<T> and IFormattable are supported. There is no way to specify an OR relationship between constraints. Hence, code equivalent to Listing 12.34 is not supported.

Listing 12.34: Combining Constraints Using an OR Relationship Is Not Allowed
public class BinaryTree<T>
    // Error: OR is not supported
    // where T: System.IComparable<T> || System.IFormattable
{
    // ...
}

Supporting this functionality would prevent the compiler from resolving which method to call at compile time.

________________________________________

8. Starting with C# 7.3.
9. Support added in C# 7.3.
10. Introduced in C# 7.3.
11. System.Enum (enum), System.Delegate, and System.MulticastDelegate are supported starting in C# 7.3.
{{ snackbarMessage }}
;