Chapter 8: Buying Generic
In This Chapter
Making your code generic — and truly powerful
Writing your own generic class
Writing generic methods
Using generic interfaces and delegates
The problem with collections is that you need to know exactly what is going in them. Can you imagine a recipe that accepts only the exact listed ingredients and no others? No substitutions — nothing even named differently? That’s how most collections treat you, but not generics.
As with prescriptions at your local pharmacy, you can save big by opting for the generic version. Generics, introduced in C# 2.0, are fill-in-the-blanks classes, methods, interfaces, and delegates. For example, the List<T>
class defines a generic array-like list that’s quite comparable to the older, nongeneric ArrayList
— but better! When you pull List<T>
off the shelf to instantiate your own list of, say, int
s, you replace T
with int
:
List<int> myList = new List<int>(); // A list limited to ints
The versatile part is that you can instantiate List<T>
for any single data type (string
, Student
, BankAccount
, CorduroyPants
— whatever), and it’s still type-safe like an array, without nongeneric costs. It’s the superarray. (I explain type-safety and the costs of nongeneric collections next.)
Generics come in two flavors in C#: the built-in generics, such as List<T>
, and a variety of roll-your-own items. After a quick tour of generic concepts, this chapter covers roll-your-own generic classes, generic methods, and generic interfaces and delegates.
Writing a New Prescription: Generics
What’s so hot about generics? They excel for two reasons: safety and performance.
Generics are type-safe
A compiler error beats the heck out of a runtime error. In fact, a compiler error beats everything except a royal flush or a raspberry sundae. Compiler errors are useful because they help you spot problems now.
The first consequence of nongenerics lacking type-safety is that you need a cast, as shown in the following code, to get the original object out of the ArrayList
because it’s hidden inside an egg, er, Object
:
ArrayList aList = new ArrayList();
// Add five or six items, then ...
string myString =
(string)aList[4]
; // Cast to string.
ArrayList aList = new ArrayList();
aList.Add(“a string”); // string -- OK
aList.Add(3); // int -- OK
aList.Add(aStudent); // Student -- OK
However, if you put a mixture of incompatible types into an ArrayList
(or another nongeneric collection), how do you know what type is in, say, aList[3]
? If it’s a Student
and you try to cast it to string
, you get a runtime error. It’s just like Harry Potter reaching into a box of Bertie Botts’s Every Flavor Beans: He doesn’t know whether he’ll grab raspberry beans or earwax.
// See if the object is the right type, then cast it ...
if(aList[i]
is
Student) // Is the object there a Student?
{
Student aStudent = (Student)aList[i]; // Yes, so it’s safe to cast.
}
// Or do the conversion and see if it went well...
Student aStudent = aList[i]
as
Student; // Extract a Student, if present;
if(aStudent != null) // if not, “as” returns null.
{
// OK to use aStudent; “as” operator worked.
}
You can avoid all this extra work by using generics. Generic collections work like arrays: You specify the one and only type they can hold when you declare them.
Generics are efficient
Polymorphism allows the type Object
to hold any other type — as with the egg carton analogy in the previous section. But you can incur a penalty by putting in value-type objects — numeric, char
, and bool
types and struct
s — and taking them out. That’s because value-type objects that you add have to be boxed. (See Book II for more on polymorphism.)
Boxing isn’t worrisome unless your collection is big (although the amount of boxing going on can startle you and be more costly than you imagined). If you’re stuffing a thousand, or a million, int
s into a nongeneric collection, it takes about 20 times as long, plus extra space on the memory heap, where reference-type objects are stored. Boxing can also lead to subtle errors that will have you tearing your hair out. Generic collections eliminate boxing and unboxing.
Classy Generics: Writing Your Own
Besides the built-in generic collection classes, C# lets you write your own generic classes, whether they’re collections or not. The point is that you can create generic versions of classes that you design.
Picture a class definition full of <T>
notations. When you instantiate such a class, you specify a type to replace its generic placeholders, just as you do with the generic collections. Note how similar these declarations are:
LinkedList<int> aList = new LinkedList<int>(); // Built-in LinkedList class
MyClass<int> aClass = new MyClass<int>(); // Custom class
Both are instantiations of classes — one built-in and one programmer-defined. Not every class makes sense as a generic; in the section “Writing generic code the easy way,” later in this chapter, I show you an example of one that does.
To show you how to write your own generic class, the following example develops a special kind of queue collection class, a priority queue.
Shipping packages at OOPs
Here’s the scene for an example: a busy shipping warehouse similar to UPS or FedEx. Packages stream in the front door at OOPs, Inc., and are shipped out the back as soon as they can be processed. Some packages need to be delivered by way of superfast next-day teleportation; others can travel a tiny bit slower, by second-day cargo pigeon; and most can take the snail route: ground delivery in your cousin Fred’s ’82 Volvo.
But the packages don’t arrive at the warehouse in any particular order, so as they come in, you need to expedite some of them as next-day or second-day. Because some packages are more equal than others, they are prioritized, and the folks in the warehouse give the high-priority packages special treatment.
Except for the priority aspect, this situation is tailor-made for a queue data structure. A queue is perfect for anything that involves turn-taking. You’ve stood (or driven) in thousands of queues in your life, waiting for your turn to buy Twinkies or pay too much for prescription medicines. You know the drill.
The shipping warehouse scenario is similar: New packages arrive and go to the back of the line — normally. But because some have higher priorities, they’re privileged characters, like those Premium Class folks at the airport ticket counter. They get to jump ahead, either to the front of the line or not far from the front.
Queuing at OOPs: PriorityQueue
The shipping queue at OOPs deals with high-, medium-, and low-priority packages coming in. Here are the queuing rules:
High-priority packages (next-day) go to the front of the queue — but behind any other high-priority packages that are already there.
Medium-priority packages (second-day) go as far forward as possible — but behind all the high-priority packages, even the ones that a laggard will drop off later, and also behind other medium-priority packages that are already in the queue.
Low-priority ground-pounders must join at the back of the queue. They get to watch all the high priorities sail by to cut in front of them — sometimes, way in front of them.
C# comes with built-in queues, even generic ones. But it doesn’t come with a priority queue, so you have to build your own. How? A common approach is to embed several actual queues within a wrapper class, sort of like this:
class Wrapper // Or PriorityQueue
{
Queue queueHigh = new Queue ();
Queue queueMedium = new Queue ();
Queue queueLow = new Queue ();
// Methods to manipulate the underlying queues...
Wrappers are classes (or methods) that encapsulate complexity. A wrapper may have an interface quite different from the interfaces of what’s inside it — that’s an adapter.
The wrapper encapsulates three actual queues here (they could be generic), and the wrapper must manage what goes into which underlying queue and how. The standard interface to the Queue
class, as implemented in C#, includes these two key methods:
Enqueue()
(pronounced “N-Q”) inserts items into a queue at the back.
Dequeue()
(pronounced “D-Q”) removes items from the queue at the front.
Here’s the code for the PriorityQueue
example on this book’s website:
// PriorityQueue -- Demonstrates using lower-level queue collection objects
// (generic ones at that) to implement a higher-level generic
// Queue that stores objects in priority order
using System;
using System.Collections.Generic;
namespace PriorityQueue
{
class Program
{
// Main -- Fill the priority queue with packages, then
// remove a random number of them.
static void Main(string[] args)
{
Console.WriteLine(“Create a priority queue:”);
PriorityQueue<Package> pq = new PriorityQueue<Package>();
Console.WriteLine(
“Add a random number (0 - 20) of random packages to queue:”);
Package pack;
PackageFactory fact = new PackageFactory();
// You want a random number less than 20.
Random rand = new Random();
int numToCreate = rand.Next(20); // Random int from 0 - 20
Console.WriteLine(“ Creating {0} packages: “, numToCreate);
for (int i = 0; i < numToCreate; i++)
{
Console.Write(“ Generating and adding random package {0}”, i);
pack = fact.CreatePackage();
Console.WriteLine(“ with priority {0}”, pack.Priority);
pq.Enqueue(pack);
}
Console.WriteLine(“See what we got:”);
int total = pq.Count;
Console.WriteLine(“Packages received: {0}”, total);
Console.WriteLine(“Remove a random number of packages (0-20): “);
int numToRemove = rand.Next(20);
Console.WriteLine(“ Removing up to {0} packages”, numToRemove);
for (int i = 0; i < numToRemove; i++)
{
pack = pq.Dequeue();
if (pack != null)
{
Console.WriteLine(“ Shipped package with priority {0}”,
pack.Priority);
}
}
// See how many were “shipped.”
Console.WriteLine(“Shipped {0} packages”, total - pq.Count);
// Wait for user to acknowledge the results.
Console.WriteLine(“Press Enter to terminate...”);
Console.Read();
}
}
// Priority enumeration -- Defines a set of priorities
// instead of priorities like 1, 2, 3, ... these have names.
// For information on enumerations,
// see the article “Enumerating the Charms of the Enum”
// on csharp102.info.
enum Priority
{
Low, Medium, High
}
// IPrioritizable interface -- Defines ability to prioritize.
// Define a custom interface: Classes that can be added to
// PriorityQueue must implement this interface.
interface IPrioritizable
{
Priority Priority { get; } // Example of a property in an interface
}
//PriorityQueue -- A generic priority queue class
// Types to be added to the queue *must* implement IPrioritizable interface.
class PriorityQueue<T> where T : IPrioritizable
{
//Queues -- the three underlying queues: all generic!
private Queue<T> _queueHigh = new Queue<T>();
private Queue<T> _queueMedium = new Queue<T>();
private Queue<T> _queueLow = new Queue<T>();
//Enqueue -- Prioritize T and add it to correct queue; an item of type T.
// The item must know its own priority.
public void Enqueue(T item)
{
switch (item.Priority) // Require IPrioritizable to ensure this property.
{
case Priority.High:
_queueHigh.Enqueue(item);
break;
case Priority.Medium:
_queueMedium.Enqueue(item);
break;
case Priority.Low:
_queueLow.Enqueue(item);
break;
default:
throw new ArgumentOutOfRangeException(
item.Priority.ToString(),
“bad priority in PriorityQueue.Enqueue”);
}
}
//Dequeue -- Get T from highest-priority queue available.
public T Dequeue()
{
// Find highest-priority queue with items.
Queue<T> queueTop = TopQueue();
// If a non-empty queue is found.
if (queueTop != null & queueTop.Count > 0)
{
return queueTop.Dequeue(); // Return its front item.
}
// If all queues empty, return null (you could throw exception).
return default(T); // What’s this? See discussion.
}
//TopQueue -- What’s the highest-priority underlying queue with items?
private Queue<T> TopQueue()
{
if (_queueHigh.Count > 0) // Anything in high-priority queue?
return _queueHigh;
if (_queueMedium.Count > 0) // Anything in medium-priority queue?
return _queueMedium;
if (_queueLow.Count > 0) // Anything in low-priority queue?
return _queueLow;
return _queueLow; // All empty, so return an empty queue.
}
//IsEmpty -- Check whether there’s anything to deqeue.
public bool IsEmpty()
{
// True if all queues are empty
return (_queueHigh.Count == 0) & (_queueMedium.Count == 0) &
(_queueLow.Count == 0);
}
//Count -- How many items are in all queues combined?
public int Count // Implement this one as a read-only property.
{
get { return _queueHigh.Count + _queueMedium.Count + _queueLow.Count; }
}
}
//Package -- An example of a prioritizable class that can be stored in
// the priority queue; any class that implements
// IPrioritizable would look something like Package.
class Package : IPrioritizable
{
private Priority _priority;
//Constructor
public Package(Priority priority)
{
this._priority = priority;
}
//Priority -- Return package priority -- read-only.
public Priority Priority
{
get { return _priority; }
}
// Plus ToAddress, FromAddress, Insurance, etc.
}
//PackageFactory -- You need a class that knows how to create a new
// package of any desired type on demand; such a class
// is a factory class.
class PackageFactory
{
//A random-number generator
Random _randGen = new Random();
//CreatePackage -- The factory method selects a random priority,
// then creates a package with that priority.
// Could implement this as iterator block.
public Package CreatePackage()
{
// Return a randomly selected package priority.
// Need a 0, 1, or 2 (values less than 3).
int rand = _randGen.Next(3);
// Use that to generate a new package.
// Casting int to enum is clunky, but it saves
// having to use ifs or a switch statement.
return new Package((Priority)rand);
}
}
}
PriorityQueue
is a bit long, so you need to look at each part carefully. After a look at the target class, Package
, you can follow a package’s journey through the Main()
method near the top.
Unwrapping the package
Class Package
, which is intentionally simple for this example (see the listing in the previous section), focuses on the priority part, although a real Package
object would include other members. All that Package
needs for the example are
A private data member to store its priority
A constructor to create a package with a specific priority
A method (implemented as a read-only property here) to return the priority
Two aspects of class Package
require some explanation: the Priority
type and the IPrioritizable
interface that Package
implements.
Specifying the possible priorities
Priorities are measured with an enumerated type, or enum
, named Priority
. The Priority
enum
looks like this:
//Priority -- Instead of priorities like 1, 2, 3, they have names.
enum Priority // See the article “Enumerating the Charms
// of the Enum” on www.csharp102.info.
{
Low, Medium, High
}
Implementing the IPrioritizable interface
Any object going into the PriorityQueue
must “know” its own priority. (A general object-oriented principle states that objects should be responsible for themselves.)
One way to enforce this requirement is to insist that all shippable objects implement the IPrioritizable
interface, which follows:
//IPrioritizable -- Define a custom interface: Classes that can be added to
// PriorityQueue must implement this interface.
interface IPrioritizable // Any class can implement this interface.
{
Priority Priority { get; }
}
Class Package
implements the interface by providing a fleshed-out implementation for the Priority
property:
public Priority Priority
{
get { return _priority; }
}
You encounter the other side of this enforceable requirement in the declaration of class PriorityQueue
, in the later section “Saving PriorityQueue for last.”
Touring Main()
Before you spelunk the PriorityQueue
class itself, it’s useful to get an overview of how it works in practice at OOPs, Inc. Here’s the Main()
method for the PriorityQueue
example:
static void Main(string[] args)
{
Console.WriteLine(“Create a priority queue:”);
PriorityQueue<Package> pq = new PriorityQueue<Package>();
Console.WriteLine(
“Add a random number (0 - 20) of random packages to queue:”);
Package pack;
PackageFactory fact = new PackageFactory();
// You want a random number less than 20.
Random rand = new Random();
int numToCreate = rand.Next(20); // Random int from 0-20.
Console.WriteLine(“ Creating {0} packages: “, numToCreate);
for (int i = 0; i < numToCreate; i++)
{
Console.Write(“ Generating and adding random package {0}”, i);
pack = fact.CreatePackage();
Console.WriteLine(“ with priority {0}”, pack.Priority);
pq.Enqueue(pack);
}
Console.WriteLine(“See what we got:”);
int total = pq.Count;
Console.WriteLine(“Packages received: {0}”, total);
Console.WriteLine(“Remove a random number of packages (0-20): “);
int numToRemove = rand.Next(20);
Console.WriteLine(“ Removing up to {0} packages”, numToRemove);
for (int i = 0; i < numToRemove; i++)
{
pack = pq.Dequeue();
if (pack != null)
{
Console.WriteLine(“ Shipped package with priority {0}”,
pack.Priority);
}
}
// See how many were “shipped.”
Console.WriteLine(“Shipped {0} packages”, total - pq.Count);
// Wait for user to acknowledge the results.
Console.WriteLine(“Press Enter to terminate...”);
Console.Read();
}
Here’s what happens in Main()
:
1. Instantiate a PriorityQueue
object for type Package
.
2. Create a PackageFactory
object whose job is to create new packages with randomly selected priorities, on demand.
A factory is a class or method that creates objects for you. You tour PackageFactory
in the section “Using a (nongeneric) Simple Factory class,” later in this chapter.
3. Use the .NET library class Random
to generate a random number and then call PackageFactory
to create that number of new Package
objects with random priorities.
4. Add each package to the PriorityQueue
by using pq.Enqueue(pack)
.
5. Write the number of packages created and then randomly remove some of them from the PriorityQueue
by using pq.Dequeue()
.
6. End after displaying the number of packages removed.
Writing generic code the easy way
Now you have to figure out how to go about writing a generic class, with all those <T>
s. Looks confusing, doesn’t it? Well, it’s not so hard, as this section demonstrates.
Here’s a small piece of a nongeneric PriorityQueue
, to illustrate:
public class PriorityQueue
{
//Queues -- The three underlying queues: all generic!
private Queue<Package> _queueHigh = new Queue<Package>();
private Queue<Package> _queueMedium = new Queue<Package>();
private Queue<Package> _queueLow = new Queue<Package>();
//Enqueue -- Prioritize a Package and add it to correct queue.
public void Enqueue(Package item)
{
switch(item.Priority) // Package has this property.
{
case Priority.High:
queueHigh.Enqueue(item);
break;
case Priority.Low:
queueLow.Enqueue(item);
break;
case Priority.Medium:
queueMedium.Enqueue(item);
break;
}
}
// And so on ...
Testing the logic of the class is easier when you write the class nongenerically first. When all the logic is straight, you can use find-and-replace to replace the name Package
with T
. (I explain a little later that there’s a bit more to it than that, but not much.)
Saving PriorityQueue for last
Why would a priority queue be last? Seems a little backward to me. But you’ve seen the rest. Now it’s time to examine the PriorityQueue
class itself. This section shows the code and then walks you through it and shows you how to deal with a couple of small issues. Take it a piece at a time.
The underlying queues
PriorityQueue
is a wrapper class that hides three ordinary Queue<T>
objects, one for each priority level. Here’s the first part of PriorityQueue
, showing the three underlying queues (now generic):
//PriorityQueue -- A generic priority queue class
// Types to be added to the queue *must* implement IPrioritizable interface.
class PriorityQueue
<T>
where T : IPrioritizable
{
// Queues -- the three underlying queues: all generic!
private Queue<T> _queueHigh = new Queue<T>();
private Queue<T> _queueMedium = new Queue<T>();
private Queue<T> _queueLow = new Queue<T>();
// The rest will follow shortly ...
These lines declare three private data members of type Queue<T>
and initialize them by creating the Queue<T>
objects. I say more in the section “Tending to unfinished business,” later in this chapter, about that odd-looking class declaration line above the “subqueue” declarations.
The Enqueue() method
Enqueue()
adds an item of type T
to the PriorityQueue
. This method’s job is to look at the item’s priority and put it into the correct underlying queue. In the first line, it gets the item’s Priority
property and switches based on that value. To add the item to the high-priority queue, for example, Enqueue()
turns around and enqueues the item in the underlying queueHigh
. Here’s PriorityQueue
’s Enqueue()
method:
//Enqueue -- Prioritize T and add it to correct queue; an item of type T.
// The item must know its own priority.
public void Enqueue(
T item
)
{
switch (item.Priority) // Require IPrioritizable to ensure this property.
{
case Priority.High:
_queueHigh.Enqueue(item);
break;
case Priority.Medium:
_queueMedium.Enqueue(item);
break;
case Priority.Low:
_queueLow.Enqueue(item);
break;
default:
throw new ArgumentOutOfRangeException(
item.Priority.ToString(),
“bad priority in PriorityQueue.Enqueue”);
}
}
The Dequeue() method
Dequeue()
’s job is a bit trickier than Enqueue()
’s: It must locate the highest-priority underlying queue that has contents and then retrieve the front item from that subqueue. Dequeue()
delegates the first part of the task, finding the highest-priority queue that isn’t empty, to a private TopQueue()
method (described in the next section). Then Dequeue()
calls the underlying queue’s Dequeue()
method to retrieve the frontmost object, which it returns. Here’s how Dequeue()
works:
//Dequeue -- Get T from highest-priority queue available.
public
T
Dequeue()
{
// Find highest-priority queue with items.
Queue<T> queueTop = TopQueue();
// If a non-empty queue is found
if (queueTop != null & queueTop.Count > 0)
{
return queueTop.Dequeue(); // Return its front item.
}
// If all queues empty, return null (you could throw exception).
return default(T); // What’s this? See discussion.
}
A difficulty arises only if none of the underlying queues have any packages — in other words, the whole PriorityQueue
is empty. What do you return in that case? Dequeue()
returns null
. The client — the code that calls PriorityQueue.Dequeue()
— should check the Dequeue()
return value in case it’s null
. Where’s the null
it returns? It’s that odd duck, default(T)
, at the end. I deal with default(T)
a little later in this chapter in the section “Determining the null value for type T: default(T).”
The TopQueue() utility method
Dequeue()
relies on the private method TopQueue()
to find the highest-priority, nonempty underlying queue. TopQueue()
just starts with queueHigh
and asks for its Count
property. If it’s greater than zero, the queue contains items, so TopQueue()
returns a reference to the whole underlying queue that it found. (The TopQueue()
return type is Queue<T>
.) On the other hand, if queueHigh
is empty, TopQueue()
tries queueMedium
and then queueLow
.
What happens if all subqueues are empty? TopQueue()
could return null
, but it’s more useful to simply return one of the empty queues. When Dequeue()
then calls the returned queue’s Dequeue()
method, it returns null
. TopQueue()
works like this:
//TopQueue -- What’s the highest-priority underlying queue with items?
private
Queue<T>
TopQueue()
{
if (_queueHigh.Count > 0) // Anything in high-priority queue?
return _queueHigh;
if (_queueMedium.Count > 0) // Anything in medium-priority queue?
return _queueMedium;
if (_queueLow.Count > 0) // Anything in low-priority queue?
return _queueLow;
return _queueLow; // All empty, so return an empty queue.
}
The remaining PriorityQueue members
PriorityQueue
is useful when it knows whether it’s empty and, if not, how many items it contains. (An object should be responsible for itself.) Look at PriorityQueue
’s IsEmpty()
method and Count
property in the earlier listing. You might also find it useful to include methods that return the number of items in each of the underlying queues. Be careful: Doing so may reveal too much about how the priority queue is implemented. Keep your implementation private.
Using a (nongeneric) Simple Factory class
In the section “Saving PriorityQueue for last,” earlier in this chapter, I use a Simple Factory object (although I just call it a “Factory” there) to generate an endless stream of Package
objects with randomized priority levels. At long last, that simple class can be revealed:
// PackageFactory -- You need a class that knows how to create a new
// package of any desired type on demand; such a
// class is a factory class.
class PackageFactory
{
Random _randGen = new Random(); // C#’s random-number generator
//CreatePackage -- This factory method selects a random priority,
// then creates a package with that priority.
public Package CreatePackage()
{
// Return a randomly selected package priority:
// need a 0, 1, or 2 (values less than 3).
int rand = _randGen.Next(3);
// Use that to generate a new package.
// Casting int to enum is clunky, but it saves
// having to use ifs or a switch statement.
return new Package(
(Priority)rand
);
}
}
Class PackageFactory
has one data member and one method. (You can just as easily implement a simple factory as a method rather than as a class — for example, a method in class Program
.) When you instantiate a PackageFactory
object, it creates an object of class Random
and stores it in the data member rand
. Random
is a .NET library class that generates random numbers.
Using PackageFactory
To generate a randomly prioritized Package
object, you call your factory object’s CreatePackage()
method this way:
PackageFactory fact = new PackageFactory();
IPrioritizable pack = fact.CreatePackage(); // Note the interface here.
CreatePackage()
tells its random-number generator to generate a number from 0 to 2 (inclusive) and uses the number to set the priority of a new Package
, which the method returns (to a Package
or, better, to an IPrioritizable
variable).
More about factories
Programmers have long known that they should avoid tight coupling. (One of the more decoupled approaches is to use the factory indirectly via an interface, such as IPrioritizable
, rather than a concrete class, such as Package
.) Programmers still create objects directly all the time, using the new
operator, and that’s fine. But factories can make code less coupled — and therefore more flexible.
Tending to unfinished business
By itself, PriorityQueue
wouldn’t prevent you from trying to instantiate it for, say, int
or string
or Student
— elements that don’t have priorities. You need to constrain the class so that it can be instantiated only for types that implement IPrioritizable
. Attempting to instantiate for a non-IPrioritizable
class should result in a compiler error.
The Dequeue()
method for PriorityQueue
returns the value null
instead of an actual object. But generic types such as <T>
don’t have a natural default null
value the way elements such as int
s, string
s, and down-and-out object references do. That part of it needs to be genericized, too.
Adding constraints
PriorityQueue
must be able to ask an object what its priority is. To make it work, all classes that are storable in PriorityQueue
must implement the IPrioritizable
interface, as Package
does. Package
lists IPrioritizable
in its class declaration heading, like this:
class Package : IPrioritizable
Then it implements IPrioritizable
’s Priority
property.
class PriorityQueue<T>
where T: IPrioritizable
Did you notice the where
clause earlier? This boldfaced where
clause specifies that T
must implement IPrioritizable
. That’s the enforcer. It means, “Make sure that T
implements the IPrioritizable
interface — or else!”
The name of a required base class that T
must derive from (or be).
The name of an interface that T
must implement, as shown in the previous example.
You can see more — Table 8-1 has the complete list.
For information about these constraints, look up Generics [C#], constraints in the Help index.
Note the struct
and class
options in particular. Specifying struct
means that T
can be any value type: a numeric type, a char
, a bool
, or any object declared with the struct
keyword. Specifying class
means that T
can be any reference type: any class type.
These constraint options give you quite a bit of flexibility for making your new generic class behave just as you want. And a well-behaved class is a pearl beyond price.
You aren’t limited to just one constraint, either. Here’s an example of a hypothetical generic class declared with multiple constraints on T
:
class MyClass<T> :
where T: class, IPrioritizable, new()
{ ... }
In this line, T
must be a class, not a value type; it must implement IPrioritizable
; and it must contain a constructor without parameters. Strict!
class MyClass<T, U> :
where T: IPrioritizable, where U: new()
You see two where
clauses, separated by a comma. The first constrains T
to any object that implements the IPrioritizable
interface. The second constrains U
to any object that has a default (parameterless) constructor.
Determining the null value for type T: default(T)
In case you read the last paragraph in the previous section and are confused, well, each type has (as mentioned earlier) a default value that signifies “nothing” for that type. For int
s, doubles
, and other types of numbers, it’s 0 (or 0.0). For bool
, it’s false
. And, for all reference types, such as Package
, it’s null
. As with all reference types, the default for string
is null
.
But because a generic class such as PriorityQueue
can be instantiated for almost any data type, C# can’t predict the proper null
value to use in the generic class’s code. For example, if you use the Dequeue()
method of PriorityQueue
, you may face this situation: You call Dequeue()
to get a package, but none is available. What do you return to signify “nothing”? Because Package
is a class type, it should return null
. That signals the caller of Dequeue()
that there was nothing to return (and the caller must check for a null
return value).
return
default(T)
; // Return the right null for whatever T is.
This line tells the compiler to look at T
and return the right kind of null
value for that type. In the case of Package
, which as a class is a reference type, the right null
to return is, well, null
. But, for some other T
, it may be different and the compiler can figure out what to use.
Revising Generics
The generics model implemented in C# 2.0 was incomplete. Generics are fine for making the programmer’s life easier, but they did little in that version to make the analyst’s life easier. It used to be very hard to model an actual business model using Generics, and that changed in C# 4.0. Although parameters in C# 2.0 all allowed for variance in several directions, generics did not.
Variance has to do with types of parameters and return values. Covariance means that an instance of a subclass can be used when an instance of a parent class is expected, while Contravariance means that an instance of a superclass can be used when an instance of a subclass is expected. When neither is possible, it is called Invariance.
All fourth-generation languages support some kind of variance. In C# 3.0 and earlier versions, parameters are covariant, and return types are contravariant. So, this works because string and integer parameters are covariant to object parameters:
public static void MessageToYou(object theMessage)
{
if (theMessage != null)
Console.Writeline(theMessage)
}
//then:
MessageToYou(“It’s a message, yay!”);
MessageToYou(4+6.6);
And this works because object return types are contravariant to string and integer return types (for example):
object theMessage = MethodThatGetsTheMessage();
Generics are nonvariant in C# 2.0 and 3.0. This means that if Basket<apple>
is of type Basket<fruit>
, those Baskets
are not interchangeable like strings and objects are in the preceding example.
Variance
If you look at a method like the following one:
public static void WriteMessages()
{
List<string> someMessages = new List<string>();
someMessages.Add(“The first message”);
someMessages.Add(“The second message”);
MessagesToYou(someMessages);
}
and then you try to call that method like you did earlier in this chapter with a string type
//This doesn’t work in C#3!!
public static void MessagesToYou(IEnumerable<object> theMessages)
{
foreach (var item in theMessages)
Console.WriteLine(item);
}
it will fail. Generics are invariant in C# 3.0. But, in Visual Studio 2010 and later this complies because IEnumerable<T>
is covariant — you can use a more derived type as a substitute for a higher-order type. The next section shows a real example.
Contravariance
In my scheduling application, I have Events
, which have a date, and then a set of subclasses, one of which is Course
. A Course
is an Event
. Courses know their own number of students.
Anyway, back at the ranch, I have a method called MakeCalendar
:
public void MakeCalendar(IEnumerable<Event> theEvents)
{
foreach (Event item in theEvents)
{
Console.WriteLine(item.WhenItIs.ToString());
}
}
Pretend it makes a calendar; for now, all it does is print the date to the console. MakeCalendar
is systemwide, so it expects some enumerable list of events.
I also have a Sort algorithm at the main system, called EventSorter
. This is used to pass into the Sort
method of collections. It expects to be called from a list of Events. Here is the EventSorter
class:
class EventSorter : IComparer<Event>
{
public int Compare(Event x, Event y)
{
return x.WhenItIs.CompareTo(y.WhenItIs);
}
}
I am writing the Instructor-Led Training section of the event manager, and I need to make a list of courses, sort them, and then make a calendar. So I make my list of courses in ScheduleCourses
and then I call sort and pass in the EventSorter
:
public void ScheduleCourses()
{
List<Course> courses = new List<Course>()
{
new Course(){NumberOfStudents=20, WhenItIs = new DateTime(2009,2,1)},
new Course(){NumberOfStudents=14, WhenItIs = new DateTime(2009,3,1)},
new Course(){NumberOfStudents=24, WhenItIs = new DateTime(2009,4,1)},
};
//Now I am passing an ICompare<Event> class to my List<Course> collection.
//It should be an ICompare<Course> but I can use ICompare<Event> because of contravariance
courses.Sort(new EventSorter());
//I am passing a List of courses, where a List of Events was expected.
//We can do this because generic parameters are covariant
MakeCalendar(courses);
}
But wait, this is a list of courses I am calling Sort from, right, not a list of events. Doesn’t matter — IComparer<Event>
is a contravariant generic for T (its return type) as compared to IComparer<Course>
, so I can still use the algorithm.
Now I have to pass my list into the MakeSchedule
method, but that method expects an enumerable collection of Events
. Because parameters are covariant for generics now, I can pass in a List of Courses, as Course
is covariant to Event
. Make sense?
There is another example of contravariance, using parameters rather than return values. If I have a method that returns a generic list of Courses
, I can call that method expecting a list of Events
, because Event
is a superclass of Course
.
You know how you can have a method that returns a String
and assign the return value to a variable that you have declared an object? Now you can do that with a generic collection, too.
In general, the C# compiler makes assumptions about the generic type conversion. As long as you’re working up the chain for parameters or down the chain for return types, C# will just magically figure the type out.
Covariance
I now have to pass my list into the MakeSchedule
method, but that method expects an enumerable collection of Events
. Because parameters are covariant for generics now, I can pass in a List of Courses, as Course
is covariant to Event
. This is covariance for parameters.