Sunday, 24 May 2015

Specifying Constraints in Generics



This article discusses the problems with Generics and how to tackle them with out of the box solutions provided with .NET Framework 2.0
Introduction
Generics, no doubt, is one of the wonderful and refreshing features introduced in Microsoft .NET Framework 2.0. Generics can be used to produce strictly type safe data structures, thus providing enormous performance optimization. Introduction of generics brought a near end to the use of boxing/unboxing to store types in data structures.
This article assumes knowledge of C# 2.0.
Syntax
The syntax of using generics is:
Hide   Copy Code
//usage with class
class MyClass< T >
{
  ...
}

//usage with method
void MyMethod< U >()
{
  ...
}
Problems with Generics
Let us consider the following example:
Hide   Copy Code
//this method returns if both the parameters are equal
public static bool Equals< T > (T t1, Tt2)
{
  return (t1 == t2);
}
Apparently, this code is a very good usage of generics. This method can be used to compare the equality of any two similar types. The problem here is that, the IL generated to compare say an integer and a string is different. int comparison is just check the values on the stack, whereas it is not as simple for string. So the IL generated for Equals<string> would be different to that of Equals<int>.

The case may be even different if the types being compared have a new definition of
== operator. Because of this, you may not use the == operator to compare any generic object references without having any type of restrictions/constraints on them.
Solution
There are two possible solutions for this (that were bundled out of the box) with C#:
  1. Runtime casting
  2. Restricting the allowable types while declaring the generic type
Runtime casting (a.k.a. yuck!), sometimes, can be a good fit here. In this, the CLR will cast the types at the runtime dynamically thus ensuring the similar functional behavior throughout the application. But, this certainly is not the best way always, especially not when the types being used are overriding the default behavior of the operators (just an example) involved in the operation.
The best fit, for most of the cases would certainly be having some kind of restriction on what types should be allowed to be replaced in the generic type. In .NET, they are called constraints.
Constraints are represented in C# using the where keyword. The following is the syntax:
Hide   Copy Code
public bool Compare< T > (T t1, Tt2)
       where T : IComparable
{
  ...
}
Some of the ways we can use constraints are as follows:
  1. Specifying the type to be a reference type:
public void MyMethod< T >()
       where T : class
{
  ...
}
Please note that class is the keyword here and should be used in the same case. Any difference in the case will lead to a compilation error.
  1. Specifying the type to be a value type:
public void MyMethod< T >()
       where T : struct
{
  ...
}
Please note that struct is the keyword here and should be used in the same case. Any difference in the case will lead to a compilation error.
  1. Specifying a constructor as a constraint:
public void MyMethod< T >()
        where T : new ()
{
  ...
}
Please note that  only a default constructor can be used in the constraints and using any parameterised constructor will be a compilation error.
  1. Specifying a static base class as a constraint:
public void MyMethod< T >()
       where T : BaseClass
{
  ...
}
  1. Specifying a generic base class as a constraint:
public void MyMethod< T, U >()
       where T : U
{
  ...
}
Points to Ponder
Though this list seems to be insufficient considering the complex scenarios, these can be mixed appropriately.
The below is a valid combination of constraints:
public void MyMethod< T >()
       where T : IComparable, MyBaseClass, new ()
{
  ...
}

//Here we are using multiple constraints.
//using IComparable and MyBaseClass would be case 4, using new() would be case 3.
The below is an example for an invalid combination of constraints:
public void MyMethod< T >()
       where T : class, struct
{
  ...
}

//here we are trying to specify the
//generic type as both reference and value type.
Summary
Generics are a wonderful feature to work with and should be used wherever appropriate for better optimization of the applications.
Please get back to me for any additional information/suggestions/clarifications/shortcomings regarding this topic.
Reader Level:

Constraints in Generics

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 willthrow 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.


No comments:

Post a Comment