C# Generics: Introduction to Generic Collections in Lesson 20

Introduction to Generic Collections

In Lesson 02, you learned about arrays and how they allow you to add and retrieve a collection of objects. Arrays are good for many tasks, but C# v2.0 introduced a new feature called generics. Among many benefits, one of the main ones is that generics allow us to create collections that enable us to do more than an array. This lesson on C# Generics will introduce you to generic collections and how to use it. Here are the objectives for this lesson:

  • Understand how generic collections can benefit you.
  • Learn how to create and use a generic List.
  • Write code that implements a generic Dictionary.

C# Generics: What Can Generics Do For Me?

Throughout this tutorial about Generics in C#, you’ve learned about types, whether built-in (int, float, char) or custom (Shape, Customer, Account). In .NET v1.0 there were collections, such as the ArrayList for working with groups of objects.

An ArrayList is much like an array, except it could automatically grow and offer many convenience methods that arrays don’t have. The problem with ArrayList and all the other .NET v1.0 collections is that they operate on type object. Since all objects derive from the object type, you can assign anything to an ArrayList.

Furthermore

The problem with this is that you incur performance overhead converting value type objects to and from the object type and a single ArrayList could accidentally hold different types. That would cause hard to find errors at runtime because you wrote code to work with one type. Generic collections fix these problems.

A generic collection is strongly typed (type safe), meaning that you can only put one type of object into it. This eliminates type mismatches at runtime. Another benefit of type safety is that performance is better with value type objects because they don’t incur the overhead of being converted to and from type object.

With generic collections, you have the best of all worlds because they are strongly typed, like arrays. You also have the additional functionality, like ArrayList and other non-generic collections, without the problems.

Creating Generic List<T> Collections in C#

The pattern for using a List collection in C# is similar to arrays. You declare the List, populate its members, then access the members. Here’s a code example of how to use a List:

    List<int> myInts = new List<int>();

    myInts.Add(1);
    myInts.Add(2);
    myInts.Add(3);

    for (int i = 0; i < myInts.Count; i++)
    {
        Console.WriteLine("MyInts: {0}", myInts[i]);
    }

The first thing you should notice is the generic collection List<int>, referred to as List of int. If you looked in the documentation for this class, you would find it defined as List<T>, where T could be any type. If you wanted the list to work on string orCustomer objects, you could define them as List<string> or List<Customer>. They would hold the only string or Customer objects. In the example above, myInts holds only type int.

Using the Add method, you can add as many int objects to the collection as you want. This is different from arrays, which have a fixed size. The List<T> class has many more methods you can use, such as Contains, Remove and more.

Furthermore

There are two parts of the loop that you need to know about. First, the condition uses the Count property of myInts. This is another difference between collections and arrays in that an array uses a Length property for the same thing. Next, the way to read from a specific position in the List<T> collection, myInts[i], is the exact same syntax you use with arrays.

The next time you use a single-dimension array, consider using a List<T> instead. That said, be sure to let your solution fit the problem and use the best tool for the job. i.e. it’s common to work with byte[] in many places in the .NET Framework.

Working with Dictionary<TKey, TValue> Collections

Another useful generic collection is the Dictionary, which works with key/value pairs. There is a non-generic collection called a Hashtable, which does the same thing, except that it operates on type object. However, you want to avoid the non-generic collections and use their generic counterparts instead. The scenario I’ll use for this example is that you have a list of Customers that you need to work with.

It would be natural to keep track of these Customers via their CustomerID. The Dictionary example will work with instances of the following Customer class:

    public class Customer
    {
        public Customer(int id, string name)
        {
            ID = id;
            Name = name;
        }

        private int m_id;

        public int ID
        {
            get { return m_id; }
            set { m_id = value; }
        }

        private string m_name;

        public string Name
        {
            get { return m_name; }
            set { m_name = value; }
        }
    }

