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.
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.
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:
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:
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.
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>();
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:
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
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:
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.
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.
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.
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
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:
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:
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
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.
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.
Dictionary
ClassThe 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:
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.
List
ClassThe 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:
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.
Queue
ClassAs 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:
Stack
ClassIn 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:
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.
3.137.163.62