Gear Up for Generics

Generics support in version 2 of the .NET Framework will help you write simpler, more powerful code, whether you consume generic classes built into the Framework or roll your own.

Gear Up for Generics
A new language feature in the .NET Framework's next version will help your code become more powerful, flexible, and clear.
by Bill McCarthy

Technology Toolbox: VB.NET, C#

Generics allow you to write more-robust code with a greater degree of flexibility. They also offer improved performance in many situations and make it possible to simplify common code constructs. The next version of the .NET Framework will include support for generics in response to what Microsoft called strong user demand.

Generics are classes and/or methods that have data type parameters. This allows .NET to compile the class and define the type or types used for members when it uses the class. Generics are commonly referred to as templates for this reason; the types are the template's blanks.

A simple example of a generic class is the IList<T> class that the .NET Framework provides in the System.Collections.Generics namespace. The <T> means the class is expecting one type parameter—T. The main difference between the IList interface and the new IList<T> interface, apart from the fact that they're in different namespaces, is that Item is As Object for IList, and Item is As T for IList<T>. This enables strong typing, which is crucial for killing bugs at design time.

By definition, the most common use of generics is the consumption of generic classes, because generics are templates. For example, if you have a class called Banana and want a strongly typed ArrayList of Banana, you can use the generic List<T> class:

Dim myBananas As List(Of Banana)

You consume the generic template simply by declaring it. This creates a strongly typed collection of Bananas in one simple line of code.

myBananas is strongly typed, so any code that uses myBananas also benefits from strong typing. So, if you Add, Insert, or Delete an object, your VB.NET or C# (or other) compiler can do compile-time type checking to ensure that you aren't mixing Bananas with Oranges or any other type that's not a Banana. This makes your code more robust, because the source of possible errors is caught when you compile, rather than left to be a potential runtime error. Languages such as VB.NET that have a background compiler do the type checking at design time "as you type," providing instant feedback that helps you keep your code on track.

Continuing the previous example, myBananas has a .Item property that sets or gets a Banana at a given index. The Item property is As Object in the current .NET version, so you often use casting code, such as this VB.NET code:

