Constraints

When a type parameter is defined, the eventual type argument is unknown. The type parameter could become anything. Only one thing is guaranteed. The type argument will inherit System.Object, which limits the functionality of the type argument to the System.Object interface.

In the following code, ZClass is generic and has a single type parameter, which is T. The code expects that T will be a collection. ZClass.MethodA will enumerate that collection and display each item. Collections should implement the IEnumerable interface. This is also necessary for the foreach loop. Unfortunately, the C# compiler spots a problem. Regardless of how the code expects to use T, 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) {       // invalid
        foreach (object item in data) {
            Console.WriteLine(item);
        }
    }
}

This problem is resolved with a generic constraint. Constraints define the intent of a type parameter. The following program uses a constraint, defined in the where clause shown in bold type, to indicate the intention of the T parameter. It is intended that the T parameter will be related to the 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. Specify Interfaces that must be 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 must have a default or parameterless constructor

Constraints can be applied to both generic types and generic methods.

Derivation Constraints

A derivation constraint requires that the type argument be related to a specified type. The constraint is enforced by the C# compiler. By default, the derivation constraint is System.Object, from which everything is derived. Value types cannot be used as constraints. A derivation constraint makes a generic type more specific. Because C# supports single but not multiple inheritance, derivation constraints are restricted to a single constraint. However, each type parameter of a generic type can have a different constraint. A type parameter list is comma-delimited. A derivation constraint list is space-delimited. See the following example:

public class ZClass<K, V> where K : XClass where V : XClass {
    void MethodA() {
    }
}

The compiler requires that type arguments adhere to the constraint, which makes the type argument type-safe. This is different than in C++, where 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 such that it suggests only items that meet the derivation constraint.

In the following code, ZClass is a generic type. It has a K and V type parameter, each with a separate, space-delimited constraint. Per the constraints, the K parameter must be derived from XClass and the V parameter must be derived from YClass. Main instantiates three generic types. The first two are all right, but the third causes compiler errors. The problem is the first type argument. WClass is not derived from XClass, which is a requirement of the first type 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 {
    }
}

Constraints also can be applied to generic methods. The following code shows a generic method with a derivation constraint:

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

A generic type can be used as a constraint. This includes both open and closed constructed types. This is demonstrated in the following code. XClass is a non-generic type, and YClass is a generic type with a single type parameter. ZClass is a generic type, also with a single type parameter, where YClass<XClass> is the constraint on that type parameter. YClass<XClass> is a closed constructed type. In Main, an instance of YClass<XClass> is created first. Next, an instance of ZClass is created, where the parameter type is YClass<XClass>, which is required by the aforementioned constraint. ZClass. MethodA is then called, where the YClass instance is passed as a parameter. As required by the constraint, this type argument is related to YClass<XClass>.

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 one parameter is derived from another parameter. In this code, the T1 parameter must be derived from T2:

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) {
        }
    }
}

An implementation of a linked list is ideal for a generic type. You can have nodes of integers, floats, employees, or even football teams. When creating a linked list, each node keeps a reference to the next and previous nodes. The nodes of the linked list consist of related types. For an employee linked list, the nodes must also be related to the Employee type, such as HourlyEmployee, SalariedEmployee, or RetiredEmployee. 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 argument 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 apply XClass as a constraint on the T type parameter.

public class ZClass<T> where T : XClass { // invalid
}

internal class XClass {
}

Look at the following code, which will not compile:

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

Why doesn’t it compile? As with any parameter, the T parameter is an inferred System.Object type. System.Object does not have an operator * symbol for multiplication. Therefore, number * number * number will not compile. An integer constraint should resolve this problem, which would force the type parameter to be an integer. Integers have a * operator. However, as previously stated, value types and primitives are not valid constraints. The following code also will not compile:

class Arithmetic<T> where T : System.Int32 { // invalid
    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, 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

Type constraints can also require that the type argument implement an interface. Although a type parameter can have a maximum of only one derivation constraint, it can have multiple interface constraints. This is expected because classes 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. Class constraints should precede any interface constraints in the type constraint list.

Interface and derivation constraints share many of the same rules and restrictions, for example the visibility of the interface constraint must equal or exceed 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 same type, as indicated in the type argument. Types that implement the IComparable interface support comparisons. If a comparison is equal, IComparable.CompareTo returns 0. IComparable.CompareTo is called to compare values in the sheet. To enforce this behavior, an interface constraint of IComparable is added to the type parameter. The following is a partial implementation of the Sheet collection that includes Find and related methods. (Some of the code already shown in this chapter 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 would fix this problem. The .NET FCL 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 Constraints

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. A value type constraint uses the struct keyword.

The following code demonstrates the value type constraint shown in bold type. The line of code that is commented out uses a reference type, which would cause compiler 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 Constraints

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

The following code has a reference type constraint, shown in bold type. 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 in this version of the previous code. You cannot use an integer type argument with a reference type constraint:

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 Constraints

Will this code compile? It looks fairly innocuous:

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

But the code does not compile. Why not? The problem is the default constructor. A default constructor, or a constructor with no arguments, sets the default state of an object. Not every type has a default constructor. The default constructor is called with a parameterless new operator. Because not every type has a default constructor, the parameterless new operator is not universally applicable. Therefore, unless explicitly allowed, the new operator is disallowed on type arguments.

The solution is the default constructor constraint. The derivation constraint does not help with constructors because a derived type does not inherit constructors from its base class. Constructor constraints mandate that a type parameter have a default constructor (which might be implicit rather than explicit). The constraint is confirmed at compile time. This allows the new operator to be used with the type argument. 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 and is separated by a comma. You still are prevented from using constructors with arguments.

Here is sample code showing the constructor constraint in bold type. The constructor constraint is used in 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");
        }
    }
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.164.34