Constraints


When a generic type is implemented, the deferred specialty of the type parameter is unknown. Managed code has plenty of cool features, but they do not include foretelling the future. Therefore, within the implementation of a generic type, a type parameter could eventually be anything. We are assured that the type parameter inherits from System.Object—nothing else. .NET guarantees that all types are a derivation of System.Object. For this reason, type parameters are implied System.Object types and must behave accordingly. This limits type parameters to the public interface of the System.Object, which assures type safeness.

In the following code, ZClass has a single parameter, which is T. My intention is that T is an array of some type. ZClass.MethodA will enumerate that array while displaying each value. System.Array is the underlying type of an array and implements the IEnumerable interface. This is the required interface for enumeration and an essential ingredient of a foreach loop. Unfortunately, the C# compiler does not know my intention. Regardless of my future intention, T is an implied System.Object, which does not implement the IEnumerable interface. Therefore this code does not compile.

     class ZClass<T> {         public void Iterate(T data) {            foreach(object item in data) {             Console.WriteLine(item);           }         }     } 

This problem is resolved with a generic constraint. Constraints define the future intention of a type parameter. The following program uses a constraint, which is a where clause, to indicate the intention of the T parameter. It is intended that the T parameter is an IEnumerable type. With this understanding, the program compiles and executes successfully:

 using System; using System.Collections; namespace Donis.CSharpBook{     public class Starter{         public static void Main(){             ZClass<int[]> obj=new ZClass<int[]>();             obj.Iterate(new int[] {1,2,3,4});         }     }     public class ZClass<T> where T: IEnumerable {         public void Iterate(T data) {            foreach(object item in data) {             Console.WriteLine(item);           }         }     } } 

There are five types of constraints:

  • Derivation constraints state the ascendancy of a type parameter.

  • Interface constraints are interfaces that are implemented by the type parameter.

  • Value type constraints restrict a type parameter to a value type.

  • Reference type constraints restrict a type parameter to a reference type.

  • Constructor constraints stipulate that the type parameter has a default or parameterless constructor.

Constraints can be applied to both generic types and methods.

Derivation Constraint

The derivation constraint states the derivation of a type parameter. The type parameter must be derived from that constraint, which is enforced by the C# compiler. This allows the compiler to relax the restriction that access to type parameters is limited to the System.Object. The public interface of the constrained type parameter is expanded to include the derivation type. A type can inherit from a single class because multiple inheritance is not available in C#. For this reason, a type parameter can optionally have a single constraint. However, each parameter can have separate derivation constraints, in which each constraint is space-delimited.

Because the constraint is enforced by the compiler, the type parameter can only be used accordingly. This avoids an unsafe usage of the type parameter. The compiler assures that all access to the type parameter is type-safe as defined in the constraint. This is different in C++, in which the compiler performs no such type-checking on type parameters. In C++, you can basically do anything with a type parameter. Errors are uncovered when the parameter template is expanded at compile time, deep in the bowels of the expansion code. This can lead to cryptic error messages that are hard to debug. Anyone that has used the Active Template Library (ATL) and had expansion errors can attest to this. The C# compiler also updates Microsoft IntelliSense per the derivation constraint. A type parameter reflects the IntelliSense of any constraints on that parameter.

