Constraints are used in Generics to restrict the types that can be substituted for type parameters. Here we will see some of the commonly used types of constraints.
When we create a new instance of a generic type we can restrict the types we can substitute for type parameters using constraints. If we try to substitute a type that does not comply with the constraint then we get a compile-time error. We specify constraints using the where clause. The following are some of the main types of constraints we can use with generics:
where T: struct value type constraint
where T: class reference type constraint
where T: new() default parameter constraint
where T: <interface name> interface constraint
Value type constraint
This type of constraint specifies that the type argument should be a value type. If we try to substitute a non-value type for the type argument then we will get a compile-time error. If we declare the generic class using the following code then we will get a compile-time error if we try to substitute a reference type for the type parameter.
public class GenericType<T> where T:struct
{
The following line will throw a compile-time error. This is because string is a reference type, so because of the value type constraint it is not valid to substitute a string as the type parameter.
GenericType<string> gnericObj = new GenericType<string>(“ashish”);
Reference type constraint
This type of constraint specifies that the type argument should be a reference type. If we try to substitute a non-reference type for the type argument then we will get a compile-time error. If we declare the generic class using the following code then we will get a compile-time error when we try to substitute a value type for the type parameter.
public class GenericType<T> where T:class
The following line will throw a compile-time error since int is a value type:
GenericType<int> gnericObj = new GenericType<int>(1);
But the following line will compile properly since string is a reference type:
GenericType<string> gnericObj = new GenericType<string>(“ashish”);
We can implement other types of constraints in a similar manner. “Default constructor constraint” means that the type should have a default constructor and “interface constraint” means that the type should implement a specific interface type.
There are many advantage of using constraints. One of the biggest advantages is that in the case of an interface constraint we are sure that a type implements a specified method so we can call that method inside the generic type. Other constraints like the reference type and the value type constraints let us determine whether we can perform actions suitable for reference or value types. Such as if we apply the reference type constraint then we can use the null value check to determine if the type parameter points to a valid object.
.NET has many collection classes in the System.Collections namespace. Also these collection classes have many generic counterparts in the System.Collections.Generic namespace. These generic collections provide better type safety and performance than their non-generic counterparts.
The generic collection classes use various types of constraints. For example the “Icomparable<T>” interface sorts and determines the equality of items. The Icomparable interface has a “CompareTo” method that implementing classes should provide. So we can be sure that if a class implements Icomparable<T> then it can be sorted if used in a collection and also compared for equality.