Using Generics With C#

Objective of the Module

  • Introduction
  • Boxing and Unboxing
  • Generic Classes
  • Generic Methods
  • Dictionary
  • Queues
  • Stack

Introduction

Generics procedure many of the benefits of strongly-typed collections as well as provide a higher quality of and a performance boost for code. Generics are very similar to C++ templates but having a slight difference in such a way that the source code of C++ templates is required when a templates is instantiated with a specific type and .NET Generics are not limited to classes only. In fact they can also be implemented with Interfaces, Delegates and Methods. The detailed specification for each collection is found under the System.Collection.Generic namespace.

Boxing and Unboxing

.Net defines two major categories of data type termed value type and reference type to represent a variable. This is where boxing and unboxing are needed. Boxing is a mechanism to explicitly convert a value type to a reference type by storing the variable into System.Object; when you box the value the CLR allocates a new object into the heap and copies the value type's value into that instance. For example you have created a variable of int type as:

int a = 20;
object b = a; //boxing

The opposite operation is Unboxing which is the process of converting back the reference type into the value type. This process verifies that the receiving data type is equivalent to the boxed type as;

int c = (int)b; // unboxing

The C# compiler sees the assignment from int to object and vice-versa. When this program is compiled and you examine the IL generated code via IL dissembler, you notice that the program respond by inserting a box instruction in the IL automatically when b is assigned the value of a and an unbox instruction when c is assigned the value b as in the following;

IL-opcode.jpg

Figure 1.1 - IL opcode

The code loads the constant 20 and stores it in the local slot; it the loads the value 20 onto the stack and boxes it. Finally it loads the boxed 20 back onto the stack and unboxes it into an int.

There are  series of operations performed by .NET CLR, such as, first an object is allocated in the managed heap, then in boxing the value is transformed into the memory location and during unboxing the value is stored on the heap and must be transferred back to the stack. So the Boxing and Unboxing process has a significant importance in Generics from the performance point of view because this process is more resource-intensive rather than using Generics.

Generic Classes

The Generic class can be defined by putting the <T> sign after the class name. It isn't mandatory to put the "T" word in the Generic type definition. You can use any word in the TestClass<> class declaration.

public class TestClass<T> { }

The System.Collection.Generic namespace also defines a number of classes that implement many of these key interfaces. The following table describes the core class types of this namespace.

Non-Generics Members Description
Collection<T>

The basis for a generic collection Comparer compares two generic objects for equality

Dictionary<TKey, TValue>

A generic collection of name/value pairs

List<T>

A dynamically resizable list of Items

Queue<T>

A generic implementation of a first-in, first-out (FIFO) list

Stack<T>

A generic implementation of a last-in, first-out (LIFO) list

Simple Generic Class Example

The following example shows a simple Generic type manipulation. The TestClass<T> defines an array of generic type with length 5. The Add() method is responsible for adding any type of objects into the collection and the Indexer property is an implementation of foreach statement iteration. Finally in the main class we instantiated the TestClass<T> class with an Integer type reference and adds some integer type elements into the collection using the Add() method.

using System;
using System.Collections.Generic;

namespace GenericApp
{
    public class TestClass<T>
    {
        // define an Array of Generic type with length 5
        T[] obj = new T[5];
        int count = 0;

        // adding items mechanism into generic type
        public void  Add(T item)
        {
            //checking length
            if (count + 1 < 6)
            {
                obj[count] = item;

            }
            count++;
        }
        //indexer for foreach statement iteration
        public T this[int index]
        {
            get { return obj[index]; }
            set { obj[index] = value; }
        } 
    }
    class Program
    {
        static void Main(string[] args)
        {
            //instantiate generic with Integer
            TestClass<int> intObj = new TestClass<int>();

            //adding integer values into collection
            intObj.Add(1);
            intObj.Add(2);
            intObj.Add(3);     //No boxing
            intObj.Add(4);
            intObj.Add(5);

            //displaying values
            for (int i = 0; i < 5; i++)
            {
                Console.WriteLine(intObj[i]);   //No unboxing
            }
            Console.ReadKey(); 
        }
    }
}      


After building and running this program, the output of the program is as shown in the following;

Simple-Generic-Example.jpg

Figure 1.2 - Simple Generic Example

There are some significant characteristics of Generic types that make them special to the conventional non-generics type as follows;

  • Type Safety

  • Performance

  • Binary Code reuse

Type Safety

One of the most significant features of Generics is Type Safety. In the case of the non-generic ArrayList class, if objects are used, any type can be added to the collections that can sometimes result in a great disaster. The following example shows adding an integer, string and object to the collection of an ArrayList type;

ArrayList obj = new ArrayList();
obj.Add(50);
obj.Add("Dog");
obj.Add(new TestClass());

Now, if the collection is iterated through the foreach statement using integer elements, the compiler accepts the code but because all the elements in the collection are not an integer, a runtime exception occurs;

foreach(int i in obj)
{
    Console.WriteLine(i); 
}

The rule of thumb in programming is that Errors should be detected as early as possible. With the generic class Test<T>, the generic type T defines what types are allowed. With the definition of Test<int>, only an integer type can be added to the collection. The compiler doesn't compile the code because the Add() method has invalid arguments as follows;

Test<int> obj = new Test<int>();
obj.Add(50);
obj.Add("Dog");            //compiler error
obj.Add(new TestClass());  //compiler error


Performance

Another feature of Generics is performance. Using value types with non-generic collection classes result in boxing and unboxing overhead when a value type is converted to reference type and vice-versa.

In the following example, the ArrayList class stores objects and the Add() method is defined to store some integer type argument. So an integer type is boxed. When the value from ArrayList is read using the foreach statement, unboxing occurrs.

ArrayList  obj = new ArrayList(); 

obj.Add(50);    //boxing- convert value type to reference type

int x= (int)obj[0]; //unboxing

 

foreach(int i in obj)

{

   Console.WriteLine(i);   // unboxing

}

Note: Generics are faster than other collections such as ArrayList.

Instead of using objects, a Generics type of the TestClass<T> class is defined as an int, so an int type is used inside the class that is generated dynamically from the compiler. Therefore boxing and unboxing no longer occurs as in the following;

TestClass<int> obj = new TestClass<int>();

obj.Add(50);    //No boxing

int x= obj[0]; // No unboxing

 

foreach(int i in obj)

{

   Console.WriteLine(i);   //No unboxing

}

Binary Code reuse

Generics provide a kind of source code protection. A Generic class can be defined once and can be instantiated with many different types. Generics can be defined in one CLR supported language and used from another .NET language. The following TestClass<T> is instantiated with an int and string types:

TestClass<int> obj = new TestClass<int>();

obj.Add(50);

 

TestClass<string> obj1 = new TestClass<string>();

Obj1.Add("hello");   

Generic Methods

While most developers will typically use the existing generic types within the base class libraries, it is certainly possible to build your own generic members and custom generic types.

The objective of this example is to build a swap method that can operate on any possible data type (value-based or reference-based) using a single type parameter. Due to the nature of swapping algorithms, the incoming parameters will be sent by reference via ref keyword.

using System;
using System.Collections.Generic;

namespace GenericApp
{
    class Program
    {
        //Generic method
        static void Swap<T>(ref T a, ref T b)
        {
            T temp;
            temp = a;
            a = b;
            b = temp;
        }
        static void Main(string[] args)
        {
            // Swap of two integers.
            int a = 40, b = 60;
            Console.WriteLine("Before swap: {0}, {1}", a, b);

            Swap<int>(ref a, ref b);

            Console.WriteLine("After swap: {0}, {1}", a, b);

            Console.ReadLine();
        }
    }
}         


After compiling this Generic method implementation program, the output is as in the following;

Generic-Methods.jpg

Figure 1.3 - Generic Methods

Dictionary

Dictionaries are also known as maps or hash tables. It represents a data structure that allows you to access an element based on a key. One of the significant features of a dictionary is faster lookup; you can add or remove items without the performance overhead.

.Net offers several dictionary classes, for instance Dictionary<TKey, TValue>. The type parameters TKey and TValue represent the types of the keys and the values it can store, respectively.

Simple Example of Dictionary

The following example demonstrates simple dictionary collections using Generics. In this program, a Dictionary type object is created that accepts an int as the key and a string as the value. Then we add some string values into the dictionary collection and finally displaying the dictionary collection elements.

Using System;
using System.Collections.Generic;

namespace GenericApp
{
    public class Program 
    {
        static void Main(string[] args)
        {
            //define Dictionary collection
            Dictionary<int,string> dObj = new Dictionary<int,string>(5);

            //add elements to Dictionary

            dObj.Add(1,1,"Tom"); 
            dObj.Add(2,"John");
            dObj.Add(3, "Maria");
            dObj.Add(4, "Max");
            dObj.Add(5, "Ram");

            //print data
            for (int i = 1; i <= dObj.Count;i++)
            {
                Console.WriteLine(dObj[i]);
            }
            Console.ReadKey();
        }
    }
}