The Customer class above has a constructor to make it easier to initialize. It also exposes its state via public properties. It isn’t sophisticated at this point. That’s okay because its only purpose is to help you learn how to use a Dictionary collection.  The following example populates a Dictionary collection with Customer objects and then shows you how to extract entries from the Dictionary:

     Dictionary<int, Customer> customers = new Dictionary<int, Customer>();

    Customer cust1 = new Customer(1, "Cust 1");
    Customer cust2 = new Customer(2, "Cust 2");
    Customer cust3 = new Customer(3, "Cust 3");

    customers.Add(cust1.ID, cust1);
    customers.Add(cust2.ID, cust2);
    customers.Add(cust3.ID, cust3);

    foreach (KeyValuePair<int, Customer> custKeyVal in customers)
    {
        Console.WriteLine(
            "Customer ID: {0}, Name: {1}",
            custKeyVal.Key,
            custKeyVal.Value.Name);
    }

The customers variable is declared as a Dictionary<int, Customer>.  Considering that the formal declaration of Dictionary isDictionary<TKey, TValue>, the meaning of customers is that it is a Dictionary where the key is type int and the value is type Customer. Therefore, any time you add an entry to the Dictionary, you must provide the key. That is because it is also the key that you will use to extract a specified Customer from the Dictionary.

I created three Customer objects, giving each an ID and a Name. I’ll use the ID as the key and the entire Customer object as the value. You can see this in the calls to Add, where I added custX.ID as the key (first parameter), as well as adding the custX instance as the value (second parameter).

Extracting information from a Dictionary is a little bit different. Iterating through the customer’s Dictionary with a for each loop, the type returned is KeyValuePair<TKey, TValue>, where TKey is type int, and TValue is type Customer. That is because those are the types that the customer’s Dictionary is defined with.

Furthermore

Since custKeyVal is type KeyValuePair<int, Customer>, it has Key and Value properties for you to read from. In our example,custKeyVal.Key will hold the ID for the Customer instance and custKeyVal.Value will hold the whole Customer instance. The parameters in the Console.WriteLine statement demonstrates this by printing out the ID. That is obtained through the Key property, and the Name, obtained through the Name property of the Customer instance, is returned by the Value property.

The Dictionary type is handy for those situations where you need to keep track of objects via some unique identifier. For your convenience, here’s Listing 20-1, shows how both the List and Dictionary collections work.

Listing 20-1. Introduction to Using Generic Collections with an Example of the List<T> and Dictionary<TKey, TValue> Generic Collections
using System;
using System.Collections.Generic;

public class Customer
{
    public Customer(int id, string name)
    {
        ID = id;
        Name = name;
    }

    private int m_id;

    public int ID
    {
        get { return m_id; }
        set { m_id = value; }
    }

    private string m_name;

    public string Name
    {
        get { return m_name; }
        set { m_name = value; }
    }
}

class Program
{
    static void Main(string[] args)
    {
        List<int> myInts = new List<int>();

        myInts.Add(1);
        myInts.Add(2);
        myInts.Add(3);

        for (int i = 0; i < myInts.Count; i++)
        {
            Console.WriteLine("MyInts: {0}", myInts[i]);
        }

        Dictionary<int, Customer> customers = new Dictionary<int, Customer>();

        Customer cust1 = new Customer(1, "Cust 1");
        Customer cust2 = new Customer(2, "Cust 2");
        Customer cust3 = new Customer(3, "Cust 3");

        customers.Add(cust1.ID, cust1);
        customers.Add(cust2.ID, cust2);
        customers.Add(cust3.ID, cust3);

        foreach (KeyValuePair<int, Customer> custKeyVal in customers)
        {
            Console.WriteLine(
                "Customer ID: {0}, Name: {1}",
                custKeyVal.Key,
                custKeyVal.Value.Name);
        }

        Console.ReadKey();
    }
}

Whenever coding with the generic collections, add a using System.Collections.Generic declaration to your file, just as in Listing 20-1.

Final Thoughts

Generic collections give you the best of both worlds with the strong typing of arrays and flexibility of non-generic collections. There are many more generic collections to choose from also, such as Stack, Queue, and SortedDictionary. Look in the System.Collections.Genericnamespace for other C# Generics collections.

The advantages of generics are that they increase the reusability of the code and are safe. The user will get compile-time errors when using a different type of data than specified in the definition. Generics also have a performance advantage due to removing the possibility of boxing and unboxing. Boxing involves converting a type to an object. Unboxing is converting the object to a type. The other definition of unboxing is unwrapping the type from the object container.

I invite you to return for Lesson 21: Anonymous Methods.