Generic Parameter Constraints for C#
Enforcing constraints on specific type parameters allows you to increase the number of allowable operations and method calls.
Dec 28, 2019 • 7 Minute Read
Introduction
Generic became part of C# with version 2.0 of the language and the CLR, or Common Language Runtime. It has introduced the concept of type parameters, which allow you to design classes and methods that defer the specification of one or more types until the class or method is declared and instantiated by client code.
In this guide, we will walk through how to create such classes and methods and discuss the constraints that are posed to the implementation by the CLR.
Generics
An example could look like this:
public class GenericArray(T)
{
public void Summarize(T input);
}
These classes and methods combine three distinct features:
- Reusability
- Type safety
- Efficiency
Generics are most often used in conjunction with collections and methods that operate on them.
A new namespace has been introduced, called System.Collections.Generic, which contains several new generic-based collection classes. It is also possible to create custom generic types and methods for your own solutions and to customize design patterns that are type-safe and efficient.
Parameters
The type parameter is a placeholder for a specific type that the client specifies when they create an instance of the generic type. A generic class cannot be used as-is because it is simply a blueprint for that type. We need to declare, then instantiate a constructed type by specifying a type argument between angle brackets, or <>. This type must be recognizable to the compiler. You can create any type of instance—there is no limitation.
For example:
GenericArray<int> myFloatGenericArray = new GenericArray<int>();
GenericArray<string> myFloatGenericArray = new GenericArray<string>();
The above example shows the instantiation for the blueprint of **GenericArraywithintandstringtypes. In each case, the parameterT` is substituted at runtime with a type argument, which allows us to create a type-safe and efficient object using only one class definition.
Some rules on naming:
- Generic type parameters should have descriptive names.
- Prefix descriptive type parameters with T, something like this: <TConfigurationItem>
Constraints
Constraints are specific rules that inform the compiler about capabilities a type argument must have. Without these, a type argument could be any type. The compiler by default assumes the System.Object class, which is the ultimate base class for any .NET type. You could say it's the motor of base classes. When a client code wants to instantiate a class with a type that is not allowed by any constraint, it will result in a compile-time error. In order to define a constraint, we need to use the where keyword, which is always context-based.
There are a total of eight constraint types:
- where T : struct: The argument must be a value type except Nullable<T>.
- where T : class: The argument must be a reference type, and it applies to classes, interfaces and delegates, even arrays.
- where T : notnull: The argument must be non-nullable.
- where T : unmanaged: The argument must be an unmanaged type, a.k.a. pointers and unsafe code.
- where T : base-class: The argument must be derived from a base class of type, or base class itself.
- where T : new(): The argument must have a parameterless constructor, which is public.
- where T : interface: The argument must be or implement a specified interface.
- where T : U: The argument must be or derive from the argument supplied for U.
You need to know that some of these constraints are mutually exclusive. The value types must have an accessible, parameterless constructor. For example, the struct constraint implies the new() constraint, and the new() constraint can't be combined with the struct constraint.
Why Use Constraints?
Enforcing constraints on specific type parameters allows you to increase the number of allowable operations and method calls to those supported by the constraining type and types that are in its inheritance hierarchy. The design of generic classes and methods means you will perform any operation on the generic member beyond simple assignment, or instantiation or calling methods not supported by the System.Object base class. You need constraints to enforce restrictions. A very practical example is when you use a base class constraints to tell the compiler that only specific type will be used as type argument for any derived class. This guarantee allows the compiler to allow methods of that type to be called in the generic class.
A Simple Example
using System;
using System.Collections.Generic;
namespace cnstraints
{
class GenericClass<T> where T : class
{
private readonly T _field;
public GenericClass(T value){
this._field = value;
}
public T genericMethod(T parameter) {
Console.WriteLine($"The type of parameter we got is: {typeof(T)} and value is: {parameter}");
Console.WriteLine($"The return type of parameter is: {typeof(T)} and value is: {this._field}");
return this._field;
}
}
class Program
{
static void Main(string[] args)
{
GenericClass<string> myGeneric = new GenericClass<string>("Hello World");
myGeneric.genericMethod("string");
Console.ReadKey();
}
}
}
Calling the following code gives us this output.
The type of parameter we got is: System.String and value is: string
The return type of parameter is: System.String and value is: Hello World
Let's investigate what is happening here. We have a generic class with the where T : class constraint. This means the argument must be a reference type and applies to classes, interfaces and delegates, even arrays. We also have a generic method, which takes a single parameter and outputs some strings.
Conclusion
In this guide, you became familiar with the concept of generics and their constraints. We know when generics were born and what type of constraints are present on this datatype. This is just scratching the surface of this topic, which can become a rabbit hole when you dig deeper to it to try to find a generic solution for your problems. You should take that into consideration when you want to go this road. For example, a simple application for a single purpose can exist without any generic implementation, but in a big corporate application, it might be a good idea to consider this facility. The decision is yours to make.
I hope this has been informative for you and I hope you found what you were looking for. If you liked this guide, give it a thumbs up!