The following example portrays some more complexities by defining an addition class emp where we are overriding the ToString() method to display the name and salary of a particular employees. Later in the Main() method , a new Dictionary<TKey,TValue) instance is created, where the key is of type string and the value is of type emp. The constructor allocates a capacity of 2 elements. The emp objects and string value as a key is added to the dictionary collection. Finally the collection elements are iterated using a foreach statement and displayed on the screen.

using System;
using System.Text;
using System.Collections.Generic;

namespace GenericApp
{
    public class emp
    {
        private string name;
        private int salary;
 
        public emp(string name,int salary)
        {
            this.name = name;
            this.salary = salary;
        }
        public override string ToString()
        {
            StringBuilder sb = new StringBuilder(200);
            sb.AppendFormat("{0},{1}",name,salary);

            return sb.ToString();
        }

    }
    public class Program 
    {
        static void Main(string[] args)
        {
            //define Dictionary collection
            Dictionary<string, emp> dObj = new Dictionary<string, emp>(2);

            //add elements to Dictionary
            emp tom = new emp("tom", 2000);
            dObj.Add("tom",tom);   // key,value
            emp john = new emp("john", 4000);
            dObj.Add("john",john);

            //print data
            foreach(Object str in dObj.Values)
            {
               Console.WriteLine(str);
            }
 
            Console.ReadKey();
        }
    }
}

Queues

Queues are a special type of container that ensures the items are being accessed in a FIFO (first in, first out) manner. Queue collections are most appropriate for implementing messaging components. We can define a Queue collection object using the following syntax:

Queue qObj = new Queue();

The Queue collection property, methods and other specification definitions are found under the Sysyem.Collection namespace. The following table defines the key members;

System.Collection.Queue Members Definition
Enqueue() Add an object to the end of the queue.
Dequeue() Removes an object from the beginning of the queue.
Peek() Return the object at the beginning of the queue without removing it.


The following demonstrates a basic Queues type collection, adds some string type values into the collection and finally displays the entire items into the collection using the while statement.

Using System;
using System.Collections;

namespace GenericApp
{
    class Program
    {
        static void Main(string[] args)
        {
            //Defines a Queue.
            Queue qObj = new Queue();

            //adding string values into collection
            qObj.Enqueue("Tom");
            qObj.Enqueue("Harry");
            qObj.Enqueue("Maria");    
            qObj.Enqueue("john");

            //displaying collections
            while(qObj.Count !=0 )
            {
                Console.WriteLine(qObj.Dequeue());
            }

            Console.ReadKey(); 
        }
    }
}

Stacks

A Stack collection is an abstraction of LIFO (last in, first out). We can define a Stack collection object using the following syntax:

Stack qObj = new Stack();

The following table illustrates the key members of a stack;

System.Collection.Stack Members Definition
Contains() Returns true if a specific element is found in the collection.
Clear() Removes all the elements of the collection.
Peek() Previews the most recent element on the stack.
Push() It pushes elements onto the stack.
Pop() Return and remove the top elements of the stack.


The following demonstrate a stack collection. First an array type object is referenced into the stack collection. Then the value of the elements in the collection are removed from the stack using the Pop() method and displayed on the screen.
 

using System;
using System.Collections;

namespace GenericApp
{
    class Program
    {
        static void Main(string[] args)
        {
            int[] iArray = new int[] {1,2,3,4,5,6,7,8,9,10 };

            //Define a stack
            Stack sObj = new Stack(iArray);

            Console.WriteLine("Total items="+sObj.Count);

            //displaying collections
            for (int i = 0; i < sObj.Count;++i )
            {
                Console.WriteLine(sObj.Pop());
            }

            Console.ReadKey(); 
        }
    }
}


In another example with a Generic implementation, 5 items are added to the stack with the Push() method. With the foreach statement, all the items are iterated using the IEnumerable interface. The enumerator of the stack does not remove the items; it just returns each item in a LIFO manner as in the following:

using System;
using System.Collections.Generic;

namespace GenericApp
{
    public class Program 
    {
        static void Main(string[] args)
        {
            //define Dictionary collection
            Dictionary<int,string> dObj = new Dictionary<int,string>(5);

           //add elements to Dictionary
            dObj.Add(1,"Tom"); 
            dObj.Add(2,"John");
            dObj.Add(3, "Maria");
            dObj.Add(4, "Max");
            dObj.Add(5, "Ram");

            //print data
            foreach(string i in dObj.Values)
            {
                Console.WriteLine(i);
            }
            Console.ReadKey();
        }
    }
}