Earlier, you saw that it is a relatively simple matter to add a method to a type when the type is generic; such a method can use the generic type parameters declared by the type. You did this, for example, in the generic class examples we have seen so far.
Generic methods use generic type parameters, much as generic types do. They can be declared in generic or nongeneric types. If declared in a generic type, their type parameters are distinct from those of their containing generic type. To declare a generic method, you specify the generic type parameters the same way you do for generic types: Add the type parameter declaration syntax immediately following the method name, as shown in the MathEx.Max<T> and MathEx.Min<T> examples in Listing 12.35.
In this example, the method is static, although this is not required.
Generic methods, like generic types, can include more than one type parameter. The arity (the number of type parameters) is an additional distinguishing characteristic of a method signature. That is, it is legal to have two methods with identical names and formal parameter types, as long as they differ in method type parameter arity.
Just as type arguments are provided after the type name when using a generic type, so the method type arguments are provided after the method type name. The code used to call the Min<T> and Max<T> methods looks like that shown in Listing 12.36 with Output 12.4.
Not surprisingly, the type arguments, int and string, correspond to the actual types used in the generic method calls. However, specifying the type arguments is redundant because the compiler can infer the type parameters from the formal parameters passed to the method. Clearly, the caller of Max in Listing 12.36 intends the type argument to be int because both of the method arguments are of type int. To avoid redundancy, you can exclude the type parameters from the call in all cases when the compiler is able to logically infer which type arguments you must have intended. An example of this practice, which is known as method type inference, appears in Listing 12.37 with Output 12.5.
For method type inference to succeed, the types of the arguments must be “matched” with the formal parameters of the generic method in such a way that the desired type arguments can be inferred. An interesting question to consider is what happens when contradictory inferences are made. For example, when you call the Max<T> method using MathEx.Max(7.0, 490), the compiler could deduce from the first argument that the type argument should be double, and it could deduce from the second argument that the type argument is int—a contradiction. A more sophisticated analysis would notice that the contradiction can be resolved because every int can be converted to a double, so double is the best choice for the type argument.12
In cases where method type inference is still not sophisticated enough to deduce the type arguments, you can resolve the error either by inserting casts on the arguments that clarify to the compiler the argument types that should be used in the inferences or by giving up on type inferencing and including the type arguments explicitly.
Notice that the method type inference algorithm, when making its inferences, considers only the arguments, the arguments’ types, and the formal parameter types of the generic method. Other factors that could, in practice, be used in the analysis—such as the return type of the generic method, the type of the variable to which the method’s returned value is being assigned, or the constraints on the method’s generic type parameters—are not considered at all by this algorithm.
Type parameters of generic methods may be constrained in the same way that type parameters of generic types are constrained. For example, you can restrict a method’s type parameter to implement an interface or to be convertible to a class type. The constraints are specified between the parameter list and the method body, as shown in Listing 12.38.
Here, the Show<T> implementation itself does not directly use any member of the IComparable<T> interface, so you might wonder why the constraint is required. Recall, however, that the BinaryTree<T> class did require this constraint (see Listing 12.39).
Because the BinaryTree<T> class requires this constraint on its T, and because Show<T> uses its T as a type argument corresponding to a constrained type parameter, Show<T> needs to ensure that the constraint on the class’s type parameter is met on its method type argument.
Sometimes you should be wary of using generics—for instance, when using them specifically to bury a cast operation. Consider the following method, which converts a stream into an object of a given type:
public static T Deserialize<T>(
Stream stream, IFormatter formatter)
{
return (T)formatter.Deserialize(stream);
}
The formatter is responsible for removing data from the stream and converting it to an object. The Deserialize() call on the formatter returns data of type object. A call to use the generic version of Deserialize() looks something like this:
string greeting =
Deserialization.Deserialize<string>(stream, formatter);
The problem with this code is that to the caller of the method, Deserialize<T>() appears to be type-safe. However, a cast operation is still performed on behalf of the caller, as in the case of the nongeneric equivalent:
string greeting =
(string)Deserialization.Deserialize(stream, formatter);
The cast could fail at runtime; the method might not be as type-safe as it appears. The Deserialize<T> method is generic solely so that it can hide the existence of the cast from the caller, which seems dangerously deceptive. It might be better for the method to be nongeneric and return object, making the caller aware that it is not type-safe. Developers should use care when casting in generic methods if there are no constraints to verify cast validity.
________________________________________