ZClass is a generic type and is defined in the following code. It has a K and V type parameter, each with a separate constraint. Per the constraints, the K parameter must be derived from XClass, while the V parameter should be derived from YClass. Main has three generic type instantiations. The first two are okay, but the third causes compile errors. The problem is the first type argument. WClass is not derived from XClass, which is a requirement of the first parameter per the constraint.

 using System; namespace Donis.CSharpBook{     public class Starter{         public static void Main(){             // good             ZClass<XClass, YClass> obj=                 new ZClass<XClass, YClass>();             // good             ZClass<XClass, WClass> obj2=                 new ZClass<XClass, WClass>();             // bad             ZClass<WClass, YClass> obj3=                 new ZClass<WClass, YClass>();         }     }     public class ZClass<K, V> where K:XClass                        where V:YClass {     }     public class XClass {     }     public class YClass {     }     public class WClass: YClass {     } } 

The following code shows a generic method with a derivation constraint:

      class ZClass {        public T MethodA<T>() where T:XClass {            return default(T);     }   } 

Generic types can function as constraints, which include both open and closed constructed types. This is demonstrated in the following code. XClass is a nongeneric type. YClass is a generic type with a single parameter. ZClass is also a generic type, also with a single parameter, where YClass<XClass> is the constraint on that parameter. YClass<XClass> is a closed constructed type. In Main, an instance of the YClass is created, where XClass is the type parameter. Next, an instance of the ZClass is created, and the parameter type is YClass<XClass>, which is required by the constraint mentioned earlier. ZClass.MethodA is then called, where the YClass instance is passed as a parameter.

 using System; namespace Donis.CSharpBook{     public class Starter{         public static void Main(){             YClass<XClass> param=new YClass<XClass>();             ZClass <YClass<XClass>> obj=new ZClass <YClass<XClass>>();             obj.MethodA(param);         }     }     public class ZClass<T> where T: YClass<XClass> {         public void MethodA(T obj) {            Console.WriteLine("ZClass::MethodA");            obj.MethodB();         }     }     public class YClass<T> {         public void MethodB() {            Console.WriteLine("YClass::MethodB");         }     }     public class XClass {         public void MethodC() {             Console.WriteLine("XClass::MethodA");         }     } } 

A type parameter can be used as a constraint. In this circumstance, one type of parameter is constraining another. You are stating that a parameter is derived from another parameter. In this code, the T1 parameter must be derived from the eventual T2 type argument:

 using System; using System.Collections; namespace Donis.CSharpBook{     public class Starter{         public static void Main(){            XClass<YClass, ZClass> obj=new XClass<YClass, ZClass>();         }     }     public class ZClass {         public void MethodA() {             Console.WriteLine("YClass::MethodA");         }     }     public class YClass: ZClass {     }     public class XClass<T1, T2> where T1:T2 {         public void MethodB(T1 arg) {         }     } } 

Nodes are the ideal generic type. You can have nodes of integers, floats, employees, or even football teams. When creating a link list, each node has reference to the next and previous node. The node should reference nodes of the same type. For an employee node, the next and previous node must also be an employee. This relationship between nodes is enforced with a recursive constraint:

     public class Node<T> where T: Node<T> {         // partial implementation         public T Previous {             get {                 return default(T);             }             set {             }         }         public T Next {             get {                 return default(T);             }             set {             }         }     } 

The type parameter cannot exceed the visibility of the constraint. In the following code, XClass has internal accessibility and is visible in the current assembly alone. The T parameter is public and is visible outside the current assembly. The accessibility of the T parameter exceeds XClass. For this reason, it is an error to use XClass as a constraint on the type parameter.

     public class ZClass<T> where T: XClass{     }     internal class XClass {     } 

In addition, value types cannot be used as constraints, which is a good introduction to a limitation with generics. Look at the following code, which will not compile. Why not?

     public class Arithmetic<T> {         public T Cubed (T number) {             return number*number*number;         }     } 

As with any parameter, the T parameter is an inferred System.Object type. System.Object does not have an operator *. Therefore, "number*number*number" will not compile. An integer constraint should resolve the problem, which would confirm that the type parameter is an integer. Integers have an operator *. However, value types and primitives are not valid constraints. The following code will not compile:

     class Arithmetic<T> where T: System.Int32 {         public T Cubed (T number) {             return number*number*number;         }     } 

The inability to use standard operators with value types is a major limitation to generics. The workaround is implementing named operators, such as Add, Multiply, and Divide, as members of the generic type.

In addition to value types, there are other restrictions on constraints. The following types cannot be used as constraints:

  • Sealed classes

  • Open constructed types

  • Primitive types

  • System.Array

  • System.Delegate

  • System.Enum

  • System.ValueType

Interface Constraints

Interfaces can also be constraints, which requires that the type argument implement the interface. Although a type parameter can have at most one derivation constraint, it can have multiple interface constraints. This is logical because a class can inherit a single base class but can implement many interfaces. The syntax for an interface constraint is identical to a derivation constraint. Class and interface constraints can be combined in a list of constraints. However, a class constraint should precede interface constraints in the list.

Interface and derivation constraints share many of the same rules and restrictions, such as the visibility of the interface constraint exceeding that of the type parameter.

In the following code, the find capability has been added to the Sheet collection. The Find method returns an array of cells that contain a certain value. A comparison is made between the cell and value, where both are the type indicated in the type argument. Types that implement the IComparable interface support comparisons. If a comparison is equal, IComparable .CompareTo returns 0. To support this behavior, an interface constraint for IComparable is added to the type parameter. This is a partial implementation of the Sheet collection that shows Find and related methods. (Some of the code shown previously is omitted.)

 using System; namespace Donis.CSharpBook{     public class Starter{         public static void Main(){             Sheet<int> asheet=new Sheet<int>(5);             for(byte row=1; row<6; ++row) {                 for(byte col=1; col<6; ++col) {                     asheet[row,col]=row*col;                 }             }             Cell [] found=asheet.Find(6);             foreach(Cell answer in found) {                 Console.WriteLine("R{0} C{1}",                     answer.row, answer.col);             }         }     }     public struct Cell {         public byte row;         public byte col;     }     public class Sheet<T> where T: IComparable {                     public Cell[] Find(T searchValue) {             int total=Count(searchValue);             int counter=0;             Cell [] cells=new Cell[total];             for(byte row=1; row<=m_Dimension; ++row) {                 for(byte col=1; col<=m_Dimension; ++col) {                     if(m_Sheet[row-1,col-1].CompareTo(searchValue)==0) {                         cells[counter].row=row;                         cells[counter].col=col;                         ++counter;                     }                 }             }             return cells;         }         public int Count(T searchValue) {             int counter=0;             for(byte row=1; row<=m_Dimension; ++row) {             for(byte col=1; col<=m_Dimension; ++col) {                     if(m_Sheet[row-1,col-1].CompareTo(searchValue)==0) {                         ++counter;                     }                 }             }             return counter;         }              } } 

This code works, but there is a subtle problem. The IComparable interface manipulates objects, which causes boxing and unboxing when working with value types. This could become expensive in a large collection of value types. In the preceding code, the type argument is an integer, which is a value type. This causes boxing with the IComparable interface. Generic interfaces obfuscate this problem. .NET Framework 2.0 includes several general-purpose generic interfaces for developers. This is the class header updated for the IComparable generic interface:

     public class Sheet<T> where T: IComparable<T> 

Value Type Constraint

A value type constraint restricts a type parameter to a value type. Value types are derived from the System.ValueType type. Primitives and structures are examples of value types. The exception is the Nullable type. The Nullable type is a value type, but it is not allowed with a value type constraint. A value type constraint is a constraint using the struct keyword.

The following code demonstrates the value type constraint. The commented source line uses a reference type, which would cause compile errors because of the value type constraint.

 using System; using System.Collections; namespace Donis.CSharpBook{     public class Starter{         public static void Main(){             ZClass<int> obj1=new ZClass<int>();             // ZClass<XClass> obj2=new ZClass<XClass>(); [illegal]         }     }     public class ZClass<T> where T: struct {         public void Iterate(T data) {         }     }     public class XClass{     } } 

Reference Type Constraint

A reference type constraint restricts a type parameter to a reference type. Reference types are generally user-defined types, including classes, interfaces, delegates, and array types. A reference type constraint uses the class keyword.

The following code has a reference type constraint. Although this code is similar to the code presented in the previous section, a reference type constraint is used instead of a value type constraint. For this reason, the illegal line has moved. You cannot use an integer type with a reference type parameter.

 using System; using System.Collections; namespace Donis.CSharpBook{     public class Starter{         public static void Main(){             // ZClass<int> obj1=new ZClass<int>(); [illegal]             ZClass<XClass> obj2=new ZClass<XClass>();             }         }         public class ZClass<T> where T: class {             public void Iterate(T data) {             }         }         public class XClass{         }     } 

Default Constructor Constraint

Will this code compile? It looks fairly innocuous:

   class ZClass<T> {        public void MethodA() {            T obj=new T();        }   } 

This code does not compile. The problem is the default constructor. Although prevalent, not every type has a default constructor. A default constructor, or a constructor with no arguments, assigns a default state to an object. The default constructor is called with a parameterless new operator. However, because a default constructor is not guaranteed, the new operator is not universally applicable. Therefore, the new operator is disallowed on type parameters.

The solution is the constructor constraint. The derivation constraint does not help with constructors because derived types do not inherit constructors for the base class. Constructor constraints mandate that a type parameter have a default constructor, which is confirmed at compile time. This allows the new operator to be used with the type parameter. The constructor constraint is added to the where clause and is a new operator. When combined with other constraints, the default constructor constraint must be the last item in the constraint list. The constructor constraint applies only to the default constructor. You are still prevented from using constructors with arguments.

Here is sample code of the constructor constraint. The constructor constraint is used on the ZClass.

 using System; namespace Donis.CSharpBook{     public class Starter{         public static void Main(){             ZClass obj=new ZClass();             obj.MethodA<XClass>();         }     }     public class ZClass {         public void MethodA<T>()                      where T:XClass, new() {             Console.WriteLine("ZClass.MethodA");            T obj=new T();            obj.MethodB();         }     }     public class XClass{         public void MethodB() {             Console.WriteLine("XClass.MethodB");         }     } } 




Programming Microsoft Visual C# 2005(c) The Language
Microsoft Visual Basic 2005 BASICS
ISBN: 0619267208
EAN: 2147483647
Year: 2007
Pages: 161

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net