In the generic stack example, the stack did not do anything with the items it contained other than store them and pop them. It didn’t try to add them, compare them, or do anything else that would require using operations of the items themselves. There’s good reason for that. Since the generic stack doesn’t know the type of the items it will be storing, it can’t know what members these types implement.
All C# objects, however, are ultimately derived from class object, so the one thing the stack can be sure of about the items it’s storing is that they implement the members of class object. These include methods ToString, Equals, and GetType. Other than that, it can’t know what members are available.
As long as your code doesn’t access the objects of the types it handles (or as long as it sticks to the members of type object), your generic class can handle any type. Type parameters that meet this constraint are called unbounded type parameters. If, however, your code tries to use any other members, the compiler will produce an error message.
The following code declares a class called Simple with a method called LessThan that takes two variables of the same generic type. LessThan attempts to return the result of using the less-than operator. But not all classes implement the less-than operator, so you can’t just substitute any class for T. The compiler, therefore, produces an error message.
class Simple<T> { static public bool LessThan(T i1, T i2) { return i1 < i2; // Error } ... }
Constraints
To make generics more useful, you need to be able to supply additional information to the compiler about what kinds of types are acceptable as arguments. These additional bits of information are called constraints. Only types that meet the constraints can be substituted for the given type parameter to produce constructed types.
Where clauses
Constraints are listed where clauses.
- where T : IComparable // where type is IComparable (constraint to interface)
- where T : Product // to a class (Product) class or any of its children
- where T : struct // a value type
- where T : class // a reference type
- where T : new() // an object that has a default constructor
We can have more than one constraint as shown in the code below. Suppose we are writing a generic class called Utilities. We want to be able to have a method that returns the maximum of two numbers. The type T must implement IComparable. Also we have a method in that same class that requires us to instantiate the type. Suppose we have a method called DoSomething(T value). Our class can have two constraints by separating them with a comma, as shown here.
public class Utilities<T> where T : IComparable, new()