Using Generic Methods

So much attention gets paid to the new generic classes that have been introduced with .NET generics, I often find developers overlooking what can be achieved through the use of generic methods. In fact, in many cases, you may discover one of the first opportunities to leverage generics is by introducing some generic methods into your non-generic types. This brings the flavor and power of generics to a class without requiring the class itself to be parameterized.

The Basics

To illustrate the fundamental value of generic methods, let's start with the simplest of examples. Suppose you have a Max() function that accepts two double values, compares them, and returns the greater of the two values. This function might appear as follows:

This method is handy for number-crunching applications. However, once you decide you want to apply this same Max() function to additional data types, you have a problem. This method can only be applied to double data types. You only have a few real, type-safe options that you can use to resolve this problem. One approach would be to create specific versions of this method to support each data type. However, doing that would force you to bloat your namespace with MaxString, MaxInt, and MaxLong methods. Not good. To get around the bloat issue, you might consider going back to using an object-based interface and tossing all type safety to the wind. Your last option here would be to provide several overloaded versions of Max() that accepted different types. That might represent some measure of improvement, but it's still not ideal.

This discussion of taking on bloat or compromising type safety is probably starting to sound like a broken record at this point. You see the same patterns over and over again in your code. You start out with a nice, general-purpose class or method only to find that, as you attempt to broaden its applicability, you discover that the tools offer you few good options to extrapolate that generality to additional data types. That's right in the sweet spot of generics.

So, let's look at how generics can be applied to the Max() method. The following code represents the generic version of the Max() method:

The syntax and concepts here are right in line with what you've already seen with generic classes. The Max() method becomes a parameterized type, accepting one or more type parameters as part of its signature. Once you've outfitted your method with a type parameter, you can then proceed to reference that type parameter throughout the scope of the function. Method parameters, return types, and types appearing in the body of your methods may all reference the type parameters that are supplied to your generic method.

For this example to work properly, I was required to apply a constraint to my type parameter, indicating that each T must implement IComparable. Constraints are addressed in detail in the book Professional .NET 2.0 GenericsChapter 7, "Generic Constraints."

All that remains at this stage is to start making some calls to this new, generic Max() method. Let's take a quick look at how clients would invoke the Max() method with a few different type arguments:

Calling a generic method, as you can see, is not all that different than calling a non-generic method. The only new wrinkle here is the introduction of a type parameter immediately following the name of the method.

Using Generic Methods

A Deeper Look

With the conceptual introduction behind us, let's now consider some more detailed examples of generic methods. The following example defines a class, SimpleClass, which declares a series of generic methods that illustrate some of the variations that are available to you when creating your own generic methods. To make things more interesting, all of these generic methods in this example are placed within a generic class. The code for the class is as follows:

This generic class accepts two type parameters, T and U, and implements four different generic methods. Let's start by looking at the Foo1() method, which accepts a single type parameter I. What's slightly different here is that this method also accesses the T type parameter that belongs to the surrounding class. The goal here is simply to illustrate the accessibility of the surrounding class's type parameters. Every generic method declared in this class may, within any part of its implementation, reference the type parameters that are associated with that class. So, this particular example includes a line of code that writes out the type of the class's T type parameter.

From the perspective of a generic method, type parameters should be treated like any other type that would traditionally be declared within the scope of your class. The rules that govern your method's ability to reference type parameters conform to the same rules that govern the use of non-generic type parameters. The Foo2() method demonstrates one more variation on this theme, referencing the type parameter U as its return type. The idea here is that you shouldn't limit your view of type parameters to just those supplied in the declaration of your method. Leveraging the type parameters of your class and your method in tandem creates a broader view of what can be achieved with a generic method.

