Understanding 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):

[VB code]
Public Function LookupRetiredEmployees(ByVal minAge As Integer) As IList
    Dim retVal As New ArrayList
    For Each emp As Employee In masterEmployeeCollection
        If ((emp.Age >= minAge) And (emp.Status = EmpStatus.Retired)) Then
            retVal.Add(emp)
        End If
    Next
    Return retVal
End Function

[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:

[VB code]
Public Class CustomerStack
    Private _items() as Customer
    Private _count as Integer

    Public Sub Push(item as Customer)
        ...
    End Sub

    Public Function Pop() as Customer
        ...
    End Function
End Class

[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:

[VB code]
Public Class Stack(Of T)
    Private _items() as T
    Private _count as Integer

    Public Sub Push(item as T) 
        ...
    End Sub

    Public Function Pop() as T 
        ...
    End Function
End Class

[C# code]
public class Stack<T> {
    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.

Understanding Generic 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:

[VB code]
Public Class Stack(Of T, U, V)

[C# code]
public class Stack<T, U, V>

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:

[VB code]
Public Class MyType
   ...
End Class

Public Class MyType(Of T)
    ...
End Class

Public Class MyType(Of T, U)
    ...
End Class

[C# Code]
public class MyType {
}

public class MyType<T> {
    ...
}

public class MyType<T, U> {
    ...
}

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:

[VB code]
Imports System.Collections.Generic

Class MySampleClass(Of T)
    Private Shared _values As List(Of T)

    Shared Sub New()
        If (GetType(T).IsAbstract = False) Then
            Throw New Exception("T must not be abstract")
        Else
            _values = New List(Of T)()
        End If
    End Sub
End Class

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

public class <b>MySampleClass</b><T> {
    private static <b>List</b><T> _values;

    static <b>MySampleClass</b>() {
        if (typeof(T).IsAbstract == false)
            throw new <b>Exception</b>("T must not be abstract");
        else
            _values = new <b>List</b><T>();
    }
}

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.

Understanding Generic Classes

Inheritance

Generic concepts, of course, are not constrained to a single class declaration. The type parameters that are supplied with a generic class declaration can also be applied to all the objects that participate in an object hierarchy. Here's a simple example that creates a generic base class and subclasses it with another generic class:

[VB code]
Public Class MyBaseClass(Of U)
    Private _parentData As U

    Public Sub New()
        ... 
    End Sub

    Public Sub New(ByVal val As U)
        Me._parentData = val
    End Sub
End Class

Public Class MySubClass(Of T, U)
    Inherits MyBaseClass(Of U)
    Private _myData As T

    Public Sub New()
        ... 
    End Sub

    Public Sub New(ByVal val1 As T, ByVal val2 As U)
        MyBase.New(val2)
        Me._myData = val1
    End Sub
End Class

[C# code]
public class MyBaseClass<U> {
    private U _parentData;

    public MyBaseClass() {}

    public MyBaseClass(U val) {
        this._parentData = val;
    }
}

public class MySubClass<T, U> : MyBaseClass<U> {
    private T _myData;

    public MySubClass() {}

    public MySubClass(T val1, U val2) : base(val2) {
        this._myData = val1;
    }
}

Notice here that you have MyBaseClass, which accepts a single type parameter. Then, you subclass MyBaseClass with MySubClass, which accepts two type parameters. The key bit of syntax to notice here is that one of the type parameters used in the declaration of MySubClass was also referenced in the declaration of the parent class, MyBaseClass.

Although this example simply passed the type parameters from the subclass to the base class, you can also use type arguments when inheriting from another class or implementing a generic interface. In this case, your generic class declarations could appear as follows:

[VB code]
Public Class MyBaseClass(Of U)
    ...
End Class

Public Class MySubClass(Of T)
    Inherits MyBaseClass(Of Integer)
    ...
End Class

[C# code]
public class MyBaseClass<U> {
}

public class MySubClass<T> : MyBaseClass<int> {
}

The subclass has been altered here and now only accepts a single type parameter. And, in place of the type parameter U that was being passed to MyBaseClass, you now pass the type argument Integer. After looking at these two examples, it should be clear that your classes can subclass another class using both open and constructed types (and some variations in between). It's probably obvious at this stage, but I should point out that your generic classes can also subclass non-generic, closed base classes as well.

Now that you know what works, let's take a quick look at some of the combinations of inheritance patterns that will not work. Suppose you have a closed type that inherits from a generic class:

[VB code]
Public Class MyBaseClass(Of T, U)
End Class

Public Class MySubClass1
    Inherits MyBaseClass(Of T, U)
End Class

Public Class MySubClass2
    Inherits MyBaseClass(Of Int32, String)
End Class

[C# code]
public class MyBaseClass<T, U> { }
   
public class MySubClass1 : MyBaseClass<T, U> { }

public class MySubClass2 : MyBaseClass<int, string> { }

In this example, you have two closed types that inherit from a generic class. MySubClass1 attempts to inherit using an open type and MySubClass2 inherits as a constructed type. When you attempt to compile this code, you're going to notice that MySubClass1 will generate an error. The compiler has no point of reference that allows it to resolve the types of T and U. As a constructed type, MySubClass1 doesn't accept any type parameters and, therefore, has no parameters that can be used because it inherits from MyBaseClass<T, U>. MySubClass2 doesn't accept type parameters either, but it still compiles because it uses type arguments and forms a constructed type as part of its inheritance declaration.

There's one more inheritance scenario that's worth discussing here. Let's look at an example where you use a constructed type as a type argument in the declaration of your inherited class:

[VB code]
Imports System.Collections.Generic

Public Class MyBaseClass(Of T, U)
End Class

Public Class MySubClass1(Of T)
    Inherits MyBaseClass(Of List(Of T), T)
End Class

Public Class MySubClass2(Of T)
    Inherits MyBaseClass(Of List(Of T), Stack(Of T))
End Class

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

public class MyBaseClass<T, U> { }

public class MySubClass1<T> : MyBaseClass<List<T>, T> { }

public class MySubClass2<T> : MyBaseClass<List<T>, Stack<T>> { }

If you look at this closely, it's really just a variation on one of the earlier examples of inheritance. The key difference here is that a mixture of constructed types and type parameters are used where the constructed types end up leveraging the type parameter from the subclass. The goal here is just to get you comfortable with the possibilities and to get you to view a constructed type like any other type argument you might pass when inheriting from a generic class.

Finally, it's worth noting that a generic class cannot use one of its type parameters as its inherited type. It must descend from an existing closed or open type. For example, the following would not be legal:

[VB code]
Public Class MyType(Of T)
   Inherits Of T
   ...
End Class

[C# code]
public class MyType<T> : T {
    ...
}

At this stage, after looking at all these examples, you should have a much better idea for how the mechanics of generic inheritance work. The rules that govern inheritance are fairly logical and don't really fall outside what you might expect. The main idea to take away here is that you can use both type parameters and type arguments as parameters when subclassing a generic class.

Understanding Generic Classes

Protected Members

Any discussion of inheritance would be incomplete without also examining the accessibility of protected members. First, I should make it clear that all the rules that govern access to protected members are unchanged for closed types. Things get a bit more interesting when you look at protected members that appear within a generic class. These members are accessible too. In fact, they are accessible in some ways you might not expect.

Here's an example that declares a base class with a protected, generic field that will then be referenced in a descendant class:

[VB code]
Public Class MyBaseClass(Of T)
    Protected _var1 As T

    Public Sub New(ByVal var1 As T)
        Me.var1 = var1
    End Sub
End Class

Public Class MySubClass(Of T, U)
    Inherits MyBaseClass(Of T)
    Private _var2 As U

    Public Sub New(ByVal var1 As T, ByVal var2 As U)
        MyBase.New(var1)
        Me._var2 = var2
    End Sub

    Private Sub Foo1()
        Dim localVar as T = Me._var1
    End Sub

    Private Sub Foo2()
        Dim sub1 As New MySubClass(Of Integer, String)(1, "12")
        Dim val1 As Integer = sub1._var1
        Dim sub2 As New MySubClass(Of Double, Double)(1.0, 5.8)
        Dim val2 As Double = sub2._var1
        End Sub
End Class

[C# code]
using System;

public class MyBaseClass<T> {
    protected T _var1;

    public MyBaseClass(T var1) {
        this.var1 = _var1;
    }
}

public class MySubClass<T, U> : MyBaseClass<T> {
    private U _var2;

    public MySubClass(T var1, U var2) : base(var1) {
        this._var2 = var2;
    }

    private void Foo1() {
        localVar T = this._var1;
    }

    private void Foo2() {
        MySubClass<int, String> sub1
                        = new MySubClass<int, String>(1, "12");
        int val1 = sub1._var1;
        MySubClass<Double, Double> sub2 
                        = new MySubClass<Double, Double>(1.0, 5.8);
        Double val2 = sub2._var1;
    }
}

This example actually ends up illustrating two key points. It includes a protected field in the base class, var1, that is then accessed by the subclass. The method Foo1() accesses var1 successfully, much like it would any other inherited, protected data member. The method Foo2() is the more interesting example. It constructs two separate instances of MySubClass and, because we're in the scope of the descendant class, is able to access the protected member var1 via these constructed types.

This little foray into generic classes and some of their characteristics should give you a better idea for how generics can be applied to bring a new dimension to your data types. While generic classes certainly share much in common with their non-generic counterpart, you'll also discover a number of subtle differences that you'll have to keep in mind as start creating your own generic types.

This article is adapted from Professional .NET 2.0 Generics by (Wrox, 2005, ISBN: 0-7645-5988-5), from chapter 4 "Generic Classes."
Copyright 2006 by WROX. All rights reserved. Reproduced here by permission of the publisher.



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.

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Protecting business operations means shifting the priorities around availability from disaster recovery to business continuity. Enterprises are shifting their focus from recovery from a disaster to preventing the disaster in the first place. With this change in mindset, disaster recovery is no longer the first line of defense; the organizations with a smarter business continuity practice are less impacted when disasters strike. This SmartSelect will provide insight to help guide your enterprise toward better …

  • Live Event Date: October 29, 2014 @ 11:00 a.m. ET / 8:00 a.m. PT Are you interested in building a cognitive application using the power of IBM Watson? Need a platform that provides speed and ease for rapidly deploying this application? Join Chris Madison, Watson Solution Architect, as he walks through the process of building a Watson powered application on IBM Bluemix. Chris will talk about the new Watson Services just released on IBM bluemix, but more importantly he will do a step by step cognitive …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds