Chapter 6. Introduction to Generics

IN THIS CHAPTER

Generics are a feature new to C# 2.0 that adds a tremendous amount of power and flexibility to object-oriented programming using the language. Many definitions of generics start off with a comparison of template-based programming in C++, but I feel that does generics an injustice.

Generics allow class designers to defer the specification of types within a class definition until such time as that class is instantiated. This chapter not only shows you how to use generics, but gives you a good idea of when you should use generics and how they can create drastic performance improvements and make code reuse significantly easier.

This chapter is by no means the definitive and complete guide for generics-based programming in C# 2.0. However, it will provide you with the information you need to know in order to quickly start using generics in your everyday coding as well as to understand the use of generics throughout the rest of the framework such as ASP.NET, ADO.NET, and so on.

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:

Image

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 Orderltem detail items, as shown in the following code:

Image

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 Myl_ist<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:

Image

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 TDetailltem : new()

The preceding constraint indicates that the type parameter specified by TDetailltem 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, IListltem
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

Image

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:

Image

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.

Building Generic Types

A generic type is any C# type that accepts a type parameter, as described in the preceding section. The hardest part of building generic types isn’t the technical details behind the syntax implementation; it is in the design of the types themselves. Although generics are an incredibly powerful tool, the use of generics purely for the sake of using generics can cause its own unique set of problems. The next section of this chapter will cover the basics of working with generic classes, Interfaces, and methods.

Creating Generic Classes

You have seen several examples of how to create and consume generic classes. Generic classes are designed to encapsulate operations and provide models that are not specific to a single data type. The operations encapsulated by generic classes can either apply to all data types, or specific data types such as all derivatives of a certain base class or all classes that implement a specific interface. To recap the preceding section, you use the type parameter operator (<>) to indicate the use of a generic type parameter within your class definition. In places where there used to be a concrete type reference such as int, string, or object, you can then use the type parameter to allow your generic class to defer the specification of the actual type until the class is instantiated.

Creating Generic Methods

In addition to supplying type parameters for the class itself, you can also supply type parameters for individual methods that belong to a class. A generic method is just a method that takes a type parameter. The important thing to keep in mind is that the type parameter that is accepted by a method is unrelated to any type parameter accepted by the class in which the method is defined. This generic method can make use of the class type parameter, but it isn’t necessary.

Listing 6.1 provides an illustration of both writing and consuming generic methods.

Listing 6.1 Creating and Consuming Generic Methods

Image

The first class, MethodTest, defines a static generic method that takes a single type argument, TOperand, that must implement the IComparable interface. The IsGreater method returns whether op1 is greater than op2, where both op1 and op2 will be of the type indicated by TOperand when the generic method is closed (closing is the process by which an open generic definition is bound to a concrete type during the instantiation or method invocation process).

Next, the generic class MethodTest2 takes a single Parameter Type TItem that also must implement the IComparable interface. This class has a single instance method that invokes the static method IsGreater on the MethodTest class. The preceding code sample not only illustrates how to create and consume generic methods, but it also shows you how the class-level type parameters are completely unrelated to the method-level type parameters, though you can use a class-level type parameter within a generic method if you choose. The following few lines of code show how to use both the MethodTest and the MethodTest2 classes:

Image

One distinct advantage of generic methods is that the compiler can create shortcuts for you. For example, if you pass two integer arguments to the IsGreater method, you don’t need to explicitly tell the compiler that the method is being invoked with integers, as shown in the following code:

Image

Creating Generic Interfaces

A generic interface functions in much the same way as a generic class. The interface itself accepts one or more type parameters in the same fashion as the generic class. Instead of utilizing the type parameter in the class definition, the type parameter is then utilized in the member declaration statements that make up the interface.