The Foo3() method exposes another issue you need to consider when using generic methods within a generic class. The type parameter it accepts shares the same name, T, as a type parameter used by the surrounding class. At a minimum, this creates some confusion. It's not clear what type T will ultimately be assigned. If you run this method in the debugger, however, you'll quickly discover that the T type parameter for Foo3() will always be the type supplied in the call to the method. This, in effect, ends up preventing this method from making any use of the T type parameter that is part of SimpleClass. Clearly, this practice limits the versatility of your generic methods and should be discouraged. Even if your class doesn't need to leverage the type parameters from the class, it would still create some degree of confusion to have these type parameters share the same name. Fortunately, the compiler will generate a warning in this scenario. So, if you happen to do this unintentionally, you'll be notified.

Generic methods may also be static, as demonstrated by the Foo4() method of this example. This method actually illustrates a few separate points. First, it's static and, as such, can be invoked without requiring clients to create an instance of SimpleClass. Even though it's static, it can still reference the type parameters from the class. At first glance, that may seem wrong. However, remember that the type parameters do not reference instance data, they simply define types. This means they can be littered freely throughout your static methods. For this method, U is used as the type of the third parameter and T as part of the return type. Finally, for one last bit of variety, this method also illustrates the use of multiple type parameters.

Calling this static method follows the same conventions as you've seen with calling static methods on generic classes. The only exception is the addition of the new type arguments that accompany the method. Let's look at a small snippet of code that calls the static Foo4() method to see what this might look like:

You'll notice that this combination of a static generic method within a generic class, both of which accept two type parameters, gets a little unwieldy. Still, it can't be accused of lacking expressiveness or type safety.

Applying Constraints

Anywhere you're allowed to create a generic type, you are also allowed to qualify that type with constraints. For a generic method, the application of constraints to the incoming type parameters is relatively simple. Chapter 7 "Generic Constraints" in the book Professional .NET 2.0 Genericslooks at constraints in detail, but here's a quick peek at the mechanics of how constraints are applied to the type parameters of your generic methods:

[VB code]
Public Sub Foo(Of I As IComparable)(ByVal val1 As I)
End Sub

