Generics in .NET: Type Safety, Performance, and Generality

Welcome to this installment of the .NET Nuts & Bolts column. To fulfill some requests I've received, this article expands on the use of lists and collections in Microsoft .NET. Its focal point is generics, which is a new addition in the version 2.0 release of the Microsoft .NET Framework. The article explains why generics are valuable and what they can add to your applications, exploring the classes in the System.Collections.Generic namespace.

The Generic Problem

The first release of the Microsoft .NET Framework introduced a System.Collections namespace. The namespace contains a number of different interfaces and classes that define various types of helpful container lists, dictionaries, and hashtables. Each has a different implementation and serves a distinct and specific purpose. A collection is an example of a commonly used class within the namespace. It is a container to which objects of any type can be added. This offers powerful flexibility and adaptability.

Collections also have a number of drawbacks, however. All items are stored as objects; this results in an overhead as items are added or removed from the collection because they have to be unboxed and boxed to specific types. The overhead creates a performance penalty each time an item in the collection or list is accessed. Given that items in collections and lists are often traversed in a loop structure, the performance penalty compounds and can really affect the overall performance of your application. Additionally, no type checking enforces consistent use of the same type within the collection at compile time, which can lead to unforeseen runtime errors.

The 1.0 and 1.1 versions of the Microsoft .NET Framework did not have a solution for the performance hit for boxing and unboxing types. The types always had to be cast when accessed from the list. They did offer a workaround to enforce consistent type use, though. The workaround involves writing wrappers around ArrayList and other collection and list types to limit the specific types that can be assigned. This is less than ideal because you end up creating a wrapper class for each specific type use. The result is a lot of code that doesn't vary a whole lot between the contained types. Even if you employ a code generator—a number will assist you with this—it still amounts to a fair amount of additional code to maintain.

CollectionBase Sample Code

The following sample demonstrates the maintenance problem. It defines a list that has the sample TestItem as the specific type it contains. The example includes sample methods for add and remove functionality that limit the collection to the specified type:

/*
 * Sample class to add to lists.
 */
private class TestItem
{
   private int _ItemValue = 0;
   public int ItemValue
   {
      get { return this._ItemValue; }
      set { this._ItemValue = value; }
   }

   public TestItem(int itemValue)
   {
         this.ItemValue = itemValue;
   }
}

/*
 * Sample collection that is specific to TestItem type.
 */
private class TestItemCollection : CollectionBase
{
   public TestItem this[ int index ]
   {
      get { return( (TestItem) List[index] ); }
      set { List[index] = value; }
   }

   public int Add( TestItem itemValue )
   {
      return( List.Add( itemValue ) );
   }

   public void Remove( TestItem itemValue )
   {
      List.Remove( itemValue );
   }
}

/*
 * Code to demonstrate using our TestItemCollection type.
 */
TestItemCollection testCollection = new TestItemCollection();

// Add some numbers to the list
for( int i = 0; i < 20; i++ )
{
   testCollection.Add(new TestItem(i));
}

IEnumerator listEnumerator = testCollection.GetEnumerator();
while( listEnumerator.MoveNext() )
{
   Console.WriteLine("{0}", ((TestItem)
                             (listEnumerator.Current)).ItemValue);
}

// Wait so you can read the output
System.Console.ReadLine();

As you can see, the item also must be cast when accessed from the collection. This obligatory cast means that an error would result if another type of object were put into this collection. The code serves its purpose to create a type safe collection for dealing with the TestItem class, but it is not reusable for other types. If you have a number of such type-specific classes, maintaining them becomes so unwieldy that it almost isn't worth it for the type safety provided.

The Generic Solution

Generics were introduced to offer a combination of type safety, performance, and generality in the defined type, which is an all-around win. A generic is a type-safe class that is declared without a specific type applied to it in the definition. Rather, the type is specified at the time the object is used. In the previous example, that would be equivalent to specifying TestItem as the type at the time you declare your collection variables. This would limit that particular item from holding any type other than a TestItem. The code will not compile if you attempt to assign something such as a DataSet. The System.Collections.Generic namespace contains a number of pre-built classes that represent common types, including the following:

  • Linked List—A doubly linked list
  • List—An array that is dynamically sized as needed
  • Queue—Represents a first in, first out collection of objects
  • Stack—Represents a last in, first out collection of objects

Using Existing Generics Sample Code

The language syntax for generics takes a bit of adjustment (at least it did for me), but in the end the functionality provided far outweighs any learning curve necessary to get used to the syntax. In C#, the generic type can be identified by the less than (<) and greater than (>) symbols after the generic type name. The specific type you want applied to the generic type is specified within the symbols. In this example, you simply use the predefined list type rather than creating your own generic code:

/*
 * Sample use of Generic List type.
 */
List<TestItem> testCollection = new List<TestItem>();

// Add some numbers to the list
for (int i = 0; i < 20; i++)
{
   testCollection.Add(new TestItem(i));
}

foreach( TestItem test in testCollection )
{
   Console.WriteLine("{0}", test.ItemValue);
}

// Wait so you can read the output
System.Console.ReadLine();

