.NET Development Foundation/Generic types
.NET Development Foundation | |
---|---|
Annexes: Generic types
[edit | edit source]Introduction
[edit | edit source]In order to understand Generics, we should first look why we would want to use them. This is best explained with a simple example. Let's say we want to create a collection of clients, this is something that we do often. We take a simple Client class that has a name and an account number. Often an ArrayList will be used to store multiple clients in memory like this:
/// <summary> /// Representation of a client /// </summary> public class Client { private string _name; private string _accountNumber; /// <summary> /// Gets or sets the account number. /// </summary> /// <value>The account number.</value> public string AccountNumber { get { return _accountNumber; } set { _accountNumber = value; } } /// <summary> /// Gets or sets the name. /// </summary> /// <value>The name.</value> public string Name { get { return _name; } set { _name = value; } } /// <summary> /// Initializes a new instance of the <see cref="T:Client"/> class. /// </summary> /// <param name="name">The name.</param> /// <param name="accountNumber">The account number.</param> public Client(string name, string accountNumber) { _name = name; _accountNumber = accountNumber; } /// <summary> /// The Main entry point of the console application /// </summary> /// <param name="args">The command line arguments</param> static void Main(string[] args) { ArrayList clients = new ArrayList(); clients.Add(new Client("Marco", "332-3355")); clients.Add(new Client("Martinus", "453-5662")); foreach (Client client in clients) { Console.WriteLine("The account {0} belongs to {1}", client.AccountNumber, client.Name); } Console.ReadLine(); } }
This probably looks familiar for most of the readers, but by using an ArrayList we are not type safe, which is in my opinion is bad. Also we need to cast (and uncast) the object when we want to do something cool with it. With value types there is an other issue, we constantly Box and unbox the objects in the list.
A better way store the clients would be to use typed collections, this will solve the problem with type safety and we don’t have to cast the retrieved object every time we use it. A good way to do this is by inheriting an object form the CollectionBase class and looks like this:
/// <summary> /// Representation of a client /// </summary> public class Client { private string _name; private string _accountNumber; /// <summary> /// Gets or sets the account number. /// </summary> /// <value>The account number.</value> public string AccountNumber { get { return _accountNumber; } set { _accountNumber = value; } } /// <summary> /// Gets or sets the name. /// </summary> /// <value>The name.</value> public string Name { get { return _name; } set { _name = value; } } /// <summary> /// Initializes a new instance of the <see cref="T:Client"/> class. /// </summary> /// <param name="name">The name.</param> /// <param name="accountNumber">The account number.</param> public Client(string name, string accountNumber) { _name = name; _accountNumber = accountNumber; } /// <summary> /// The Main entry point of the console application /// </summary> /// <param name="args">The command line arguments</param> static void Main(string[] args) { ClientList clients = new ClientList(); clients.Add(new Client("Marco", "332-3355")); clients.Add(new Client("Martinus", "453-5662")); foreach (Client client in clients) { Console.WriteLine("The account {0} belongs to {1}", client.AccountNumber, client.Name); } Console.ReadLine(); } } /// <summary> /// A list of clients /// </summary> public class ClientList : CollectionBase { /// <summary> /// Adds the specified client to the list. /// </summary> /// <param name="client">The client.</param> /// <returns></returns> public int Add(Client client) { return List.Add(client); } /// <summary> /// Gets or sets the <see cref="T:Client"/> at the specified index. /// </summary> /// <value></value> public Client this[int index] { get { return (Client)List[index]; } set { List[index] = value; } } }
This looks a lot better than when we where using the ArrayList in the first example and is in general a good way to go. But what if your application grows, and we get more types that we want to store in collections like an Account, or a bank. With the 1.1 framework we had to create a new collection class for every type of object that we used, or fall back to the ugly ArrayList method. However with the new 2.0 framework MS has added Generics. This makes it possible to create classes and methods that use type parameters. This allows developers to create classes and methods that defer the specifications of certain types until the class is defined and instantiated in the code. By using the generic type parameters developers can write classes that others can use without incurring the risks involved with un-typed classes like ArrayList and reduces the work developers have to do compared to creating typed collections. So lets look at how the code looks when we are using the Generic List<T> class from the framework.
/// <summary> /// Representation of a client /// </summary> public class Client { private string _name; private string _accountNumber; /// <summary> /// Gets or sets the account number. /// </summary> /// <value>The account number.</value> public string AccountNumber { get { return _accountNumber; } set { _accountNumber = value; } } /// <summary> /// Gets or sets the name. /// </summary> /// <value>The name.</value> public string Name { get { return _name; } set { _name = value; } } /// <summary> /// Initializes a new instance of the <see cref="T:Client"/> class. /// </summary> /// <param name="name">The name.</param> /// <param name="accountNumber">The account number.</param> public Client(string name, string accountNumber) { _name = name; _accountNumber = accountNumber; } /// <summary> /// The Main entry point of the console application /// </summary> /// <param name="args">The command line arguments</param> static void Main(string[] args) { List<Client> clients = new List<Client>(); clients.Add(new Client("Marco", "332-3355")); clients.Add(new Client("Martinus", "453-5662")); foreach (Client client in clients) { Console.WriteLine("The account {0} belongs to {1}", client.AccountNumber, client.Name); } Console.ReadLine(); } }
Thanks to Generics we have created a type-safe collection as easy as we would normally instantiate an ArrayList without writing our own typed collection. Now that we have had a brief look at how we can use Generics to reduce the amount of code we need to write, while still using typed collections let's see how we could create our custom Generic Class. To demonstrate this I have created an example Class which inherits from the DictionaryBase class. This implementation of the dictionary class will accept a GUID as the key and has a Type parameter as the value type. If you are a bit like me you will use GUIDS to identify your records in a database so using this as the key in a Typed collection was something I liked to do a lot. So now I’ve got a cool dictionary that I can use with all my objects I created when retrieving data from my database, and I will give you the code:
/// <summary> /// Representation of a client /// </summary> public class Client { private string _name; private string _accountNumber; /// <summary> /// Gets or sets the account number. /// </summary> /// <value>The account number.</value> public string AccountNumber { get { return _accountNumber; } set { _accountNumber = value; } } /// <summary> /// Gets or sets the name. /// </summary> /// <value>The name.</value> public string Name { get { return _name; } set { _name = value; } } /// <summary> /// Initializes a new instance of the <see cref="T:Client"/> class. /// </summary> /// <param name="name">The name.</param> /// <param name="accountNumber">The account number.</param> public Client(string name, string accountNumber) { _name = name; _accountNumber = accountNumber; } /// <summary> /// The Main entry point of the console application /// </summary> /// <param name="args">The command line arguments</param> static void Main(string[] args) { GuidDictionary<Client> clients = new GuidDictionary<Client>(); Guid clientID1 = Guid.NewGuid(); Guid clientID2 = Guid.NewGuid(); clients.Add(clientID1, new Client("Marco", "332-3355")); clients.Add(clientID2, new Client("Martinus", "453-5662")); Console.WriteLine("The account {0} belongs to {1}", clients[clientID1].AccountNumber, clients[clientID1].Name); Console.WriteLine("The account {0} belongs to {1}", clients[clientID2].AccountNumber, clients[clientID2].Name); Console.ReadLine(); } } public class GuidDictionary<T> : DictionaryBase { public void Add(Guid id, T item) { Dictionary.Add(id, item); } public T this[Guid id] { get { return (T)Dictionary[id]; } set { Dictionary[id] = value; } } }
Well it's probably not as wonderful as you expected, a lot of methods the standard dictionary has aren’t even implemented, but hey I have to leave some fun things to do for you the reader.
So what exactly did we do in the above code? We have created a Dictionary that is indexed by a Guid and uses a type parameter to limit our add method and our indexer. Now when we create a new instance of the class and specify the type parameter we have a type-safe dictionary which can be used to store and retrieve the type of objects by the given Guid.
The T in the declaration is only a name for the thing, we could as easily use the following: VeryLongNameForTheTypeParameter instead of T. Ok so we have seen how to use the type parameter to create a generic class. But before we move on to the next part lets look at the System.Collections.Generic.Dictionary<>. This class must be instantiated by providing 2 type parameters named TKey and TValue. This shows us that we can use more than one type parameter in our definition, and also shows that it was a waste of time to build my own dictionary class I could as easily used the generic dictionary like this: Dictionary<Guid, Client> and be over with it.