A huge benefit of allowing generic type parameters to be present in interfaces is that the classes that implement those Interfaces will not need to perform any unnecessary boxing/unboxing or typecasting. You will learn about boxing and unboxing in Chapter 16, “Optimizing your .NET 2.0 Code.” For now, it should suffice to know that boxing and unboxing are operations done to switch between value and reference types, and the operation is costly. The code in Listing 6.2 illustrates a generic interface.

Listing 6.2 A Generic Interface

Image

What the preceding interface accomplishes is indicating that any class that implements IDataHandler<TRowType> will provide the standard Add, Delete, Update, and Select methods typically associated with a data access object regardless of the underlying type of the data being stored. This is an extremely useful interface to have as it allows a lot of flexibility when working with the data tier.

Using Generic Collections

So far you have seen the basics of how you can implement generics in your own code as you develop solutions in C# 2.0. One of the most common uses of generics is in the implementation and manipulation of strongly typed collections. This section of the chapter gives you samples of how to work with some of the most common generic collection types available in version 2.0 of the .NET Framework: the Dictionary<> class, the List<> class, the Queue<> class, and the Stack<> class.

Using the Dictionary Class

The Dictionary class is designed to store values keyed to a lookup value. Traditionally programmers have used this class to store values associated with strings such as names, keywords, or GUIDs, but you can use an object of any type as the key and an object of any type as the value.

Adding generics to the picture allows you to avoid the use of costly System.Object instances as both the key and value, and you can specify the type of the key and the type of the value at the time of instantiation using type parameters, as shown in the following sample snippet:

Image

The preceding sample creates and manipulates a Dictionary<> class where all of the keys are strings, and all of the values are numbers, storing the number of days in each month.

Using the List Class

The List<> class is fairly self-explanatory. Its sole purpose is the storage of a list of items. Without generics, Lists were responsible for storing a list of System.Object types, and whenever the developer needed a true data type out of the list, it would involve typecasting operations and potentially convoluted code. As you can see in the following example, creating and using arbitrary lists of strongly typed data has never been easier:

Image

The preceding code produces the following output:

There are 1 customers, the first one is Kevin Hoffman

Something to pay attention to is the fact that you can now write code like the following without having to write any of your own custom classes:

custList[0].FirstName

To get a list to work as shown in the preceding line of code, developers used to have to spend countless hours creating their own strongly typed collection classes. Now you can create strongly typed collections at runtime with no performance loss and no typecasting required in a way that makes your code easier to read and reuse.

Using the Queue Class

As you saw in Chapter 4, “Arrays and Collections,” the Queue class is designed as a FIFO (First-In First-Out) collection. When you enqueue an item, it remains at the front of the Queue. As you add more items to the Queue, they stack up at the end the same as people would while waiting in line to get into a movie or to reach that elusive bank teller. Each time you dequeue an item from the Queue, you remove an item from the beginning of the collection and can then process it. The generic Queue<> class allows you to specify the data type of the items in the queue at the time of instantiation, as shown in the following sample:

Image

Using the Stack Class

In contrast to the Queue<> class, the Stack<> class is a LIFO (Last-in First-out) collection. If you think of a pile of items sitting on the floor representing a stack, you know that in order to get to the item on the bottom of the stack, you have to remove all of the items on top first. The same goes for the Stack<> class; when you push an item onto a stack, it goes on top, and when you pop an item off the stack, you take the topmost item. You can also peek at the item currently sitting on top of the stack without removing it. The following code illustrates pushing items onto a generic stack and popping them off. To be sure you know how stacks work, you can play with the order in which items are pushed to see the results:

Image

Summary

This chapter has given you a glimpse at the incredible power that a developer can wield using generics. Generics are a tool that allow developers to maximize performance and code reuse as well as create a robust object-oriented programming environment by not having to resort to repetitive typecasting and the overuse of parameters of type System.Object. Using generics, developers can defer the specification of data types until such time as their class is instantiated, but still reap the benefits of strong data types. After reading this chapter, you should be able to continue learning all that C# 2.0 has to offer and feel comfortable when you see generics being used by the .NET Framework and in your own sample code.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.146.176.68