If you try to assign an object of any type other than TestItem to the testCollection instance above, you receive a compile time error.

Generics in .NET: Type Safety, Performance, and Generality

Defining a Generic Sample Code

To give you a feel for what defining your own types looks like, the following basic example defines a generic type. It really isn't a whole lot different than normal class declarations:

/*
 * Sample generic type allowing you to add a node and access as
 * an array.
 */
class TestHolder<T>
{
   private List<T> childNodes;

   public T this[int index]
   {
      get { return (T)this.childNodes[index]; }
   }

   public TestHolder()
   {
      childNodes = new List<T>();
   }

   public void Add(T node)
   {
      this.childNodes.Add(node);
   }
}

The following example code uses the TestHolder defined above. Note that it doesn't use a foreach statement to traverse the list (because it's a custom class and I didn't provide an enumerator):

/*
 * Code to demonstrate using our TestHolder
 */
TestHolder<TestItem> testCollection = new TestHolder<TestItem>();

// Add some numbers to the list
for (int i = 0; i < 200; i++)
{
   testCollection.Add(new TestItem(i));
}

for (int i = 0; i < 200; i++)
{
   Console.WriteLine("{0}", testCollection[i].ItemValue);
}

// Wait so you can read the output
System.Console.ReadLine();

Quick Performance Comparison

Earlier, I touched on a performance issue due to the boxing and unboxing required with non-generic collections. This section uses the previous examples to perform a quick performance comparison between the two.

Performance Comparison Code

I've incorporated Environment.TickCount into the previous code examples to track the number of milliseconds for execution. It won't be the most scientifically correct comparison ever done, but it should work for this purpose. The results are displayed to the console:

static void Main(string[] args)
{
   TestClassic();
   TestGeneric();
}

static void TestClassic()
{
   /*
    * Code to demonstrate using our TestItemCollection
    */
   TestItemCollection testCollection = new TestItemCollection();

   // Add some numbers to the list
   int start = Environment.TickCount;
   Console.WriteLine("Starting " + start);
   for (int i = 0; i < 200; i++)
   {
      testCollection.Add(new TestItem(i));
   }

   System.Collections.IEnumerator listEnumerator =
      testCollection.GetEnumerator();
   while (listEnumerator.MoveNext())
   {
      Console.WriteLine("{0}", ((TestItem)
                                (listEnumerator.Current)).ItemValue);
   }
   int end = Environment.TickCount;
   int diff = end - start;
   Console.WriteLine("Ending time {0}, difference {1}", end, diff);

   // Wait so you can read the output
   System.Console.ReadLine();

}

static void TestGeneric()
{
   /*
    * Code to demonstrate using our TestItemCollection
    */
   List<TestItem> testCollection = new List<TestItem>();

   // Add some numbers to the list
   int start = Environment.TickCount;
   Console.WriteLine("Starting " + start);
   for (int i = 0; i < 200; i++)
   {
      testCollection.Add(new TestItem(i));
   }

   foreach( TestItem test in testCollection )
   {
      Console.WriteLine("{0}", test.ItemValue);
   }
   int end = Environment.TickCount;
   int diff = end - start;
   Console.WriteLine("Ending time {0}, difference {1}", end, diff);

   // Wait so you can read the output
   System.Console.ReadLine();

}

As the dialogs in Figures 1 and 2 show, the generic version is faster than the non-generic version—even for simply traversing the list. You can imagine the performance savings when you get into larger lists or more complex operations than simply writing to the console.

Figure 1: Classic Results

Figure 2: Generic Results

Other Considerations

There is much more to generics than what this article demonstrated. You can apply generics to parameters, method return types, and more. Do some more exploration on your own to get the full understanding.

Future Columns

The next column has yet to be determined. If you have something in particular that you would like to see explained, please e-mail me at mstrawmyer@crowechizek.com.



About the Author

Mark Strawmyer

Mark Strawmyer is a Senior Architect of .NET applications for large and mid-size organizations. He specializes in architecture, design and development of Microsoft-based solutions. Mark was honored to be named a Microsoft MVP for application development with C# for the fifth year in a row. You can reach Mark at mark.strawmyer@crowehorwath.com.

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: December 11, 2014 @ 1:00 p.m. ET / 10:00 a.m. PT Market pressures to move more quickly and develop innovative applications are forcing organizations to rethink how they develop and release applications. The combination of public clouds and physical back-end infrastructures are a means to get applications out faster. However, these hybrid solutions complicate DevOps adoption, with application delivery pipelines that span across complex hybrid cloud and non-cloud environments. Check out this …

  • VMware vCloud® Government Service provided by Carpathia® is an enterprise-class hybrid cloud service that delivers the tried and tested VMware capabilities widely used by government organizations today, with the added security and compliance assurance of FedRAMP authorization. The hybrid cloud is becoming more and more prevalent – in fact, nearly three-fourths of large enterprises expect to have hybrid deployments by 2015, according to a recent Gartner analyst report. Learn about the benefits of …

Most Popular Programming Stories

More for Developers

RSS Feeds