Table of contents >> Objects > Generic constraints
Medium importance article

At their most basic definition, generic type constraints are just ways in which we can perform a sort of validation on the concrete types that can be used on the generic type. This will have a visible effect only on the types that we can substitute for the generic types. Here is the basic syntax of one or more constraints used on generic types:

1
SomeGenericTypeOrMemberName<T> where T : constraint1, constraint2, constraintN

These constraints have the same syntax whether we use them on generic types directly (classes, etc), or on generic members (methods, properties, etc). Let's have a practical example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System;
 
namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            var result = GetANewT<Program>();
            
            Console.ReadLine();
        }
        
        static T GetANewT<T>()
        {
            T someReturnValue = new T();
            return someReturnValue;
        }
    }
}

When you will try to compile the above code, you will see that you will get a compiler error: Cannot create an instance of the variable type 'T' because it does not have a new() constraint.

The explanation for the error is the fact that, as explained in the previous lesson, generic types can be any type. Observe the following example for an explanation of why this cannot be, in our previous example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System;
 
namespace HelloWorld
{
    class SomeClass { public SomeClass() { } }
    class SomeOtherClass { public SomeOtherClass(int i, char c) { } }
    
    class Program
    {
        static void Main(string[] args)
        {
            var result1 = GetANewT<SomeClass>();
            var result2 = GetANewT<SomeOtherClass>();
            
            Console.ReadLine();
        }
        
        static T GetANewT<T>()
        {
            T someReturnValue = new T();
            return someReturnValue;
        }
    }
}

Notice that when I call GetANewT with a type of SomeClass, the T generic type becomes SomeClass. So, actually, inside the GetANewT method, I'm really actually saying SomeClass someReturnValue = new SomeClass();. Well, if you look at SomeClass, it does have a parameterless constructor, which means we can instantiate it without issues. But, when it comes to using GetANewT with a type of SomeOtherClass, this class doesn't have a parameterless constructor, it requires two arguments, one of type int, one of type char. This means that the compiler does not, and cannot know what these parameters are, and what values should it give to them, when it gets to instantiating this class.

In addition to this problem, just think about it: what if I call GetANewT with a type of int or bool? Sure, using the int type with the new keyword is allowed, but this is not the sane way of using numeric types, as we have seen countless times before.

Now, let's modify the above code by adding a special generic constraint:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System;
 
namespace HelloWorld
{
    class SomeClass { public SomeClass() { } }
    class SomeOtherClass { public SomeOtherClass(int i, char c) { } }
    
    class Program
    {
        static void Main(string[] args)
        {
            var result1 = GetANewT<SomeClass>();
            var result2 = GetANewT<SomeOtherClass>();
            
            Console.ReadLine();
        }
        
        static T GetANewT<T>() where T : new()
        {
            T someReturnValue = new T();
            return someReturnValue;
        }
    }
}

One immediate effect you will notice is that the error inside the GetANewT disappears. This is because the now, the method requires the generic parameter to only accept types that have a parameterless constructor: where T : new().

The second thing that you observe is that the error moved inside the Main method, where we call GetANewT with a type of SomeOtherClass. This happens because, as I have just explained, it only accepts now types that have a parameterless constructor. SomeOtherClass does not.

If you somehow imagined or hoped that we can specify types with constructors that require parameters, in our case, something like:

1
where T : new(int i, char c)

you are in no luck. Microsoft decided not to go to this extent, so you will get a compiler error.

Anyway, there are more kinds of constraints available, this is the complete list:

  • where T : new() - the generic type must be a reference type, and it must have a default (public and parameterless) constructor.
  • where T : struct - the generic type must be a non-nullable value type, such as all primitive data types, with the exception of string (int, float, bool, etc). You can't combine the struct constraint with the unmanaged constraint.
  • where T : class - the generic type must be a reference type, such as any non-nullable class, interface, delegate or array.
  • where T : class? - the generic type must be a reference type, such as any nullable or non-nullable class, interface, delegate or array.
  • where T : SomeClassType - the generic type must be or derive (inherit) from the specified base class. Because C# does not support multiple inheritance, only one class type constraint can be specified. In a nullable context, T must be a non-nullable reference type derived from the specified base class.
  • where T : SomeClassType? - the generic type must be or derive (inherit) from the specified base class. Because C# does not support multiple inheritance, only one class type constraint can be specified. In a nullable context, T must be a non-nullable reference type derived from the specified base class.
  • where T : SomeInterfaceType - the generic type must be or implement the specified interface. Multiple interface constraints can be specified. The constraining interface can also be generic. In a nullable context, T must be a non-nullable type that implements the specified interface.
  • where T : SomeInterfaceType? - the generic type must be or implement the specified interface. Multiple interface constraints can be specified. The constraining interface can also be generic. In a nullable context, T may be a nullable reference type, a non-nullable reference type, or a value type. T may not be a nullable value type.
  • where T : U - the generic type T must be or derive from the type provided for the generic type U. In a nullable context, if U is a non-nullable reference type, T must be a non-nullable reference type. If U is a nullable reference type, T may be either nullable or non-nullable.
  • where T : notnull - the generic type must be a non-nullable type. The argument can be a non-nullable reference type or a non-nullable value type.
  • where T : default - this constraint resolves the ambiguity when you need to specify an unconstrained type parameter when you override a method or provide an explicit interface implementation. The default constraint implies the base method without either the class or struct constraint.
  • where T : unmanaged - the generic type must be a non-nullable unmanaged type. The argument can be a non-nullable reference type or a non-nullable value type. This constraint implies the struct constraint and can't be combined with either the struct or new() constraints.

Most of the notions highlighted above are not familiar to you, because we haven't learned about them yet. However, refer back to this list every once in a while, when you have mastered more of the language.

One other advantage of constraints, aside from the fact that they... constrain us into using only specific data types, is that once these generic types are constrained, they behave in the limits of the constraints. For instance, if a generic type is constrained to only accept a specific type of class (we will learn about inheritance in the future), the compiler will actually know that only that type of class can be passed through that generic argument, and it will allow you tu use members of that specific class, as if you asked for a parameter of that classes' type. If I create a generic method where the generic parameter is constrained to only accept classes of type Program, then, inside that method, I can use the members of the Program class, such as the Main method, through that generic parameter. The compiler recognizes that only stuff of type Program can be passed through the constraint, so it considers the generic type as if it actually is a Program type, thus allowing us to access its members without any conversions or casting.

As an advanced thought process, which you don't necessarily have to understand right now (especially because it relies on stuff that we will learn much later), consider this example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using System;
 
namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            TakeSomeValue(6);
            TakeSomeValue<int>(6);
            
            Console.ReadLine();
        }
        
        static void TakeSomeValue<T>(T arg) where T : IComparable
        {
        }
        
        static void TakeSomeValue(IComparable arg)
        {
        }
    }
}

The above two methods TakeSomeValue behave in the same exact way, they both can be called with numbers, because, as we will learn much later, int implements the IComparable interface, so it can pass through the parameter of type IComparable of the method. The same thing happens in the case of the generic method, where the constraint ensures that the generic type implements the IComparable interface. So, if we can achive the same result, why use the generic version? In fact, I find the non-generic version to be much more readable than the generic one, don't you agree?

However, there are three things to consider here:

  • When using the generic method, the constraint only checks if the type of the passed parameter fulfills its requirement, but does not act in any way upon it; the int value arrives still as an int, and can be used so, as explained above.
  • When using the non-generic version, only the part that is the IComparable is "cut" from the int, in a process called boxing. If we want the value to be treated as an int again, and not as an IComparable, we need to cast it back to an int, in a process called unboxing. This might be irrelevant in most cases, but it might make a difference in some certain cases.
  • The non-generic version is not as versatile as the generic one, once the type we want to give to the method needs to meet more than one requirements. If we want it to implement IComparable, but also IEnumerable, the non generic version will fall short, since it cannot easily fulfill these two different requirements.