Some Specifics on Generics

Published on: Author: Rob Windsor Leave a comment

Generics are the most significant language addition to .NET 2.0. They allow for code re-use in ways not previously available and make it much easier to write type-safe, better performing code. That is, they help you turn runtime exceptions into compile-time errors while making your application run faster. Sound good?

What’s the Problem?
The classic example used to demonstrate the problem Generics solve is the general purpose collection. In .NET 1.1 collection classes like the ArrayList and HashTable are object based so they can store data of any type. Unfortunately this benefit has a cost, the lack of type safety. The user of the ArrayList cannot restrict the types it stores, which can potentially lead to runtime errors.

Dim al As New ArrayList()
al.Add(11)
al.Add(42)
al.Add(18.0)  ' will cause a runtime error in the for loop
Dim sum As Integer = 0
For i As Integer = 0 To al.Count
    sum += CInt(al(i))
Next


ArrayList al = new ArrayList();
al.Add(11);
al.Add(42);
al.Add(18.0);  // will cause a runtime error in the for loop
int sum = 0;
for (int i = 0; i < al.Count; i++) { sum += (int)al[i]; }

The solution to this problem in .NET 1.1 is to create a strongly-type collection class by inheriting from CollectionBase (or DictionaryBase is you want HashTable like functionality). While this task is not difficult it is tedious and leads to code bloat. You have to write almost exactly the same code for each and every type which requires a strongly-typed collection.



Public Class IntCollection
    Inherits CollectionBase
    Default Public Property Item(ByVal index As Integer) As Integer
        Get
            Return (CType(List(index), Integer))
        End Get
        Set(ByVal Value As Integer)
            List(index) = Value
        End Set
    End Property
    Public Function Add(ByVal value As Integer) As Integer
        Return (List.Add(value))
    End Function
    Public Function IndexOf(ByVal value As Integer) As Integer
        Return (List.IndexOf(value))
    End Function
    Public Sub Insert(ByVal index As Integer, ByVal value As Integer)
        List.Insert(index, value)
    End Sub
    Public Sub Remove(ByVal value As Integer)
        List.Remove(value)
    End Sub
End Class


public class IntCollection : CollectionBase
{
    public int this[int index]
    {
        get { return ((int)List[index]); }
        set { List[index] = value; }
    }
    public int Add(int value)
    {
        return (List.Add(value));
    }
    public int IndexOf(int value)
    {
        return (List.IndexOf(value));
    }
    public void Insert(int index, int value)
    {
        List.Insert(index, value);
    }
    public void Remove(int value)
    {
        List.Remove(value);
    }
}


A second problem is performance. Value types need to be boxed when being added to the collection and the unboxed when they are retrieved. This is a “double whammy” because not only do you pay the penalty for boxing but you also add more and more work to the garbage collection mechanism as the collection grows. Even if you’re storing reference types there is still some penalty for casting.

Using Generics
Generics are designed to address the issues stated above without sacrificing developer productivity. The idea is to allow classes to be parameterized by the types they store and manipulate. The type parameter or parameters, which are enclosed in angle brackets and separated by commas, can be added to a class or method declaration. The type parameter acts as a placeholder for the actual type that will be used at runtime. For example you could create a generic Stack class:


Public Class Stack(Of T)
    Private _items As New List(Of T)
    Public Sub Push(ByVal item As T)
        _items.Add(item)
    End Sub
    Public Function Pop() As T
        If _items.Count = 0 Then
            Throw New InvalidOperationException("Stack is empty")
        End If
        Dim index As Integer = _items.Count - 1
        Dim item As T = _items(index)
        _items.RemoveAt(index)
        Return item
    End Function
End Class



public class Stack<T> {
    private List<T> _items = new List<T>();
    public void Push(T item) { _items.Add(item); }
    public T Pop() {
        if (_items.Count == 0) {
            throw new InvalidOperationException(“Stack is empty”);
        }
        int index = _items.Count – 1;
        T item = _items[index];
        _items.RemoveAt(index);
        return item;
    }
}

In this case the type parameter is T (it is common practice to use a single capital letter as the name for a type parameter but you are free to use more descriptive names if you choose) so the type would be know as “Stack of T”. T is used as the type for the regular parameter to Push and the return type of Pop. It is interesting to note that it is also used as the type argument in the construction of the internal List of items. The type argument defines the specific type you wish to use for that instance of the generic class.

The .NET Framework 2.0 adds several new generic collection classes in the System.Collections.Generic namespace, the most commonly used being List<T> (the generic version of ArrayList) and Dictionary<K, T> (the generic version of HashTable).


Dim al As New List(Of Integer)
al.Add(11)
al.Add(42)
al.Add(18.0)  ' will cause a complie time error
Dim sum As Integer = 0
For i As Integer = 0 To al.Count
    sum += al(i)  ' no cast or unboxing required
Next



List<int> al = new List<int>();
al.Add(11);
al.Add(42);
al.Add(18.0);  //will cause a complie time error
int sum = 0;
for (int i = 0; i < al.Count; i++) { 
    sum += al[i]; // no cast or unboxing required
}



Generic Methods
You have seen adding type parameters to classes, you can do the same thing for methods. This can be done even if the class itself is not generic. To do this you use the same syntax style as with classes, adding the type parameter after the name of the method but before the regular parameter list.


Private Class Utility
    Public Function Max(Of T)(ByVal a As T, ByVal b As T) As T
        If a > b Then
            Return a
        End If
        Return b
    End Function
End Class
' usage of Max
Public Sub MaxTest()
    Dim u As New Utility()
    Dim i As Integer = u.Max(Of Integer)(7, 11)
    ' Use type inference
    Dim d As Double = u.Max(10.0, 42.42)
End Sub



private class Utility
{
    public T Max<T>(T a, T b)
    {
        if (a > b) { return a; }
        return b;
    }
}
 
// usage of Max
public void MaxTest()
{
    Utility u = new Utility();
    int i = u.Max<int>(7, 11);
    double d = u.Max(10.0, 42.42);  //OK, uses type inference
}


When using generic methods, the C# compiler is able to infer the type arguments from the types of the regular parameters passed so you do not have to explicitly state them.

Constraints
Time to “fess up”, the code for the Max method in the example directly above does not compile. The condition (a > b) causes a compiler error because it does not have meaning for all possible values of T. To get it to work you need to modify the code somewhat.


Public Class Utility
    Public Function Max(Of T)(ByVal a As T, ByVal b As T)
        If a.CompareTo(b) > 0 Then
            Return a
        End If
        Return b
    End Function
End Class



public class Utility
{
    public T Max<T>(T a, T b)
    {
        if (a.CompareTo(b) > 0) { return a; }
        return b;
    }
}


OK, it still doesn’t compile. Using CompareTo is a valid solution but it requires that the type assigned to T implement the IComparable interface. You need to indicate this constraint to the C# compiler. This requires one additional code change.



Public Class Utility
    Public Function Max(Of T As IComparable(Of T)) _
        (ByVal a As T, ByVal b As T)
        If a.CompareTo(b) > 0 Then
            Return a
        End If
        Return b
    End Function
End Class



public class Utility
{
    public T Max<T>(T a, T b) where T : IComparable<T> 
    {
        if (a.CompareTo(b) > 0) { return a; }
        return b;
    }
}

Other common constraints are: requiring a particular base type for the type argument, indicating that the type argument must be a reference type or value type, and indicating that the type argument must have a default constructor.


Conclusion
Generics are better for you than Broccoli (assuming that easier to write, type-safe, performant code is important to you, if not the please do a Google search for Ruby to find material more to your taste). As you work with .NET 2.0 you will begin to get the “Zen of Generics” and then you will see that the immediate benefits of strongly-typed collections are really just the tip of the Iceberg<T> where T : Big, ICold.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>