Overview of Generics

Generics are a feature of C# 2.0 that allow you to defer the specification of an actual data type until your code creates an instance of the generic type. This section introduces you to some of the benefits of using generics, as well as illustrating how to use type parameters and how to implement constraints on a type parameter.

Benefits of Generics

Generics provide the developer with an extremely high-performance way of programming as well as increasing code reuse and allowing for extremely elegant solutions.

Before you see how generics make the world a better place, you need to see an example of the problem they address. Consider the following class:

public class Customer {     public void AddDetailItem( object detailItem )     {         if (detailItem is OrderItem)             PerformAction( (OrderItem)detailItem );         if (detailItem is CalendarEvent)             AddCalendarEvent( (CalendarEvent)detailItem);         ...     } } 

When writing complex code, especially in multitiered applications or when using code written by other developers or third-party companies, it is often necessary to deal with things in the abstract. We often use interfaces so that we can guarantee that parameters will conform to a specific contract, or we will use the object class to allow for a lot of runtime flexibility.

The problem is that this runtime flexibility comes at a price. In the preceding code, there is a lot of typecasting going on and some implicit Reflection operations (the is operator).

Using type parameters and generics, you could specify at the time of instantiation whether you want the Customer class to work with CalendarEvent detail items or OrderItem detail items, as shown in the following code:

public class Customer<T> {     public void AddDetailItem( T detailItem )     {       ...     } } 

While the preceding class is a Customer, it should be pointed out that the most common use of generics is to create and use specialized collection classes. You will see how to use some of the new Generic collection classes provided with the .NET Framework 2.0 later in this chapter.

Introduction to Type Parameters

As you can see in the preceding sample, the code is far simpler. The data type object has been replaced with the type parameter T. What this means is that C# will actually defer the definition of that type until runtime. When a generic class is instantiated, it is instantiated with a type parameter, binding the incomplete amorphous class implementation with a real type, creating a concrete generic class instance, as shown in the following example:

Customer<OrderItem> customer = new Customer<OrderItem>(); 

Type parameters are always specified using the new generics operator <>.

Although you can name your type parameters whatever you like, a convention exists that defines the letter "T" as the standard name for the first type parameter. If your class requires additional type parameters, convention dictates that you continue with the alphabet starting at the capital letter "U" and proceeding from there. If you manage to make it to "Z" with type parameters, you may have a fairly large design problem that generics simply can't fix.

Another convention that is far more friendly is to only use single letters when the purpose of the type parameter is extremely obvious, such as defining MyList<T>. For situations where the purpose of the type parameter isn't self-explanatory, Microsoft actually recommends that you use a descriptive name for the type parameter that is prefixed with a capital T, as shown in the following code:

public class MyGenerics<TDataAccessClass> 

To specify multiple type parameters in a class definition, simply separate them with commas:

public class MyGenerics<T, U, V> 

And to instantiate a class that requires multiple type parameters, you separate the types with commas as well:

MyGenerics<int, string, object> x = new MyGenerics<int, string, object>(); 

Constraining Type Parameters

When you have a type parameter specified in your class definition, there is actually very little you can do with it by default. For example, how do you create a new instance of that object? You might think that the following code should compile error-free:

public class Customer<T> {     public void AddDetailItem(string itemName)     {         T newItem = new T();         items.Add(newItem);     } } 

Unfortunately C# has no way of knowing whether the data type specified by the parameter T has a default constructor, so the compiler will not allow you to use that type in a new statement with no parameters.

To get around this, you can use constraints on type parameters. These constraints specify certain requirements of the type parameter that must be met. For example, you can specify that any type parameter passed to your Generic class implementation must implement a default (parameterless) constructor.

All constraints on Generic classes are indicated with the where keyword, and you can specify multiple constraints on the same parameter by separating the constraints with a comma.

A sample of specifying a constraint is shown in the following code:

public class Customer<TDetailItem> where TDetailItem : new() 

The preceding constraint indicates that the type parameter specified by TDetailItem must implement a default constructor. The following few lines show a few more complex ways of specifying constraints:

public class CustomList<T> where T: class, IListItem public class Customer<T,U> where T: new() where U: ICustomerData 

Table 6.1 shows all of the constraints that you can apply to generic type parameters.

Table 6.1. Generic Type Parameter Constraints



where T:struct

Indicates that T must be a value type (except Nullable).

where T:class

Indicates that T must be a reference type, including any class, delegate, or interface.

where T:new()

Indicates that T must implement a default constructor. The new() constraint must be specified last if it is used with multiple constraints on the same type.

where T: (base class)

Indicates that T must either be or derive from the base class indicated.

where T: (interface)

Indicates that T must either be or implement the interface indicated.

An extremely common design pattern in object-oriented programming is the factory pattern. In this pattern, code requests new instances of a given type from that type's factory rather than instantiating them directly. This gives the factory complete control over the instantiation process, as well as the ability to do things like cache preinstantiated types, perform transparent Remoting or Web Services calls to obtain support data, and much more.

To prevent the factory from having to do excessive typecasting, the pattern often calls for the development of a single factory for each class. So, if you have a Customer class, you will also have a CustomerFactory class. This model is also used in things like Container-Managed Persistence and Object-Relational Mapping.

To see how you can make quick use of generics and type parameter constraints, take a look at the following code, which creates a factory class that can serve up both the Customer type as well as the SpecialCustomer type:

public class CustomerFactory<TCustomer, U> where TCustomer : Customer, new() {    public TCustomer GetCustomer()    {      return new TCustomer();    } } 

The preceding sample constrains the TCustomer type by requiring the TCustomer parameter to be or derive from the Customer type as well as implement a default constructor. Because the TCustomer argument has been constrained with the new() constraint, the code can explicitly create a new instance of that type. Through the power of generics, this new instance is never anything but the exact type specified. There is no typecasting required in the preceding factory class, which is a huge boon to object-oriented developers.

Microsoft Visual C# 2005 Unleashed
Microsoft Visual C# 2005 Unleashed
ISBN: 0672327767
EAN: 2147483647
Year: 2004
Pages: 298

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net