Understanding Generics—Constraining Type Parameters Using where
Microsoft .NET Framework, ASP.NET, Visual C# (CSharp, C Sharp, C-Sharp) Developer Training, Visual Studio
Constraining Type Parameters Using where
Currently, the CarCollection<T> class does not buy you much beyond uniquely named public methods.
Furthermore, an object user could create an instance of CarCollection<T> and specify a completely
unrelated type parameter:
// This is syntactically correct, //but confusing at best... CarCollection<int> myInts = new CarCollection<int>(); myInts.AddCar(5); myInts.AddCar(11);
To illustrate another form of generic abuse, assume that you have now created two new classes
(SportsCar and MiniVan) that derive from the Car type:
public class SportsCar : Car { public SportsCar(string p, int s) : base(p, s){} // Assume additional SportsCar methods. } public class MiniVan : Car { public MiniVan(string p, int s) : base(p, s){} // Assume additional MiniVan methods. }
Given the laws of inheritance, it is permissible to add a MiniVan or SportsCar type directly into
a CarCollection<T> created with a type parameter of Car:
// CarCollection<Car> can hold any type deriving from Car. CarCollection<Car> myCars = new CarCollection<Car>(); myInts.AddCar(new MiniVan("Family Truckster", 55)); myInts.AddCar(new SportsCar("Crusher", 40));
Although this is syntactically correct, what if you wished to update CarCollection<T> with
a new public method named PrintPetName()? This seems simple enough—just access the correct
item in the List<T> and invoke the PetName property:
// Error! System.Object does not have a // property named PetName. public void PrintPetName(int pos) { Console.WriteLine(arCars[pos].PetName); }
However, this will not compile, given that the true identity of T is not yet known, and you cannot
say for certain if the item in the List<T> type has a PetName property. When a type parameter is
not constrained in any way (as is the case here), the generic type is said to be unbound. By design,
unbound type parameters are assumed to have only the members of System.Object (which clearly
does not provide a PetName property).
You may try to trick the compiler by casting the item returned from the List<T>’s indexer
method into a strongly typed Car, and invoking PetName from the returned object:
// Error! // Cannot convert type 'T' to 'Car' public void PrintPetName(int pos) { Console.WriteLine(((Car)arCars[pos]).PetName); }
This again does not compile, given that the compiler does not yet know the value of the type
parameter <T> and cannot guarantee the cast would be legal.
To address such issues, .NET generics may be defined with optional constraints using the where
keyword. As of .NET 2.0, generics may be constrained in the ways listed in Table 10-2.
Table 10-2. Possible Constraints for Generic Type Parameters
| Generic Constraint | Meaning in Life |
where T : struct
| The type parameter <T> must have System.ValueType in its chain
of inheritance. |
where T : class
| The type parameter <T> must not have System.ValueType in its
chain of inheritance (e.g., |
where T : new()
| The type parameter <T> must have a default constructor. This is
very helpful if your generic type must create an instance of the type parameter, as you cannot assume the format of custom constructors. Note that this constraint must be listed last on a multiconstrained type. |
where T : NameOfBaseClass
| The type parameter <T> must be derived from the class specified
by |
where T : NameOfInterface
| The type parameter <T> must implement the interface specified
by |
When constraints are applied using the where keyword, the constraint list is placed after the
generic type’s base class and interface list. By way of a few concrete examples, ponder the following
constraints of a generic class named MyGenericClass:
// Contained items must have a default ctor. public class MyGenericClass<T> where T : new() {...} // Contained items must be a class implementing IDrawable // and support a default ctor. public class MyGenericClass<T> where T : class, IDrawable, new() {...} // MyGenericClass derives from MyBase and implements ISomeInterface, // while the contained items must be structures. public class MyGenericClass<T> : MyBase, ISomeInterface where T : struct {...}
On a related note, if you are building a generic type that specifies multiple type parameters, you can specify a unique set of constraints for each:
// <K> must have a default ctor, while <T> must // implement the generic IComparable interface. public class MyGenericClass<K, T> where K : new() where T : IComparable<T> {...}
If you wish to update CarCollection<T> to ensure that only Car-derived types can be placed
within it, you could write the following:
public class CarCollection<T> : IEnumerable<T> where T : Car { ... public void PrintPetName(int pos) { // Because all subitems must be in the Car family, // we can now directly call the PetName property. Console.WriteLine(arCars[pos].PetName); } }
Notice that once you constrain CarCollection<T> such that it can contain only Car-derived types,
the implementation of PrintPetName() is straightforward, given that the compiler now assumes <T>
is a Car-derived type. Furthermore, if the specified type parameter is not Car-compatible, you are
issued a compiler error:
// Compiler error! CarCollection<int> myInts = new CarCollection<int>();
Do be aware that generic methods can also leverage the where keyword. For example, if you wish
to ensure that only System.ValueType-derived types are passed into the Swap() method created previously
in this chapter, update the code accordingly:
// This method will swap any Value types. static void Swap<T>(ref T a, ref T b) where T : struct { ... }
Understand that if you were to constrain the Swap() method in this manner, you would no longer be able to swap string types (as they are reference types).
|

