Some things are good in their own time. Bell bottoms were deemed to be good in the 1970s, but today, after a short revival, they are considered goofy. In contrast, blue jeans have always been deemed to be good, whether you choose Wranglers or Levi’s. The same thing is true of code idioms: some idioms are always good, and some are good in their own time.
For example, templates are deemed good, but for VB.NET programmers their time hadn’t come until now. Called generics in .NET, Microsoft is introducing generics into languages like C# and VB.NET. (Microsoft could have introduced generics into VB earlier, but the audience probably just wasn’t ready.)
Unlike bell-bottoms, generics have always been useful, because they help eliminate the need to write the same algorithm over and over when the only thing that changes is data type — think Stack algorithm — and they help eliminate ugly conditional type checking and coercion. Now, C# and VB.NET support generics.
Historically, generics and templates have been perceived as very hard to use, but they aren’t, not really. In this article, I demonstrate how to define generic methods and tell you about their implementation in .NET 2.0.
The Problems Generics Solve
.NET has a common type system. This means all objects have, at their absolute root, a class named object
, even simple types like int/Integer. In a technical sense, this means that you could write an abstract data type, Stack (although one already exists), and store objects in it. Since Stack already exists, Listing 1 offers a simple example that illustrates a problem with stacks based on a common subtype.
Listing 1: Problems with common sub-types and data structures.
Imports System.Collections Module Module1 Sub Main() Dim s As Stack = New Stack s.Push(New Gun) s.Push(New Camera) While (s.Count > 0) Dim o As Object = s.Pop o.Aim() o.Shoot() End While Console.ReadLine() End Sub End Module Public Class Gun Public Sub Aim() End Sub Public Sub Shoot() Console.WriteLine("You're dead") End Sub End Class Public Class Camera Public Sub Aim() End Sub Public Sub Shoot() Console.WriteLine("You're a supermodel") End Sub End Class
From the example in Listing 1, you see that we can put any type of object in the basic Stack class. The result is that, without a lot of type checking and type coercion, we can’t be sure that some unexpected type hasn’t found its way in the stack, resulting in an unanticipated runtime error, or worse, supermodels with holes in them.
One solution is to write a type-specific Stack for each kind of object we want to store using a stack. Generics eliminate the need to write type-specific stacks (or anything else), and eliminate the need to for type checking and type coercion. Well, slightly more precisely, Generics eliminate the need to us to write type-specific algorithms, which in turn also eliminates the need to write type-checking and type-coercing code.
Using a Generic Stack
The problem is easily mitigated by using generics. In our supermodel-homicide example, we can use the System.Collections.Generic.Stack
class, which would prevent us from shooting the supermodel with the Gun, because Gun wouldn’t be permitted in a stack of cameras (see Listing 2).
Listing 2: A revision of listing 1 that uses a Generic stack ensuring type homogeneity. Imports System.Collections.Generic
.
Module Module1 Sub Main() 'Dim s As Stack = New Stack Dim s As Stack(Of Camera) = New Stack(Of Camera) 's.Push(New Gun) s.Push(New Camera) While (s.Count > 0) Dim o As Object = s.Pop o.Aim() o.Shoot() End While Console.ReadLine() End Sub End Module Public Class Gun Public Sub Aim() End Sub Public Sub Shoot() Console.WriteLine("You're dead") End Sub End Class Public Class Camera Public Sub Aim() End Sub Public Sub Shoot() Console.WriteLine("You're a supermodel") End Sub End Class
In the revised example, trying to put Gun
in the Stack(Of Camera)
would report a compile-time error, which effectively means we wouldn’t be shooting supermodels with guns, even by accident.
Generic methods can be written independently, or as part of a generic class. They are the first step toward creating type agnostic solutions. In the rest of this article, we focus on writing generic methods.
Writing a generic class is easy once you know how to write generic methods. Simply wrap the generic method in a class, and add the notation to the class header for the permits specifying the generic type when the class is declared and instantiated.
Defining Generic Methods
A generic method is a method where one or more parameter types are unspecified in the method header. It is up to the method caller to provide the data type at each point of invocation. In C# and in VB.NET, this means we use a non-data type for at least one parameter type; T
is used by convention. It also means that we use an extended notation specific to the language. In C#, we add a <T> after the method name before the parentheses, and in VB.NET we use an (Of T)
after the method name and before the parameter parentheses. (Listing 3 provides a VB.NET example; Listing 4 is a C# example.)
It is worth noting that we can have multiple generic types. Return types and local parameters can be defined as the generic type, too.
Listing 3: A generic method body for Pop in VB.NET.
Public Function Pop(Of T)() As T End Function Listing 4: A generic method body for Pop in C#. public T Pop<T>() { }
The just-in-time compiler for .NET is responsible for converting a method call against these generic methods into a type-specific method instance based on the type provided at invocation. For example, to use the stand-alone C# Pop
method to Pop a Camera instance, we would call Pop this way:
Pop<Camera>
Adding Generic Method Constraints
Generic methods in .NET support optional method constraints that limit the kinds of data types with which a particular method can be invoked. In C#, this is accomplished by using a
WHERE T : is_kind
predicate at the end of the method header and an
(Of T As is_kind)
after the Of T predicate, when defining a generic constraint for VB.NET. For example, we could define a generic method named Swap
,and limit possible types to non-nullable structures like int/Integer but not string/String by using a generic constraint. Listing 5 shows Swap
in VB.NET, and Listin6 shows Swap
in C#. Both have the constraint that limits the generic method to struct/Structure.
Listing 5: Demonstrates a generic method with a structure constraint in VB.NET.
Module Module1 Sub Main() Dim I As Integer = 5 Dim J As Integer = 7 Swap(Of Integer)(I, J) Console.WriteLine("I = " & I) Console.WriteLine("J = " & J) Console.ReadLine() End Sub Public Sub Swap(Of T As Structure)(ByRef a As T, ByRef b As T) Dim temp As T = a a = b b = temp End Sub End Module
After Swap(Of Integer)(I, J)
is called, I
is 7 and J
is 5.
Listing 6: Swap implemented as a C# method with the struct constraint. This code produces results identical to the code in Listing 5.
using System; using System.Collections.Generic; using System.Text; namespace SwapInCSharp { class Program { static void Main(string[] args) { int i = 5; int j = 7; Swap(ref i, ref j); Console.WriteLine("i = " + i.ToString()); Console.WriteLine("j = " + j.ToString()); Console.ReadLine(); } public static void Swap (ref T a, ref T b) where T : struct { T temp = a; a = b; b = temp; } } }
The complete list and definition of possible constraints is provided in the .NET help documentation. By definition, generic types can be limited to structures, classes, classes with default constructors, base classes, and specific interfaces.
Tip: An example of a nullable struct/Structure is the string/String class. The int/Integer is not nullable can be coerced to be nullable by adding a question mark (
?
) after the data type. As defined in Listings 5 and 6, we couldn’t use this version of Swap for string/String types.
Writing Generic Class Headers
To create generic classes, all you need to do is have a class with at least one generic member and a means of letting the class consumer specify the actual type of the generic member. This is done in the class header, much the same way it is accomplished in a generic method.
To define a generic class in VB.NET, add the (Of T)
predicate after the class name, and to define a generic class in C# add a <T>
after the class name. When you create an instance of the class, simply specify the data type of the generic parameter T. That’s all there is to it.
Summary
There are a lot of small subtleties in how generic methods are defined and used, but the basic use is simple: convert the data type of parameters and local variables to the named generic type reference, and you are all set. Figuring out when to use generic methods takes some practice; their implementation in .NET is comparatively easy.
The good news is that you do not need generics every day. You should not let their presence intimidate you, because you don’t have to use generics at all. Generics do provide useful benefits without presenting any practical limitation to getting started with Visual Studio 2005 today.
Paul Kimmel is the founder of Software Conceptions, Inc, founded in 1990, and the co-founder of the Greater Lansing Area .NET Users Group. Look for his upcoming book, UML DeMystified.