Tuesday, July 31, 2007

Generic Classes

Many developers will view themselves primarily as consumers of generics. However, as you get more comfortable with generics, you're likely to find yourself introducing your own generic classes and frameworks. Before you can make that leap, though, you'll need to get comfortable with all the syntactic mutations that come along with creating your own generic classes. Fortunately, you'll notice that the syntax rules for defining generic classes follow many of the same patterns you've already grown accustomed to with non-generic types. So, although there are certainly plenty of new generic concepts you'll need to absorb, you're likely to find it quite easy to make the transition to writing your own generic types.

Parameterizing Types

In a very general sense, a generic class is really just a class that accepts parameters. As such, a generic class really ends up representing more of an abstract blueprint for a type that will, ultimately, be used in the construction of one or more specific types at run-time. This is one area where, I believe, the C++ term templates actually provides developers with a better conceptual model. This term conjures up a clearer metaphor for how the type parameters of a generic class serve as placeholders that get replaced by actual data types when a generic class is constructed. Of course, as you might expect, this same term also brings with it some conceptual inaccuracies that don't precisely match generics.

The idea of parameterizing your classes shouldn't seem all that foreign. In reality, the mindset behind parameterizing a class is not all that different than the rationale you would use for parameterizing a method in one of your existing classes. The goals in both scenarios are conceptually very similar. For example, suppose you had the following method in one of your classes that was used to locate all retired employees that had an age that was greater than or equal to the passed-in parameter (minAge):

[C# code]
public IList LookupRetiredEmployees(int minAge) {
IList retVal = new ArrayList();
foreach (Employee emp in masterEmployeeCollection) {
if ((emp.Age >= minAge) && (emp.Status == EmpStatus.Retired))
retVal.Add(emp);
}
return retVal;
}
}
Now, at some point, you happen to identify a handful of additional methods that are providing similar functionality. Each of these methods only varies based on the status (Retired, Active, and so on) of the employees being processed. This represents an obvious opportunity to refactor through parameterization. By adding status as a parameter to this method, you can make it much more versatile and eliminate the need for all the separate implementations. This is something you've likely done. It's a simple, common flavor of refactoring that happens every day.

So, with this example in mind, you can imagine applying this same mentality to your classes. Classes, like methods, can now be viewed as being further generalized through the use of type parameters. To better grasp this concept, let's go ahead and build a non-generic class that will be your candidate for further generalization:

[C# code]
public class CustomerStack {
private Customer[] _items;
private int _count;

public void Push(Customer item) {...}
public Customer Pop() {...}
}
This is the classic implementation of a type-safe stack that has been created to contain collections of Customers. There's nothing spectacular about it. But, as should be apparent by now, this class is the perfect candidate to be refactored with generics. To make your stack generic, you simply need to add a type parameter (T in this example) to your type and replace all of your references to the Customer with the name of your generic type parameter. The result would appear as follows:

[C# code]
public class Stack {
private T[] _items;
private int _count;

public void Push(T item) {...}
public T Pop() {...}
}
Pretty simple. It's really not all that different than adding a parameter to a method. It's as if generics have just allowed you to widen the scope of what can be parameterized to include classes.

Type Parameters
By now, you should be comfortable with the idea of type parameters and how they serve as a type placeholder for the type arguments that will be supplied when your generic class is constructed. Now let's look at what, precisely, can appear in a type parameter list for a generic class.

First, let's start with the names that can be assigned to type parameters. The rules for naming a type parameter are similar to the rules used when defining any identifier. That said, there are guidelines that you should follow in the naming of your type parameters to improve the readability and maintainability of your generic class. These guidelines, and others, are discussed in Chapter 10, "Generics Guidelines" of Professional .NET 2.0 Generics.

A generic class may also accept multiple type parameters. These parameters are provided as a delimited list of identifiers:

[C# code]
public class Stack
As you might suspect, each type parameter name must be unique within the parameter list as well as within the scope of the class. You cannot, for example, have a type parameter T along with a field that is also named T. You are also prevented from having a type parameter and the class that accepts that parameter share the same name. Fortunately, the names you're likely to use for type parameters and classes will rarely cause collisions.

In terms of scope, a type parameter can only be referenced within the scope of the generic class that declared it. So, if you have a child generic class B that descends from generic class A, class B will not be able to reference any type parameters that were declared as part of class A.

The list of type parameters may also contain constraints that are used to further qualify what type arguments can be supplied of a given type parameter. Chapter 7, "Generic Constraints" (Professional .NET 2.0 Generics) looks into the relevance and application of constraints in more detail.

Overloaded Types
The .NET implementation of generics allows programmers to create overloaded types. This means that types, like methods, can be overloaded based on their type parameter signature. Consider the declarations of the following types:
[C# Code]
public class MyType {
}

public class MyType {
...
}

public class MyType {
...
}
Three types are declared here and they all have the same name and different type parameter lists. At first glance, this may seem invalid. However, if you look at it from an overloading perspective, you can see how the compiler would treat each of these three types as being unique. This can introduce some level of confusion for clients, and this is certainly something you'll want to factor in as you consider building your own generic types. That said, this is still a very powerful concept that, when leveraged correctly, can enrich the power of your generic types.

Static Constructors
All classes support the idea of a static (shared) constructor. As you might expect, a static constructor is a constructor that can be called without requiring clients to create an instance of a given class. These constructors provide a convenient mechanism for initializing classes that leverage static types.

Now, when it comes to generics, you have to also consider the accessibility of your class's type parameters within the scope of your static constructor. As it turns out, static constructors are granted full access to any type parameters that are associated with your generic classes. Here's an example of a static constructor in action:

[C# code]
using System.Collections.Generic;

public class MySampleClass {
private static List _values;

static MySampleClass() {
if (typeof(T).IsAbstract == false)
throw new Exception("T must not be abstract");
else
_values = new List();
}
}
This example creates a class that accepts a single type parameter, T. The class has a data member that is used to hold a static collection of items of type T. However, you want to be sure, as part of initialization, that T is never abstract. In order to enforce this constraint, this example includes a static constructor that examines the type information about T and throws an exception if the type of T is abstract. If it's not abstract, the constructor proceeds with the initialization of its static collection.

This is just one application of static constructors and generic types. You should be able to see, from this example, how static constructors can be used as a common mechanism for initializing any generic class that has static data members.

No comments: