Generics

Introduction

Generics are by far the most complicated new feature of C# 2.0 - and probably the most useful. The basic idea behind generics is that types and methods can be parameterized in terms of types - the same method implementation might be able to deal with both Stream and int without knowing in advance which type it's working with. Generics allow type safety to be achieved with a single implementation where traditionally a different implementation would have been needed for each type, or compile-time type safety would have been lost. It's a massive topic - this page will initially just scratch the surface of what's available. Indeed, while writing it I've kept on thinking of extra things to mention - hence the empty headings at the bottom. As time progresses the page is likely to become more and more detailed, hopefully with fewer and fewer gaps - but I thought it would be better to publish something early rather than to wait for an entirely "finished" page which might never arrive.

A simple example of this is ArrayList. In .NET 1.1, the System.Collections.ArrayList type is used when you don't care about compile-time type safety, and if you want a strongly typed list you have to derive from CollectionBase or use one of the System.Collection.Specialized classes. This is because the interface of ArrayList (and IList itself) is defined just in terms of System.Object. Even if the developer knows that a particular ArrayList should only contain strings, there's no way of telling the compiler that.

In .NET 2.0, you can use the new System.Collections.Generic.IList<T> interface, which is implemented by (amongst other things) System.Collections.Generic.List<T>. The <T> part of the name says that the type has one type parameter. Other types have more type parameters - for example, System.Collections.Generic.IDictionary<TKey, TValue> has two type parameters, one for the key and one for the value of the dictionary entries. This allows you to specify that you want a dictionary with string keys and int values, for instance.

A simple example - using IList<T>

Most of the time you're likely to be using generics rather than writing your own generic methods and types. Here's a very simple example to demonstrate the benefits:

using System;
using System.Collections.Generic;

class Test
{
                  static void Main()
    {
        IList<string> list = new List<string>();
        
        list.Add("Hello");
        list.Add("There");
        
                  // Wouldn't compile - the list is strongly typed,
                  // so there's effectively only an Add(string) method
                  // list.Add(new object());

        
                  // No need for a cast here - again, the list is
                  // strongly typed, so the indexer is of type string.
                  string x = list[0];
    }
}

Here we create a list of strings, add two strings to it, then fetch the first one back using the indexer. The equivalent code using ArrayList would be:

using System;
using System.Collections;

class Test
{
                  static void Main()
    {
        IList list = new ArrayList();
        
        list.Add("Hello");
        list.Add("There");
        
                  // This would compile, because the list isn't
                  // strongly typed.
                  // list.Add(new object());

                  // We need a cast here because the interface only
                  // says that the indexer will return an object.
                  string x = (string)list[0];
    }
}

If we'd added a non-string (a bare System.Object, or an XmlDocument, for example) at the start of the list, the cast on the last line would fail - but only at runtime. With a generic list, the cast isn't required and wouldn't fail anyway, because the list wouldn't have allowed the non-string to be added in the first place.

Value types in generics

The non-generic ArrayList type is always backed by an object[] - an array of references to any old object. That means that if you want to store value types in the list, they have to be boxed first. When you retrieve the value, if you want it back as a value type again you need to unbox it. This boxing and unboxing can be expensive if you do a lot of it - both in processor time and especially in memory. Each value requires an extra object to be created, so a list of 100,000 bytes creates 100,000 objects (each with the overhead of being an object) as well as a reference to each of those objects. Generics allow the List<T> type to be backed by an array of the appropriate type - so a List<byte> is backed by a byte[]. No boxing or unboxing is required, giving a massive boost in memory and processor efficiency when adding to and accessing the list.

For those who are interested in the implementation details, the runtime creates one implementation of the generic type for each different value type used as type parameters, but shares almost all of the implementation of all the code for a generic type parameterised by different reference types. So List<string>, List<XmlDocument> and List<object> all share most of their code, whereas List<byte>, List<int> and List<DateTime> would each have their own separate implementation. The implementations are only created as and when they're needed.

Writing your own generic types

Writing your own generic methods

Type parameter constraints

Type parameter inference

Default value expressions

Covariance and contravariance - and the lack of it!

Ohter pages on generics

There are hundreds of guides to C# 2.0 on the web, and each of them deals with generics in one form or other. In addition, there are many blog entries on individual aspects of generics. Here's a list of links to pages which I've found particularly interesting and enlightening:

Generic type parameter variance in the CLR
A blog entry by Rick Byers explaining what the CLR itself supports in terms of type parameter variance.

Previous page: Implementing iterators with yield statements

Back to the main C# page.