Constrained Types


In the preceding examples, the type parameters could be replaced by any type. For example, given this declaration:

 class Gen<T> {

any type can be passed to T. Thus, it is legal to create Gen objects in which T is replaced by int, double, string, FileStream, or any other type. Although having no restrictions on the type argument is fine for many purposes, sometimes it is useful to limit the types that can be passed to a type parameter. For example, you might want to create a method that operates on the contents of a stream, including a FileStream or MemoryStream. This situation seems perfect for generics, but you need some way to ensure that only stream types are used as type arguments. You don’t want to allow a type argument of int, for example. You also need some way to tell the compiler that the methods defined by a stream will be available for use. For example, your generic code needs some way to know that it can call the Read( ) method.

To handle such situations, C# provides constrained types. When specifying a type parameter, you can specify a constraint that the type parameter must satisfy. This is accomplished through the use of a where clause when specifying the type parameter, as shown here:

 class class-name<type-param> where type-param : constraints { // ...

Here, constraints is a comma-separated list of constraints.

There are five types of constraints:

  • You can require that a certain base class be present in a type argument by using a base class constraint. This constraint is specifi ed by naming the desired base class.

  • You can require that one or more interfaces be implemented by a type argument by using an interface constraint. This constraint is specified by naming the desired interface.

  • You can require that the type argument supply a parameterless constructor. This is called a constructor constraint. It is specified by new( ).

  • You can specify that a type argument must be a reference type by specifying the reference type constraint: class.

  • You can specify that the type argument be a value type by specifying the value type constraint: struct.

Of these constraints, the base class constraint and the interface constraint are probably the most often used, with the remaining adding fine-grained control. Each of the constraints is examined in detail in the following sections.

Using a Base Class Constraint

The base class constraint enables you to specify a base class that a type argument must inherit. A base class constraint serves two important purposes. First, it lets you use the members of the base class specified by the constraint within the generic class. For example, you can call a method or use a property of the base class. Without a base class constraint, the compiler has no way to know what members a type argument might have. By supplying a base class constraint, you are letting the compiler know that all type arguments will have the members defined by the specified base class.

The second purpose of a base class constraint is to ensure that only type arguments that support the specified base class can be used. This means that for any given base class constraint, the type argument must be either the base class, itself, or a class derived from that base class. If you attempt to use a type argument that does not inherit the specified base class, a compile-time error will result.

The base class constraint uses this form of the where clause:

 where T : base-class-name 

Here, T is the name of the type parameter, and base-class-name is the name of the base class. Only one base class can be specified.

Here is a simple example that demonstrates the base class constraint mechanism:

 // A simple demonstration of a base class constraint. using System; class A {   public void hello() {     Console.WriteLine("Hello");   } }

 // Class B inherits A. class B : A { } // Class C does not inherit A. class C { } // Because of the base class constraint, all type arguments // passed to Test must have A as a base class. class Test<T> where T : A {   T obj;   public Test(T o) {     obj = o;   }   public void sayHello() {     // OK to call hello() because it's declared     // by the base class A.     obj.hello();   } } class BaseClassConstraintDemo {   public static void Main() {     A a = new A();     B b = new B();     C c = new C();     // The following is valid because     // A is the specified base class.     Test<A> t1 = new Test<A>(a);     t1.sayHello();     // The following is valid because     // B inherits A.     Test<B> t2 = new Test<B>(b);     t2.sayHello();     // The following is invalid because     // C does not inherit A. //    Test<C> t3 = new Test<C>(c); // Error!   } }

In this program, class A is inherited by B, but not by C. Notice also that A declares a method called hello( ). Next, notice that Test is a generic class that is declared like this:

 class Test<T> where T : A {

The where clause stipulates that any type argument passed to T must have A as a base class.

Now notice that Test declares the method sayHello( ), shown next:

 public void sayHello() {   // OK to call hello() because it's declared   // by the base class A.   obj.hello(); }

This method calls hello( ) on obj, which is a T object. The key point is that the only reason that hello( ) can be called is because the base class constraint ensures that any type argument passed to T will inherit A, which declares hello( ). If the base class constraint had not been used, the compiler would have no way of knowing that a method called hello( ) was defined for all objects of type T. You can prove this for yourself by removing the where clause. The program will no longer compile because the hello( ) method will be unknown.

In addition to enabling access to members of the base class, the base class constraint enforces that only types that inherit the base class can be passed as type arguments. This is why the following line is commented out:

 //    Test<C> t3 = new Test<C>(c); // Error!

Because C does not inherit A, it can’t be used as a type argument when constructing a Test object. You can prove this by removing the comment symbol and trying to recompile.

Before continuing, let’s review the two effects of a base class constraint: A base class constraint enables a generic class to access the members of the base class. It also ensures that only those type arguments that fulfill this constraint are valid, thus preserving type safety.

Although the preceding example showed the “how” of base class constraints, it did not show the “why.” To better understand their value, let’s work through another, more practical example. Assume that you want to create a mechanism that manages lists of telephone numbers. Furthermore, assume that you want to use different lists for different groupings of numbers. Specifically, you want one list for friends, another for suppliers, and so on. To accomplish this, you might start by creating a base class called PhoneNumber that stores a name and a phone number linked to that name. Such a class might look like this:

 // A base class that stores a name and phone number. class PhoneNumber {   string name;   string number;   public PhoneNumber(string n, string num) {     name = n;     number = num;   }   public string Number {     get { return number; }     set { number = value; }   }   public string Name {     get { return name; }     set { name = value; }   } }

Next, you create two classes that inherit PhoneNumber: Friend and Supplier. They are shown here:

 // A class of phone numbers for friends. class Friend : PhoneNumber {   bool isWorkNumber;   public Friend(string n, string num, bool wk) :     base(n, num)   {     isWorkNumber = wk;   }   public bool IsWorkNumber {     get {       return isWorkNumber;     }   }   // ... } // A class of phone numbers for suppliers. class Supplier : PhoneNumber {   public Supplier(string n, string num) :     base(n, num) { }   // ... }

Notice that Friend adds a property called IsWorkNumber, which returns true if the telephone number is a work number.

To manage the telephone lists, you create a class called PhoneList. Because you want this class to manage any type of phone list, you make it generic. Furthermore, because part of the list management is looking up numbers given names, and vice versa, you add the constraint that requires that the type of objects stored in the list must be instances of a class derived from PhoneNumber.

 // PhoneList can manage any type of phone list // as long as it is derived from PhoneNumber. class PhoneList<T> where T : PhoneNumber {   T[] phList;   int end;   public PhoneList() {     phList = new T[10];     end = 0;   }   public bool add(T newEntry) {     if(end == 10) return false;     phList[end] = newEntry;     end++;     return true;   }   // Given a name, find and return the phone info.   public T findByName(string name) {     for(int i=0; i<end; i++) {       // Name can be used because it is a member of       // PhoneNumber, which is the base class constraint.       if(phList[i].Name == name)         return phList[i];     }     // Name not in list.     throw new NotFoundException();   }   // Given a number, find and return the phone info.   public T findByNumber(string number) {     for(int i=0; i<end; i++) {       // Number can be used because it is also a member of       // PhoneNumber, which is the base class constraint.       if(phList[i].Number == number)         return phList[i];     }     // Number not in list.     throw new NotFoundException();   }   // ... }

The base class constraint enables code inside PhoneList to access the properties Name and Number for any type of telephone list. It also guarantees that only valid types are used to construct a PhoneList object. Notice that PhoneList throws a NotFoundException if a name or number is not found. This is a custom exception that is declared as shown here:

 class NotFoundException : ApplicationException {}

The following program puts together all the pieces and demonstrates PhoneList. Notice that a class called EmailFriend is also created. This class does not inherit PhoneNumber. Thus, it can’t be used to create a PhoneList.

 // A more practical demonstration of a base class constraint. using System; // A custom exception that is thrown if a name or number // is not found. class NotFoundException : ApplicationException {} // A base class that stores a name and phone number. class PhoneNumber {   string name;   string number;   public PhoneNumber(string n, string num) {     name = n;     number = num;   }   public string Number {     get { return number; }     set { number = value; }   }   public string Name {     get { return name; }     set { name = value; }   } } // A class of phone numbers for friends. class Friend : PhoneNumber {   bool isWorkNumber;   public Friend(string n, string num, bool wk) :     base(n, num)   {     isWorkNumber = wk;   }   public bool IsWorkNumber {     get {       return isWorkNumber;     }   }   // ... } // A class of phone numbers for suppliers. class Supplier : PhoneNumber {   public Supplier(string n, string num) :     base(n, num) { }   // ... } // Notice that this class does not inherit PhoneNumber. class EmailFriend {   // ... } // PhoneList can manage any type of phone list // as long as it is derived from PhoneNumber. class PhoneList<T> where T : PhoneNumber {   T[] phList;   int end;   public PhoneList() {     phList = new T[10];     end = 0;   }   public bool add(T newEntry) {     if(end == 10) return false;     phList[end] = newEntry;     end++;     return true;   }   // Given a name, find and return the phone info.   public T findByName(string name) {     for(int i=0; i<end; i++) {       // Name can be used because it is a member of       // PhoneNumber, which is the base class constraint.       if(phList[i].Name == name)         return phList[i];     }     // Name not in list.     throw new NotFoundException();   }   // Given a number, find and return the phone info.   public T findByNumber(string number) {     for(int i=0; i<end; i++) {       // Number can be used because it is also a member of       // PhoneNumber, which is the base class constraint.       if(phList[i].Number == number)         return phList[i];     }     // Number not in list.     throw new NotFoundException();   }   // ... } // Demonstrate base class constraints. class UseBaseClassConstraint {   public static void Main() {     // The following code is OK because Friend     // inherits PhoneNumber.     PhoneList<Friend> plist = new PhoneList<Friend>();     plist.add(new Friend("Tom", "555-1234", true));     plist.add(new Friend("Gary", "555-6756", true));     plist.add(new Friend("Matt", "555-9254", false));     try {       // Find the number of a friend given a name.       Friend frnd = plist.findByName("Gary");       Console.Write(frnd.Name + ": " + frnd.Number);       if(frnd.IsWorkNumber)         Console.WriteLine(" (work)");       else         Console.WriteLine();     } catch(NotFoundException) {       Console.WriteLine("Not Found");     }     Console.WriteLine();     // The following code is also OK because Supplier     // inherits PhoneNumber.     PhoneList<Supplier> plist2 = new PhoneList<Supplier>();     plist2.add(new Supplier("Global Hardware", "555-8834"));     plist2.add(new Supplier("Computer Warehouse", "555-9256"));     plist2.add(new Supplier("NetworkCity", "555-2564"));     try {       // Find the name of a supplier given a number       Supplier sp = plist2.findByNumber("555-2564");       Console.WriteLine(sp.Name + ": " + sp.Number);     } catch(NotFoundException) {         Console.WriteLine("Not Found");     }     // The following declaration is invalid     // because EmailFriend does NOT inherit PhoneNumber. //    PhoneList<EmailFriend> plist3 = //        new PhoneList<EmailFriend>(); // Error!   } }

The output from the program is shown here:

 Gary: 555-6756 (work) NetworkCity: 555-2564

You might want to try experimenting with this program a bit. For example, try creating different types of telephone lists. Also, try using isWorkNumber from within PhoneList. As you will see, the compiler won’t let you do it. The reason is that isWorkNumber is a property defined by Friend, not by PhoneNumber. Thus, PhoneList has no knowledge of it.

Using an Interface Constraint

The interface constraint enables you to specify an interface that a type argument must implement. The interface constraint serves the same two important purposes as the base class constraint. First, it lets you use the members of the interface within the generic class. Second, it ensures that only type arguments that implement the specified interface are used. This means that for any given interface constraint, the type argument must be either the interface or a class that implements that interface.

The interface constraint uses this form of the where clause:

 where T : interface-name 

Here, T is the name of the type parameter, and interface-name is the name of the interface. More than one interface can be specified by using a comma-separated list. If a constraint includes both a base class and interface, then the base class must be listed first.

The following program illustrates the interface constraint by reworking the telephone list example shown in the previous section. In this version, the PhoneNumber class has been converted into an interface called IPhoneNumber. This interface is then implemented by Friend and Supplier.

 // Use an interface constraint. using System; // A custom exception that is thrown if a name or number // is not found. class NotFoundException : ApplicationException { } // An interface that supports a name and phone number. public interface IPhoneNumber {   string Number {     get;     set;   }   string Name {     get;     set;   } } // A class of phone numbers for friends. // It implements IPhoneNumber. class Friend : IPhoneNumber {   string name;   string number;   bool isWorkNumber;   public Friend(string n, string num, bool wk) {     name = n;     number = num;     isWorkNumber = wk;   }   public bool IsWorkNumber {     get {       return isWorkNumber;     }   }   // Implement IPhoneNumber   public string Number {     get { return number; }     set { number = value; }   }   public string Name {     get { return name; }     set { name = value; }   }   // ... } // A class of phone numbers for suppliers. class Supplier : IPhoneNumber {   string name;   string number;   public Supplier(string n, string num) {     name = n;     number = num;   }   // Implement IPhoneNumber   public string Number {     get { return number; }     set { number = value; }   }   public string Name {     get { return name; }     set { name = value; }   }   // ... } // Notice that this class does not implement IPhoneNumber. class EmailFriend {   // ... } // PhoneList can manage any type of phone list // as long as it implements IPhoneNumber. class PhoneList<T> where T : IPhoneNumber {   T[] phList;   int end;   public PhoneList() {     phList = new T[10];     end = 0;   }   public bool add(T newEntry) {     if(end == 10) return false;     phList[end] = newEntry;     end++;     return true;   }   // Given a name, find and return the phone info.   public T findByName(string name) {     for(int i=0; i<end; i++) {       // Name can be used because it is a member of       // IPhoneNumber, which is the interface constraint.       if(phList[i].Name == name)         return phList[i];     }     // Name not in list.     throw new NotFoundException();   }   // Given a number, find and return the phone info.   public T findByNumber(string number) {     for(int i=0; i<end; i++) {       // Number can be used because it is also a member of       // IPhoneNumber, which is the interface constraint.       if(phList[i].Number == number)         return phList[i];     }     // Number not in list.     throw new NotFoundException();   }   // ... } // Demonstrate interface constraints. class UseInterfaceConstraint {   public static void Main() {     // The following code is OK because Friend     // implements IPhoneNumber.     PhoneList<Friend> plist = new PhoneList<Friend>();     plist.add(new Friend("Tom", "555-1234", true));     plist.add(new Friend("Gary", "555-6756", true));     plist.add(new Friend("Matt", "555-9254", false));     try {       // Find the number of a friend given a name.       Friend frnd = plist.findByName("Gary");       Console.Write(frnd.Name + ": " + frnd.Number);       if(frnd.IsWorkNumber)         Console.WriteLine(" (work)");       else         Console.WriteLine();     } catch(NotFoundException) {       Console.WriteLine("Not Found");     }     Console.WriteLine();     // The following code is also OK because Supplier     // implements IPhoneNumber.     PhoneList<Supplier> plist2 = new PhoneList<Supplier>();     plist2.add(new Supplier("Global Hardware", "555-8834"));     plist2.add(new Supplier("Computer Warehouse", "555-9256"));     plist2.add(new Supplier("NetworkCity", "555-2564"));     try {       // Find the name of a supplier given a number       Supplier sp = plist2.findByNumber("555-2564");       Console.WriteLine(sp.Name + ": " + sp.Number);     } catch(NotFoundException) {         Console.WriteLine("Not Found");     }     // The following declaration is invalid     // because EmailFriend does NOT implement IPhoneNumber. //    PhoneList<EmailFriend> plist3 = //        new PhoneList<EmailFriend>(); // Error!   } }

Using the new( ) Constructor Constraint

The new( ) constructor constraint enables you to instantiate an object of a generic type. Normally, you cannot create an instance of a generic type parameter. However, the new( ) constraint changes this because it requires that a type argument supply a parameterless constructor. (This parameterless constructor can be the default constructor provided automatically when no explicit constructor is declared.) With the new( ) constraint in place, you can invoke the parameterless constructor to create an object.

Here is a simple example that illustrates the use of new( ):

 // Demonstrate a new() constructor constraint. using System; class MyClass {   public MyClass() {     // ...   }   //... } class Test<T> where T : new() {   T obj;   public Test() {     // This works because of the new() constraint.     obj = new T(); // create a T object   }   // ... } class ConsConstraintDemo {   public static void Main() {     Test<MyClass> x = new Test<MyClass>();   } }

First, notice the declaration of the Test class, shown here:

 class Test<T> where T : new() {

Because of the new( ) constraint, any type argument must supply a parameterless constructor. As explained, this can be the default constructor or one that you create.

Next, examine the Test constructor, shown here:

 public Test() {   // This works because of the new() constraint.   obj = new T(); // create a T object }

A new object of type T is created, and a reference to it is assigned to obj. This statement is valid only because the new( ) constraint ensures that a constructor will be available. To prove this, try removing the new( ) constraint, and then attempt to recompile the program. As you will see, an error will be reported.

In Main( ), an object of type Test is instantiated, as shown here:

 Test<MyClass> x = new Test<MyClass>();

Notice that the type argument is MyClass and that MyClass defines a parameterless constructor. Thus, it is valid for use as a type argument for Test. It must be pointed out that it was not necessary for MyClass to explicitly declare a parameterless constructor. Its default constructor would also satisfy the constraint. However, if a class needs other constructors in addition to a parameterless one, then it would be necessary to also explicitly declare a parameterless version.

Two important points about using new( ): First, it can be used with other constraints, but it must be the last constraint in the list. Second, new( ) allows you to construct an object using only the parameterless constructor, even when other constructors are available. In other words, it is not permissible to pass arguments to the constructor of a type parameter.

The Reference Type and Value Type Constraints

The last two constraints enable you to indicate that a type argument must be either a reference type or a value type. These constraints are useful in the few cases where the difference between reference and value types is important to generic code. The general forms of the where statements for these constraints are shown here:

 where T : class where T : struct

Here, T is the name of the type parameter. When additional constraints are present, class or struct must be the first constraint in the list.

Here is an example that demonstrates the reference type constraint:

 // Demonstrate a reference constraint. using System; class MyClass {   //... } // Use a reference constraint. class Test<T> where T : class {   T obj;   public Test() {     // The following statement is legal only     // because T is guaranteed to be a reference     // type, which can be assigned the value null.     obj = null;   }   // ... } class ClassConstraintDemo {   public static void Main() {     // The following is OK because MyClass is a class.     Test<MyClass> x = new Test<MyClass>();     // The next line is in error because int is     // a value type. //    Test<int> y = new Test<int>();   } }

First, notice how Test is declared:

 class Test<T> where T : class {

The class constraint requires that any type argument for T be a reference type. In this program, this is necessary because of what occurs inside the Test constructor:

 public Test() {   // The following statement is legal only   // because T is guaranteed to be a reference   // type, which can be assigned the value null.   obj = null; }

Here, obj (which is of type T) is assigned the value null. This assignment is valid only for reference types. In C#, you cannot assign null to a value type. Therefore, without the constraint, the assignment would not have been valid, and the compile would have failed. This is one case in which the difference between value types and reference types might be important to a generic routine.

The value type constraint is the complement of the reference type constraint. It simply ensures that any type argument is a value type, including a struct or an enum. Here is an example:

 // Demonstrate a value type constraint. using System; struct MyStruct {   //... } class MyClass {   // ... } class Test<T> where T : struct {   T obj;   public Test(T x) {     obj = x;   }   // ... } class ValueConstraintDemo {   public static void Main() {     // Both of these declarations are legal.     Test<MyStruct> x = new Test<MyStruct>(new MyStruct());     Test<int> y = new Test<int>(10);     // But, the following declaration is illegal! //    Test<MyClass> z = new Test<MyClass>(new MyClass());   } }

In this program, Test is declared as shown here:

 class Test<T> where T : struct {

Because T of Test now has the struct constraint, T can be passed only value type arguments. This means that Test<MyStruct> and Test<int> are valid, but Test<MyClass> is not. To prove this, try removing the comment symbol from the start of the last line in the program and recompiling. An error will be reported.

Using a Constraint to Establish a Relationship Between Two Type Parameters

One of the more interesting aspects of the base class constraint is that it allows you to establish a relationship between two type parameters. For example, consider the following generic class declaration:

 class Gen<T, V> where V : T {

In this declaration, the where clause tells the compiler that V must inherit T. If this relationship is not present when an object of type Gen is declared, then a compile-time error will result. A constraint that uses a type parameter, such as that just shown, is called a naked type constraint. The following example illustrates this constraint:

 // Create relationship between two type parameters. using System; class A {   //... } class B : A {   // ... } // Here, V must inherit T. class Gen<T, V> where V : T {   // ... } class NakedConstraintDemo {   public static void Main() {     // This declaration is OK because B inherits A.     Gen<A, B> x = new Gen<A, B>();     // This declaration is in error because     // A does not inherit B. //    Gen<B, A> y = new Gen<B, A>();   } }

First, notice that class B inherits class A. Next, examine the two Gen declarations in Main( ). As the comments explain, the first declaration:

 Gen<A, B> x = new Gen<A, B>();

is legal because B inherits A. However, the second declaration:

 //    Gen<B, A> y = new Gen<B, A>();

is illegal because A does not inherit B.

Using Multiple Constraints

There can be more than one constraint associated with a parameter. When this is the case, use a comma-separated list of constraints. In this list, the first constraint must be class or struct (if present), or the base class (if one is specified). It is illegal to specify both a class or struct constraint and a base class constraint. Next, must come any interface constraints. The new( ) constraint must be last. For example, this is a valid declaration:

 class Gen<T> where T : MyClass, IMyInterface, new() { // ...

In this case, T must be replaced by a type argument that inherits MyClass, implements IMyInterface, and has a parameterless constructor.

When using two or more type parameters, you can specify a constraint for each parameter by using a separate where clause. For example:

 // Use multiple where clauses. using System; // Gen has two type arguments and both have // a where clause. class Gen<T, V> where T : class                 where V : struct {   T ob1;   V ob2;   public Gen(T t, V v) {     ob1 = t;     ob2 = v;   } } class MultipleConstraintDemo {   public static void Main() {     // This is OK because string is a class and     // int is a value type.     Gen<string, int> obj = new Gen<string, int>("test", 11);     // The next line is wrong because bool is not     // a reference type. //    Gen<bool, int> obj = new Gen<bool, int>(true, 11);   } }

In this example, Gen takes two type arguments, and both have a where clause. Pay special attention to its declaration:

 class Gen<T, V> where T : class                 where V : struct {

Notice that the only thing that separates the first where clause from the second is whitespace. No other punctuation is required or valid.




C# 2.0(c) The Complete Reference
C# 2.0: The Complete Reference (Complete Reference Series)
ISBN: 0072262095
EAN: 2147483647
Year: 2006
Pages: 300

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