Code Climber Generic Methods Part III

Code Climber Generic Methods Part III

This post continues the examination of generic methods and type parameters found in the previous two Code Climber posts. The first post focused on the basics of using generics, and on how to pass generic classes to a generic method. The second post dug into some of the details and terminology involved with generics, and particularly generic methods. This post will help wrap up the discussion of generic methods, and lay the groundwork for a post on creating generic classes.

Understanding Type Parameter Substitutions

In the previous article, I used the word substitution when talking about type parameters. In this section of the text I’m going to explain why I used that word, and why there are limits on the kinds of substitutions that take place in a generic method.

The C# rules governing the use of types make these kinds of substitutions fundamentally different from the kinds of free form substitutions found in C/C++ macros. Much of this post will be dedicated to helping you see exactly how these substitutions are governed by a set of strict rules that closely define how type parameters can be used.

Consider the following map of a the parts of a typical generic method:

public return-type MethodName<type-parameter>(formal-parameter-list)

{

     body

}

 

The parts shown here in green italics are the parts affected by the type parameter.

This map shows that the type parameter holds sway in three places:

  • the formal-parameter list
  • the body
  • the return type

In this map I omit a bit of generic syntax called a constraint clause. There is nothing overly complex about constraint clauses, but I want to stick with the basics in this post, so I will leave that subject for another time.

These are the only parts of a method that can use type parameters. This is not a particularly strong startement, however, since most of the code found in a method falls into one of these categories. Nevertheless, it helps understand exactly where you can use a type parameter, and where you cannot.

A Simple Example of Substitution

 

Consider this code fragment, which I will call Sample One:

 

   1: public void CallGetData()

   2: {

   3: int value = GetData<int>(5);

   4: }

   5:  

   6: public T GetData<T>(T value)

   7: {

   8: return default(T);

   9: }

 

Because the type argument in the call to GetData on line 3 is specified as <int>, the implementation of GetData on line 6 is transformed by a series of substitutions that involve Integers. Once complete, the transformation of the GetData method shown on line 6 looks something like this, which I will call Sample Two:

 

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

   2: {

   3: return default(int);

   4: }

 

If we ignore the type parameter itself, you can see that all the instances of T on lines 6 and 8 of Sample One are transformed into int in Sample Two. Thus the following code:

public T GetData<T>(T value)

becomes 

public int GetData<T>(int value)

And this code:

return default(T);

becomes

return default(int);

These substitutions never literally appear in your source code, but this shows what the compiler is doing behind the scenes when you use generics.

 

This is not a simple substitution mechanism like you find in C/C++ macros. In macros, you are free to substitute, or transform, nearly any part of your code. In generics, however, substitution is only for type names, and the substitutions will only work if the rules for using types are correctly followed. You will hear more about this subject in the next section of this post.

 

If I wanted to state the substitution process in algorithmic form, I might write something like this:

 

  • Substitute the type found in the type argument for each instance of the type parameter in your formal-parameter list, in your code body, or in your return type.

 

As you will see in future articles, the way that these substitutions take place can be a bit tricky in certain circumstances. This is particularly true when you have to infer the type from a literal. Nevertheless, the basic substitution mechanism is quite simple. In fact, the whole subject of generics is really quite simple. It’s just that angle brackets are a bit odd looking, and there are some annoying corner cases that can cause a bit of confusion.

The default Keyword

 

I’m going to include one more bit of theory in this blog, in order to drive home the point I made in the last section about substitutions in generics being bound by specific rules.

 

Why does the code shown in Sample One from the previous section include the keyword default? In particular, why don’t I just write: return T?

 

The simple answer is that it won’t compile. It won’t compile because we can’t return a type from a function. For instance, we can’t write this:

return int;

Code like that doesn’t compile, nor does it make any sense. We don’t, under most circumstance, want to return a type. Given the reflection tools built into the C# language, it is possible to do such a thing, but it is not a common goal, nor would the syntax shown here help us achieve it.

 

So why use the keyword default? To answer that question, I need to first note that we don’t know whether the GetData method from Sample One is returning a reference type or a value type. I pass in a type parameter called T, but there are no constraints put on the value of T. The code must return a value that will be valid whether T is a reference or a value type.

 

The C# online documentation states that when used with generics, default “[s]pecifies the default value of the type parameter. This will be null for reference types and 0 for value types.” When working with structs, its members will be set to 0 or null, depending on whether they are value or reference types.

 

In general, this means that default(T) returns 0 if you pass in a value type, and it returns null if you pass in a reference type. The default keyword thus solves our problem through a little sleight of hand. In particular, it returns a valid value regardless of whether T is a value or reference type.

 

As an aside, I’ll show this alternative bit of code:

 

   1: public System.Type GetData<T>()

   2: {

   3: return typeof(T);

   4: }

 

Like the code in Sample One, this code cleanly compiles. It uses the type parameter T in a legal manner that works for all types. We can pass any type name to either typeof or default and expect the code to return some valid value. Thus it would have served my purposes in terms of being short and easy to understand, but it did not use T as the return type, so I passed it by.

An Extended Generics Example with Overloaded Methods

 

To close out this discussion of generic methods, I will show a somewhat longer example, found below in Listing 1. Here a method called DisplayData is overloaded so that it appears three times.  Each overload is a generic method, and each takes a single parameter.  The code calls DisplayData five times. You might want to check to see if you can tell which instance of DisplayData is called in each case.

 

