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, ints, 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

Note

When you declare an array, you must specify the exact type of data it can hold. If you specify int, the array can't hold anything other than ints or other numeric types that C# can convert implicitly to int. You see compiler errors at build-time if you try to put the wrong kind of data into an array. Thus the compiler enforces type-safety, enabling you to fix a problem before it ever gets out the door.

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.

Note

The old-fashioned nongeneric collections aren't type-safe. In C#, everything IS_A Object because Object is the base type for all other types, both value-types and reference-types. But when you store value-types (numbers, chars, bools, and structs) in a collection, they must be boxed going in and unboxed coming back out. It's as though you're putting items in an egg carton and having to stuff them inside the eggs so that they fit, and then breaking the eggshells to get the items back out. (Reference-types such as string, Student, and BankAccount don't undergo boxing.)

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.

Warning

Fine, but the second consequence is this: You can put eggs in the carton, sure. But you can also add marbles, rocks, diamonds, fudge — you name it. An ArrayList can hold many different types of objects at the same time. So it's legal to write this:

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.

Note

To be safe, you have to resort to using the is operator (discussed in Book II ) or the alternative, the as operator:

// 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 structs — 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, ints 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.

Note

Don't get me wrong: Boxing allows C# to have a unified type system, which has great benefits that usually outweigh the inconvenience and cost of boxing.

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; later in this chapter, I show you an example of one that does.

Note

Classes that logically could do the same things for different types of data make the best generic classes. Collections of one sort or another are the prime example. If you find yourself mumbling, "I'll probably have to write a version of this for Student objects, too," it's probably a good candidate for generics.

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.

Note

For the shipping-priority queue, the wrapper provides the same interface as a normal queue, thus pretending to be a normal queue itself. It implements an Enqueue() method that determines an incoming package's priority and decides which underlying queue it gets to join. The wrapper's Dequeue() method finds the highest-priority Package in any of the underlying queues. The formal name of this wrapper class is PriorityQueue.

Here's the code for the PriorityQueue example on this book's Web site:

Note

// 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.

Tip

When you run PriorityQueue, run it several times. Because it's built around random numbers, you get varying results on each run. Sometimes it may "receive" zero packages, for instance. (Slow days happen, I guess.)

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. Read on.

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 this book's Web site.
{
  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.)

Tip

You can informally "promise" that class Package has a member to retrieve its priority, but you should make it a requirement that the compiler can enforce. You require any object placed in the PriorityQueue to have such a member.

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; }
}

Note

The notation { get; } is how to write a property in an interface declaration.

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.

Tip

The simple way to write a generic class is to write a nongeneric version first and then substitute the <T>s. For example, you would write the PriorityQueue class for Package objects, test it, and then "genericize" it.

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 us. 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 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.

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

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:

Note

// PackageFactory is part of the PriorityQueue example on the Web site.
  // 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.

Note

Take a look at the PackageFactoryWithIterator example on csharp102.info.

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).

Note

Note that I have CreatePackage return a reference to IPrioritizable, which is more general than returning a reference to Package. This example shows indirectionMain() refers to a Package indirectly, through an interface that Package implements. Indirection insulates Main() from the details of what CreatePackage returns. You then have greater freedom to alter the underlying implementation of the factory without affecting Main().

More about factories

Tip

Factories are helpful for generating lots of test data. (A factory needn't use random numbers — that's just what was needed for the PriorityQueue example.)

Note

Factories improve programs by isolating object creation. Every time you mention a specific class by name in your code, you create a dependency on that class. The more such dependencies you have, the more tightly coupled (bound together) your classes become.

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

Note

PriorityQueue needs a couple of small bits of "spackling." Here are the issues:

  • 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 ints, strings, 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.

Note

A matching limitation is needed for PriorityQueue. You want the compiler to squawk if you try to instantiate for a type that doesn't implement IPrioritizable. In the nongeneric form of PriorityQueue (written specifically for type Package, say), the compiler squeals automatically (I recommend earplugs) when one of your priority queue methods tries to call a method that Package doesn't have. But, for generic classes, you can go to the next level with an explicit constraint. Because you could instantiate the generic class with literally any type, you need a way to tell the compiler which types are acceptable — because they're guaranteed to have the right methods.

Note

You add the constraint by specifying IPrioritizable in the heading for PriorityQueue, like this:

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!"

Note

You specify constraints by listing one or more of the following elements (separated by commas) in a where clause:

  • 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.

Table 8-1. Generic Constraint Options

Constraint

Meaning

Example

MyBaseClass

T must be, or extend, MyBaseClass.

where T: MyBaseClass

IMyInterface

T must implement IMyInterface.

where T: IMyInterface

struct

T must be any value type.

where T: struct

class

T must be any reference type.

where T: class

new()

T must have a parameterless constructor.

where T: new()

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!

Note

You might have two generic parameters and both need to be constrained. (Yes, you can have more than one generic parameter — think of Dictionary<TKey, TValue>.) Here's how to use two where clauses:

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 ints, 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).

Note

The compiler can't make sense of the null keyword in a generic class because the class may be instantiated for all sorts of data types. That's why Dequeue() uses this line instead:

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.

Note

If you think PriorityQueue is flexible, take a look at an even more flexible version of it — and encounter some object-oriented design principles — in the ProgrammingToAnInterface program, available with this chapter.

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

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