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.
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.
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.
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:
Back to the main C# page.