[C# code]
public void Foo<I>(I val1) where I : IComparable {}

As you can see, the syntax here leverages the same approach that you may have seen applied to generic classes. You simply add the interface qualifier for a given type parameter and that will allow you to treat any reference to that the type parameter as conforming to the supplied interface.

Using Generic Methods

Overriding Generic Methods

Generic methods may also be overridden by descendant classes. In fact, generic methods introduce surprisingly few new facets when it comes when overriding methods. The syntax for overriding generic methods follows the same pattern as non-generic methods where the overriding method must match, precisely, the parameters of the parent method. With a generic method, the only difference is that the parameters can be expressed as type parameters. Here's a simple example:

A series of examples are shown in this section, each of which attempts to override a generic method. The goal here is to provide a sampling of permutations so you can have a better feel for what's possible. This example sets things up by declaring a generic class, Person, and creating a descendant generic Employee class that overrides a handful of its parent's virtual, generic methods.

Most of the overrides, at this stage, are just as you would expect. The overriding method simply matches the signature of its parent. You should pay particular attention the role type parameters play in this example. In some instances, the type parameters of the surrounding class are referenced and, in others, the generic methods reference their own type parameters. The Foo2() method, for example, accepts type parameters of I and J and references the U type parameter that is part of the class declaration.

The other method here that offers a slight twist is Foo4(). This method matches the parent's signature but uses entirely different type parameter names. This is only meant to demonstrate that — even in an overriding scenario — the names of the type parameters are still just placeholders. The fact that these names are different in the base class does not prevent you from successfully overriding it with alternate type parameter names.

This first example (and those that follow) demonstrates a few areas where VB and C# diverge in their approach to overriding generic methods. In this first set of examples, C# compiles both of these classes successfully. However, VB throws an error on the Foo1() here. It preemptively determines that there are instances where the type for the T parameter can make overloaded versions of Foo1() that collide.

The next example takes this a little further and adds another class that changes the inheritance scheme. The following generic Customer class also extends the Person class and overrides two of its generic methods:

[VB code]
Public Class Customer(Of T, U)
Inherits Person(Of T, U)
'Error: can't verify this is unique for all permutations
Public Overrides Sub Foo1(Of I)(ByVal val1 As T)
End Sub
Public Overrides Function Foo5(Of I As IComparable)
(ByVal val1 As I) As I
End Function
End Class

In contrast with the previous example, this class uses the T and U type parameters in its inheritance declaration. By referencing the same type parameter for T in both the base and descendant class, you are able to override the Foo1() method that references the T parameter in the base class. This is only possible because the T in both classes is guaranteed to reference the same type. Of course, Foo1() fails in the VB example again for the same reasons discovered in the previous example.

The other override here, the Foo5() method, demonstrates how constraints factor into the signature of a generic method that's being overridden. Here, you might think that Foo5() would not successfully override the declaration in its parent, because the Person class included a constraint as part of its declaration. For C#, the inclusion of the matching constraint would actually generate a compile-time error here. When constraints are part of the base class in C#, the overriding method always inherits the constraints and cannot alter them. The opposite is true in VB, where the overriding method is required to include the constraint as part of the method's signature. The rationale behind this inconsistency is not clear.

There's one final scenario worth exploring. You'll notice that the Person class actually includes an overloaded method, Foo1(). This method has one version that accepts a T type parameter and the other accepts an integer. Now, consider this example where the T type argument supplied to the parent is an integer:

This class would seem to be valid. Its declaration of the Foo1() method certainly matches that of the parent class. The problem here isn't that the method doesn't match — it's that two methods from the Person class both match this signature. This issue is caused by the use of an integer in its inheritance from the Person class. That integer causes that the Foo1(T val1) method to collide with the other Foo1() declaration.

As noted earlier, this is one area where VB and C# vary in their handling of the Foo1() method. VB identifies this error at the point of declaration, whereas C# won't throw the error until a type argument is supplied that creates a situation where the signatures of the overloaded methods collide.

Using Generic Methods

Type Inference

One really handy aspect of generic methods is their ability to examine the types of incoming arguments and infer which method should be invoked. This eliminates the need to explicitly specify type arguments when calling a generic method. Consider the following method declaration and corresponding calls:

This example declares a generic method that accepts two type parameters and provides three examples of calls to that method. The first call provides explicit type arguments. In this case, the types of the supplied parameter must match the types specified in the type parameter list. The next two examples both successfully call the InferParams() method without supplying any type arguments. They both use type inference, where the type is inferred from the types of the supplied arguments.

Leveraging this mechanism makes sense in most situations. However, in instances where you've overloaded a method, you may encounter some degree of ambiguity. Suppose you were to add the following overloaded method to the preceding example:

This method overloads the previous InferParams() method, adding a version that includes specific integer and string types in its parameter list. Now, when you execute the sample calls to this method, it's not clear, from looking at the code, which method will get called. As it turns out, the example that supplied explicit type parameters would get you into this new method, and the version that infers the parameter types will get you into the other version of this method. Still, if you're overloading like this, it's probably not wise to rely on inference for your type parameters.

As you can see, generic methods are quite handy. They bring a whole new set of possibilities to how you approach your methods — especially those methods that may currently struggle with type-safety issues. And, thanks to type inference, consumers are often able to leverage generic methods without any direct awareness of any generic syntax. Authors of generic methods, on the other hand, do have some new factors to take into consideration when composing generic methods. Still, the handful of new rules that accompany generic methods are largely intuitive and most developers should be able to make the transition to generic methods with relative ease.

About the Author

Tod Golding

Tod Golding has 20 years of experience as a software developer, lead architect, and development manager for organizations engaged in the delivery of large-scale commercial and internal solutions. He has an extensive background leveraging .NET, J2EE, and Windows DNA technologies, and is skilled with C#, Java, and C++. Tod is the author of Professional .NET 2.0 Generics published by WROX.