Listing 1: A long example with some simple code that tells us a little about how calls to overloaded generic methods might look.

 

   1: using System;

   2: using System.Collections;

   3: using System.Collections.Generic;

   4: using System.Text;

   5:  

   6: namespace GenericCodeSpace

   7: {

   8: struct Point

   9: {

  10: int x;

  11: int y;

  12: }

  13:  

  14: public class GenericMethod

  15: {

  16: public void RunTest()

  17: {

  18: // Create sample data

  19: int[] intData = {0, 1, 2};

  20: String[] stringData = {"This", "is", "a", "series", "of", "Words"};

  21:  

  22: List<int> intList = new List<int>(intData);

  23: List<String> stringList = new List<string>(stringData);

  24: Stack<String> stringStack = new Stack<string>(stringData);

  25:

  26: // Show the data

  27: DisplayData<int>(intData);

  28: DisplayData<String>(stringData);

  29: DisplayData<int>(intList);

  30: DisplayData<String>(stringList);

  31: DisplayData<String>(stringStack);

  32:  

  33: // Further experiements

  34: int value = GetData<int>();

  35: ArrayList list = GetData<ArrayList>();

  36: Point p = GetData<Point>();

  37: int value2 = GetData<int>(5);

  38: }

  39:

  40: public void DisplayData<T>(T[] values)

  41: {

  42: foreach (T value in values)

  43: {

  44: Console.Write(value + " ");

  45: }

  46: }

  47:  

  48: public void DisplayData<T>(List<T> values)

  49: {

  50: foreach (T value in values)

  51: {

  52: Console.Write(value + " ");

  53: }

  54: }

  55:  

  56: public void DisplayData<T>(Stack<T> values)

  57: {

  58: foreach (T value in values)

  59: {

  60: Console.Write(value + " ");

  61: }

  62: }

  63:  

  64: public T GetData<T>()

  65: {

  66: return default(T);

  67: }

  68:  

  69: public int GetData<T>(int value)

  70: {

  71: return default(int);

  72: }

  73: }

  74: }

 

In lines 19 and 20 I declare two simple arrays. One is of type int, the other of type string. On lines 27 and 28 I call DisplayData with these arrays as arguments. These calls both resolve to the instance of DisplayData found on line 40.

 

On lines 22 and 23, I declare two generic Lists, one of type string and one of type int. I initialize these generic Lists with the arrays declared on lines 19 and 20. These Lists are used in the calls to DisplayData on lines 29 and 30, and both calls resolve to the instance of DisplayData found on line 48. Notice the header for this method:

 

  48: public void DisplayData<T>(List<T> values)

 

When called with a type argument of type <int>, this header is transformed by substitution to code that looks something like this:

 

  48a: public void DisplayData<T>(List<int> values)

 

We still are substituting for a type, just as I explained earlier in this post. In this instance, however, it is not the type List that gets transformed. That type stays the same. It is the type parameter taken by List that is transformed. This is fairly intuitive, and more or less as one would expect. Nevertheless, it pays to spend a moment to make sure you fully understand what is happening here, as it is a simple example of the type of substitution that can get a bit confusing in more complex cases.

 

The rest of the code in Listing 1 is fairly straightforward. For instance, a generic type called a Stack appears in line 24. It is used in the call to DisplayData found on line 31. As you might expect, this call resolves to the instance of DisplayData found on line 56:

 

  56: public void DisplayData<T>(Stack<T> values)

 

Listing 1 ends with a few calls to GetData which are similar to those discussed in the previous section of this post. I’ve included them here mostly so that you can step through them in the debugger, and watch how the values are transformed by the call to default. In particular, notice the call on line 36, which passes in as an argument a struct called Point. If you step through that code in the debugger you can see what happens to a struct when passed to default. As I explained earlier, both of its fields are set to zero.

 

When setting up this example in Visual Studio, you can create either a console or a Windows Forms application. In either case, add the code in Listing 1 as a separate file. Give the file a useful name, such as GenericMethod.cs. If you’ve created Windows Forms application, you might Call the code in this manner:

 

   1: private void button1_Click(object sender, EventArgs e)

   2: {

   3: GenericMethod genericMethod = new GenericMethod();

   4: genericMethod.RunTest();

   5: }

 

Don’t forget that you might need to add using GenericMethod at the top of the file which calls into the code in Listing 1. When making the call, first declare an instance of the GenericMethod class, as shown above in line 3. Then call its RunTest method, as shown on line 4. To step through the code in Visual Studio with the default key bindings, set a breakpoint on line 19 of Listing 1 by clicking in the gutter to the left of the source . Run the program. When you hit the breakpoint, press F11 repeatedly to step through the example.

Summary

 

In this post, you had a chance to learn a little terminology. In particular, you heard a bit about formal-parameters, type parameters and type arguments. The text then dove into the particulars of how type parameters work. You saw that in many cases, generic programming can be reduced to a few simple substitutions. It is important to note, however, that these substitutions must conform to the C# rules for using a type. There can be no free form substitutions such as those found in C/C++ macros.

 

In the middle passages of this post, you encountered the default keyword. When used with generics, this keyword can help solve a conflict between value and reference types. The example was a bit painful to explore, but it is useful in part because it highlights the restrictions that govern the use of type parameters in generic code.

 

I have now had a good chance to explore generic methods. The examination has not been exhaustive, but the basics of the subject were introduced and examined in some depth. The next posts will move on to a discussion of how to creat your own generic classes.