CodeClimber: Generic Methods Part II

This is the second in a multi-part series of blogs on generics in C#. In the previous blog, you learned by example the basics of using existing generic classes, and also encountered a few simple facts about how to  pass those classes to generic methods. This blog will continue to explore these themes.

 

I’m going to close this blog by showing you another simple example.  I’ll start out, however, in a more discursive mode, so that I can spend time explaining how generics work. I’ll first define a few basic terms that are frequently used when discussing generic methods. In this blog, and the next blog in this series, I'll discuss those terms in some depth, and specify exactly how they are used in generic methods. By the time you are done, the basic ideas behind generics should be quite clear.

Some Terminology

 

In this section, I’m going to define two terms:

 

  • type parameter

  • type argument.

     

Look at the following very simple method declaration:

 

public T GetData<T>()

 

We can tell this is a generic method because it includes a type parameter. The type parameter here is the code which appears in brackets: <T> .  For the purposes of this article, you can think of a type parameter in a generic method as a mechanism for defining the type of certain parameters, variables or return types.

 

Think for a moment about standard local variable declarations such as the following:

 

int a;

int b = 5;

 

There are two parts to the first declaration shown here: the type name, and the variable name. A type name is also frequently called a type specifier, or type identifier. A type parameter affects the type name, not the variable name or its value. Thus we write the following:

 

1: public void GetData<T>(T value)

   2: {

   3: T myValue = value;

   4: }

 

Here the T on line 3 takes the place of a type name. It appears in the place that a type name, such as int, or string, might appear.

 

In the context of the GetData method shown here, it would make no sense, and would not be legal, to write either of the following:

 

int T = value;

int myValue = T;

 

The examples illustrate that a type parameter is used to define a type, not a variable name, or a constant. A type parameter specifies the particular type to be used for a series of substitutions performed within a method. These substitutions are quite straight forward, but they are bound by the rules of syntax that govern the use of types. In other words, we can only substitute the type of T for the places where a type would safely be used in your code. This is not like C macro substitution, because you can only substitute for types.

 

The syntax for declaring a type parameter is actually very much like the syntax for declaring normal parameters that you have always used when passing arguments to a method.

Here is a normal method that takes a normal parameter:

 

public int GetData(int a)

 

In this normal method, we use parenthesis to designate our parameter. If I want to be very precise, I can call this kind of parameter a formal-parameter.

 

Now let’s look again at a generic method header:

 

public T GetData<T>(int a)

 

This method has a normal parameter designated with parenthesis. This is the formal-parameter. But it also has a parameter which uses angle brackets instead of parenthesis: <T>. This odd looking parameter is called a type parameter. Its role is to designate a placeholder for a type to be specified by the user. By adding this placeholder, we are saying that this method has a type identifier in it with an unspecified type. When the user calls the method, he will specify a type argument, and that type will be substituted for each instance of the placeholder. For example, the declaration public T GetData<T>(T a) will be transformed by substitution into something like this: int GetData<T>(int a) . The placeholder T has been replaced with the specific type called int.

 

We resolve the type of the type parameter by including a type argument when we call the method. For instance, we might write something like this:

 

value = GetData<int>();

Here the type argument, which is declared in brackets as an int, defines how the type parameter will be defined in the GetData method. We know that the type parameter <T>, will, in this case, be an int, because we specified that type in our type argument.

Consider this method invocation:

value = GetData(5);

Here we have a normal argument with a value of five. We might call this a formal-argument. Formal or not, we can still think of it as nothing more than a plain old argument of the kind seen in countless programs.

Now look at this code:

value = GetData<int>(5);

The formal argument is still there, but now we also have a type argument. The brackets might look a bit strange, but the concept is simple enough.

The Letter T

When creating a formal-parameter, any valid identifier will do for the parameter name. The same is true of type parameters. It is merely a convention to use T as the identifier in a type parameter. The example used in this section looks like this:

   1: public void GetData<T>(T value)

   2: {

   3: T myValue = value;

   4: }

We could, however, have just as easily written the following code:

1:  public void GetData<MyTypeParameter>(MyTypeParameter value)

   2: {

   3: MyTypeParameter myValue = value;

   4: }

Both examples have the same meaning, and both are legal. The convention, however, is to use the lettter T, as shown in the first example. To help keep your code as easy to read as possible, it is strongly recommended that you follow the standard convention, and stick with the letter T whenever possible.

A Generic Example

 

So far, this blog has featured a good deal of talking, and not much code. Let’s change the tone and finish this post with a simple example. Consider the code shown here:

 

1: using System;

   2: using System.Collections.Generic;

   3: using System.Text;

   4:  

   5: namespace ConsoleApplication3

   6: {

   7: // Declare two classes

   8: class Class1 { }

   9: class Class2 { }

  10:  

  11: class Program

  12: {

  13: // One function to use both types of arrays

  14: void UseArray<T>(T[] classArray)

  15: {

  16: foreach (T classInstance in classArray)

  17: {

  18: Console.WriteLine(classInstance.GetType().Name);

  19: }

  20: }

  21:  

  22: // Code to call UseArray

  23: static void Main(string[] args)

  24: {

  25: Program program = new Program();

  26:

  27: Class1[] classOneArray = new Class1[2] { new Class1(), new Class1() };

  28: program.UseArray<Class1>(classOneArray);

  29:

  30: Class2[] classTwoArray = new Class2[] { new Class2(), new Class2() };

  31: program.UseArray<Class2>(classTwoArray);

  32:  

  33: Console.ReadLine();

  34: }

  35: }

  36: }

In this example code, you can see two very simple classes called Class1 and Class2. There are declared on lines 8 and 9.

On lines 14 though 20, you can see a very simple generic method that takes an array as a formal parameter. The type of this array is left unspecified, and is designated only by a type parameter called T.

On line 28, a type argument is used to specify that T should be of type Class1. When called in this manner, the code in lines 14 to 20 is transformed as follows:

  14a: void UseArray(Class1[] classArray)

  15a: {

  16a: foreach (Class1 classInstance in classArray)

  17a: {

  18a: Console.WriteLine(classInstance.GetType().Name);

  19a: }

  20a: }

The type argument specified on line 28 was substituted for each of the instances of T found in the UseArray method. The end result is the code shown above in lines 14a through 20a. A method declared in this manner, however, could not be called with an array of type Class2[]:

  program.UseArray(classTwoArray);

The problem is that classTwoArray is an array of objects of type Class2, and this second version of the UseArray method takes only arrays of Class1. This code would therefore generate a compile time exception.

The beauty of the code shown in lines 14 through 20 is that it can be called with either an array of Class1 or an array of Class2. In either case, you would still be working with an array of a specific type, and will be bound to stringent rules for type checking. It is not that type checking is being loosened in order to allow you to work with two different types. All the rules of type checking are still in effect. The code shown in lines 14 through 20 is just as type specific as the code shown in lines 14a through 20a. The difference is that the generic method can switch types depending on the value in a type argument, while the code shown in 14a through 20a is restricted to a single type.

Summary

In this blog, you encountered two terms:

  • type parameter: The syntax used in a method header to designate the placeholder for the type a particular method will use in a series of substituions: void GetData<T>(); The substitutions are very straight forward, but they are governed by the rules for using type names.
  • type argument: The type passed in the invocation of a generic method: GetData<int>(). 

To review what has been covered in this section, consider a generic method from two points of view:

 

  1. Syntactic: We know GetData<T> is a generic method because the method takes a type parameter. In other words, if a method has bit of syntax in it that looks like this: <T>, then it is a generic method.
  2. Semantic: We know GetData is a generic method because it takes a parameter used as a placeholder for a type to be designated by the user. As a result, the generic method can be used not just with a single type, but with several different types, such as an Integer, a String or an ArrayList.

Now that both the syntactic and semantic side of type arguments is familiar, it is time to move on and look at this whole matter from a different perspective. In the next blog in this series, I will focus on understanding type parameter substitutions. In particular, you will see how those substitutions work, and the rules that govern their behavior. As you will discover, the behaviour found in C# generics is very different from the behavior you see in C/C++ macros.