aBanana = CType(myBananas.Item(i), _

You'd perform the same cast in C# with this code:

aBanana = (Banana) 
   myBananas.Item(i) ;

Cast No More
Generics made the preceding code a thing of the past. You use this VB.NET code instead:

aBanana = myBananas.Item(i)

This is the C# version:

aBanana = myBananas.Item(i) ;

You end up writing less code and more-robust code, because you no longer need to do the runtime casts.

The .NET Framework will include generic interfaces as well as classes to facilitate the functionality I've described. Two important interfaces it uses for the List<T> class are IList<T> and IEnumerable<T>. IList<T> provides the benefits I showed you previously, such as Item being a Banana instead of an Object. IEnumerable<T> provides type-safe enumeration.

If you iterate any kind of collection today, the IEnumerable interface returns an IEnumerator that you use to move to the next item and return the current item. The problem with this implementation is that the IEnumerator contract defines the Current item as As Object. This means that any For Each code must perform runtime casts, creating a potential source of errors inside a loop, of all places.

Now that generics include IEnumerator<T>, your For Each code can be type-safe and compiler-checked "as you type." Compilers such as the VB.NET compiler look for IEnumerable<T> and use it in preference to the older IEnumerable interface when both are present. If IEnumerable<T> isn't present, they'll fall back on the IEnumerable interface.

If you use VB.NET, the difference between IEnumerator<T> and IEnumerator might not be immediately apparent unless you examine the generated Microsoft Intermediate Language (MSIL). The reason is that VB.NET generates the casting code for you in the case of IEnumerator. Suppose you write this VB.NET code today:

For Each objBanana In myBananas

The preceding code actually does a runtime cast, such as this:

objBanana = _
   CType(ImyBananas.GetEnumerator. _
   Current, Banana)

The fact that the preceding code is a runtime cast is problematic. This code would also compile, which is obviously bad, because you don't have oranges in your bananas:

For Each objOrange As Orange In _

Your code would look identical with generics, but if you use a generic List or any other collection that implements IEnumerable<T>, the compiler can warn you that Orange isn't a valid type for myBananas. This is a significant improvement over the interfaces available today. Even if you write a strongly typed collection for each type you want to deal with, the IEnumerator contract means that you can't have compile-time type safety. Only generics offer this capability. Application code that uses generics can result in a slight performance gain, because you don't need to do many of the casting operations. The performance gain can be significant when you work with value types, because you no longer need to box and unbox the value types.

Create Your Own Generics
As I've shown you, generics simplify the amount of code you have to write and provide greater design-time feedback when you consume the generic classes that come in the .NET Framework. You'll also probably want to create your own generic classes or methods at some point. For example, you might want to create your own business class collection that can be strongly typed for different types of business classes. Generics provide the means to achieve strong typing and code reuse. When you write your template classes, you should also consider implementing nongeneric equivalent interfaces (see the sidebar, "Generics vs. Polymorphism").

I'll abbreviate the example business object collection class template name to BOCollection. You use this code to create a strongly typed collection:

Public Class BOCollection(Of T)

T is the type the business object collection will contain. You don't need to use T as the variable name, but this is the common convention. You can also have more than one type parameter. For example, you might have a class that does relations for two different types of business objects, be it a parent-child relation such as Customer and Invoices or whatever your business rules dictate. Your class definition might be this:

Public Class Relation(Of Parent, _
End Class

Then, you can use the type parameters Parent and Child inside your class for return types, local variables, method parameters, and so on. Anywhere you define a type in your class, you can replace the type with the generic's type parameters. For example, you could add a GetChildren function to your relation class:

Public Class Relation(Of Parent, _
   Public Function GetChildren( _
   p As Parent) As Child()
      ' code to get children here
   End Function
End Class

You can also use other generics inside your generic, including ones that inherit from generic types or implement interfaces. Using the BOCollection example, you typically implement IList<T>:

Public Class BOCollection(Of T)
   Implements System.Collections. _
      Generic.IList(Of T)

   Default Public Property _
      Item(ByVal index As Int32) 
      As T _
         Implements System. _
         Collections.IList( _
         Of T).Item
   End Get
   Set(ByVal value As T)
   End Set
   End Property

   ' add the rest of the 
   ' interface implementation 
   ' members
End Class

So far, I've shown how you can use the type parameter in definitions. However, if you were to try to write any meaningful code with variables declared as the type parameter, you'd find that only System.Object's methods and properties are available to you. The reason is that the type parameter can be System.Object or anything derived from it, so the rules of lowest common denominator apply.

If the BOCollection<T> class will be dealing with types derived from a business object base class, you can define a constraint on the type parameter. The constraint means the type the user specifies must be of this constraint type, by either inheritance or interface implementation. The BOCollection looks like this in C#:

public class BOCollection<T>: System.Collections.Generic.IList<T>
   where T: BusinessObject

You can use the As qualifier in VB.NET to specify the constraint on the type parameter, instead of using a where statement:

Public Class BOCollection(Of T As _
   Implements System.Collections. _
      Generic.IList(Of T)

Constraints can also be multiple constraints. For example, you can indicate that a type must be IComparable as well as a Business Object. Once you've defined the constraint, you can then work with any variables of the type parameter type, using all the members as defined in the constraint types. This code will fail if the constraint is in place and Banana is not derived from Business Object:

Dim obj as BOCollection(Of Banana)

You've seen that constraints provide two distinct benefits: They let you work with further derived objects in your template, and they specify constraints that consumers of your template must meet.

Generics provide great functionality, flexibility, and—most important—a new level of strong typing that makes code faster and easier to write, faster to run, and more robust. Generics will make your development experience far more enjoyable if you use them wisely.


comments powered by Disqus

Subscribe on YouTube