Chapter 15
Generics

What’s in This Chapter

  • Defining generic classes and methods
  • Constraining generic types
  • Instantiating generic classes
  • Using generic collection classes
  • Defining generic extension methods

Wrox.com Downloads for This Chapter

Please note that all the code examples for this chapter are available as a part of this chapter’s code download on the book’s website at www.wrox.com/go/csharp5programmersref on the Download Code tab.

Chapter 11, “OOP Concepts,” describes a class as like a blueprint or cookie cutter for creating objects. After you define a class, you can use it to create any number of objects with similar general characteristics but different details.

Similarly, a generic is like a cookie cutter for creating classes. After you define a generic, you can use it to create any number of classes that have similar features.

For example, the System.Collections.Generic namespace described in the preceding chapter defines a generic List class. That class lets you create lists of strings, lists of integers, lists of Employee objects, or lists of just about anything else.

This chapter explains how you can define and use your own generic classes.

Advantages of Generics

A generic class takes one or more data types as parameters. When you create an instance of a generic class, those parameters are filled in with specific data types such as string, int, or Employee. Tying the class to specific data types gives it several advantages over nongeneric classes:

  • Strong typing—Methods can take parameters and return values that have the class’s instance type instead of a nonspecific object type. For example, a List<string> can hold only string values; its Add method can add only strings to the list; and its Item method returns string values. This makes it more difficult to accidentally add ints, Employees, or other incorrect types of objects to the list.
  • IntelliSense—By providing strong typing, a class built from a generic lets Visual Studio provide IntelliSense. If you make a List<Employee>, Visual Studio knows that the items in the collection are Employee objects, so it can give you appropriate IntelliSense.
  • No boxing—Because the class manipulates objects with a specific data type, your program doesn’t need to convert items to and from the nonspecific object data type. For example, if a program stores TextBox controls in a nongeneric collection, the program must convert the TextBox controls to and from the object class when it adds and uses items in the collection. Avoiding these steps makes the code more efficient.
  • Code reuse—You can use a generic class with more than one data type. For example, if you have built a generic PriorityQueue class, you can make PriorityQueues holding Student, Applicant, MotorVehicle, or Donor objects. Without generics, you would need to build four separate classes to build strongly typed priority queues for each of these types of objects. Reusing this code makes it easier to write, test, debug, and maintain the code.

The main disadvantage to generics is that they are slightly more complicated and confusing than nongeneric classes. If you know that you will only ever need to provide a class that works with a single type, you can simplify things slightly by not using a generic class. If you think you might want to reuse the code later for another data type, it’s easier to just build the class generically from the start.

Defining Generics

C# allows you to define generic classes, structures, interfaces, methods, and delegates. The basic syntax for all those is similar, so when you know how to make generic classes, making generic structures, interfaces, and the others is fairly easy.

To define a generic class, make a class declaration as usual. After the class name, add one or more type names for data types surrounded by brackets. The type names are similar to the parameters names you would define for a method except they are types, not simple values. The class’s code can use the names to refer to the types associated with the instance of the generic class. This may sound confusing, but an example should make it fairly easy to understand.

Suppose you want to build a binary tree that can hold any kind of data in its nodes. The following code shows how you could define a BinaryNode class to hold the tree’s data. The type name T is highlighted in bold where it appears.

public class BinaryNode<T>
{
    public T Value;
    public BinaryNode<T> LeftChild, RightChild;
}

The class’s declaration takes a type parameter named T. (Many developers use the name T for the type parameter. If the class takes more than one type parameter separated by commas, they start each name with T as in TKey and TData.)

The class defines a public field named Value that has type T. This is the data that is stored in the node.

The class also defines two fields that refer to the node’s left and right children in the binary tree. Those fields are references to objects from this same class: BinaryNode<T>.

The following code shows how a program could use this class to build a small binary tree of Employee objects.

// Define the tree's root node.
BinaryNode<Employee> root = new BinaryNode<Employee>();
root.Value = new Employee("Ben", "Baker");

// Create the root's left child.
root.LeftChild = new BinaryNode<Employee>();
root.LeftChild.Value = new Employee("Ann", "Archer");

// Create the root's right child.
root.RightChild = new BinaryNode<Employee>();
root.RightChild.Value = new Employee("Cindy", "Carter");

This code first creates a new BinaryNode<Employee> to represent the tree’s root. It sets that node’s Value property to a new Employee object representing Ben Baker.

Next, the code sets the root’s LeftChild equal to a new BinaryNode<Employee>. It sets that node’s Value to a new Employee object representing Ann Archer.

Finally, the code uses similar steps to give the root a right child holding an Employee object representing Cindy Carter.

Generic Constructors

Like any other class, generic classes can have constructors. For example, the following constructor initializes a BinaryNode object’s LeftChild and RightChild references.

// Set this node's value and children.
public BinaryNode(T value,
    BinaryNode<T> leftChild = null,
    BinaryNode<T> rightChild = null)
{
    Value = value;
    LeftChild = leftChild;
    RightChild = rightChild;
}

Notice how this code can use the type T without defining it. That type variable was defined in the class declaration, so it can be used throughout the class’s code.

To use the constructor, the main program adds normal parameters after the type parameters in the object declaration. The following code uses the new constructor to create a binary tree similar to the previous one.

// Define the child nodes.
BinaryNode<Employee> leftChild =
    new BinaryNode<Employee>(new Employee("Ann", "Archer"));
BinaryNode<Employee> rightChild =
    new BinaryNode<Employee>(new Employee("Cindy", "Carter"));

// Define the tree's root node.
BinaryNode<Employee> root = new BinaryNode<Employee>
(
    new Employee("Ben", "Baker"),
    leftChild,
    rightChild
);

This code uses the constructor to create the left and right child nodes. It doesn’t pass children into those constructor calls, so the child nodes’ left and right children are set to null.

The code then creates the root node, this time passing the constructor the root’s left and right children.

Multiple Types

If you want the class to work with more than one type, you can add other types to the declaration separated by commas. For example, suppose that you want to create a dictionary that associates keys with pairs of data items. Example program GenericPairDictionary uses the following code to define the generic PairDictionary class. This class acts as a dictionary that associates a key value with a pair of data values. The class declaration includes three data types named TKey, TValue1, and TValue2.

// A Dictionary that associates a pair of data values with each key.
public class PairDictionary<TKey, TValue1, TValue2>
{
    // A structure to hold paired data.
    public struct ValuePair
    {
        public TValue1 Value1;
        public TValue2 Value2;
        public ValuePair(TValue1 value1, TValue2 value2)
        {
            Value1 = value1;
            Value2 = value2;
        }
    }

    // A Dictionary to hold the paired data.
    private Dictionary<TKey, ValuePair> ValueDictionary =
        new Dictionary<TKey, ValuePair>();

    // Return the number of data pairs.
    public int Count
    {
        get { return ValueDictionary.Count; }
    }

    // Add a key and value pair.
    public void Add(TKey key, TValue1 value1, TValue2 value2)
    {
        ValueDictionary.Add(key, new ValuePair(value1, value2));
    }

    // Remove all data.
    public void Clear()
    {
        ValueDictionary.Clear();
    }

    // Return True if PairDictionary contains this key.
    public bool ContainsKey(TKey key)
    {
        return ValueDictionary.ContainsKey(key);
    }

    // Return a data pair.
    public void GetValues(TKey key, out TValue1 value1, out TValue2 value2)
    {
        ValuePair pair = ValueDictionary[key];
        value1 = pair.Value1;
        value2 = pair.Value2;
    }

    // Set a data pair.
    public void SetValues(TKey key, TValue1 value1, TValue2 value2)
    {
        ValueDictionary[key] = new ValuePair(value1, value2);
    }

    // Return a collection containing the keys.
    public Dictionary<TKey, ValuePair>.KeyCollection Keys
    {
        get { return ValueDictionary.Keys; }
    }
    // Remove a particular entry.
    public void Remove(TKey key)
    {
        ValueDictionary.Remove(key);
    }
}

The PairDictionary class defines a ValuePair class to hold pairs of data values. The ValuePair class has two public fields of types TValue1 and TValue2. Its only method is a constructor that makes initializing the values easier.

Notice that the ValuePair class is not generic. It uses the TValue1 and TValue2 types defined by the PairDictionary class’s declaration, but it doesn’t define any generic types of its own.

Next, the PairDictionary class declares a generic Dictionary<TKey, ValuePair> object named ValueDictionary. The class delegates its Count, Add, Clear, ContainsKey, GetValues, SetValues, Keys, and Remove methods to ValueDictionary.

The following code creates an instance of the generic PairDictionary class that uses integers as keys and strings for both data values. It adds three entries to the PairDictionary and then retrieves and displays the entry with key value 82.

// Create the PairDictionary and add some data.
PairDictionary<int, string, string> dictionary =
    new PairDictionary<int, string, string>();
dictionary.Add(21, "Arthur", "Ash");
dictionary.Add(82, "Betty", "Barter");
dictionary.Add(13, "Charlie", "Carruthers");

// Display the values for key value 82.
string value1, value2;
dictionary.GetValues(82, out value1, out value2);
Console.WriteLine(value1 + " " + value2);

Constrained Types

To get the most out of your generic classes, you should make them as general as possible. Depending on what the class is for, however, you may need to constrain the class’s generic types.

For example, suppose you want to make a generic SortedBinaryNode class similar to the BinaryNode class described earlier but that keeps its values sorted. The node’s Add method should insert a new value in the proper position in the tree.

When you call a node’s Add method, the method compares the node’s value to the new value. It then passes the new value to its left or right child depending on whether the new value is greater than or less than the node’s value.

For example, suppose node A contains the value 20 and you pass its Add method the new value 15. The value 15 is less than 20, so node A sends the new value into its left subtree.

If node A has a left child, it calls that child’s Add method to add the child somewhere in that subtree.

If node A has no left child, it creates a new node to hold the value 15 and takes that node as its new left child.

Determining whether a new value belongs in a node’s left or right subtree is straightforward if the node holds ints or strings, but there’s no obvious way to determine whether one Employee object should be placed before another. The SortedBinaryNode class works only if the data type of its objects allows comparison.

One way to ensure you can compare objects is to require that the type of the items implements the IComparable interface. Then the program can use the CompareTo method to see whether one item is greater than or less than another item.

To require that a generic type implements an interface, add a where clause after the class’s declaration, as shown in the following code.

public class SortedBinaryNode<T> where T : IComparable<T>
{
    ...
}

This code requires that type T implements IComparable<T>.

The SortedBinaryTree example program, which is available for download on this book’s website, uses the following complete SortedBinaryNode class.

public class SortedBinaryNode<T> where T : IComparable<T>
{
    public T Value;
    public SortedBinaryNode<T> LeftChild, RightChild;

    // Set this node's value and children.
    public SortedBinaryNode(T value,
        SortedBinaryNode<T> leftChild = null,
        SortedBinaryNode<T> rightChild = null)
    {
        Value = value;
        LeftChild = leftChild;
        RightChild = rightChild;
    }

    // Add a new value to this node's subtree.
    public void Add(T newValue)
    {
        // See if it belongs in the left or right child's subtree.
        if (newValue.CompareTo(Value) < 0)
        {
            // Left subtree.
            if (LeftChild == null)
                // Add it in a new left child.
                LeftChild = new SortedBinaryNode<T>(newValue);
            else
                // Add it in the existing left subtree.
                LeftChild.Add(newValue);
        }
        else
        {
            // Right subtree.
            if (RightChild == null)
                // Add it in a new right child.
                RightChild = new SortedBinaryNode<T>(newValue);
            else
                // Add it in the existing right subtree.
                RightChild.Add(newValue);
        }
    }
}

The program uses an Employee class that implements IComparable<Employee>. Its CompareTo method, which is required by the interface, compares two Employee objects’ full names and returns a value indicating which one comes first alphabetically.

The SortedBinaryTree example’s main program uses the following code to build a small sorted tree of Employee objects.

// Create some Employees.
Employee jody = new Employee("Jody", "Adams");
Employee wanda = new Employee("Wanda", "Cortez");
Employee george = new Employee("George", "McGee");
Employee dom = new Employee("Dom", "Hall");
Employee linda = new Employee("Linda", "Brock");

// Create the root node.
SortedBinaryNode<Employee> root = new SortedBinaryNode<Employee>(jody);

// Add some other Employees to the tree.
root.Add(wanda);
root.Add(george);
root.Add(dom);
root.Add(linda);

The code first creates some Employee objects. It then makes a root node holding the Employee representing Jody Adams.

The program then calls the root node’s Add method, passing it various Employee objects. You can follow each Employee as it is added to the tree. For example, Wanda Cortez comes alphabetically after Jody Adams, so the wanda Employee is added to the root node’s right subtree. If you follow each of the Employee objects, you’ll get the tree shown in Figure 15-1.

c15f001.eps

Figure 15-1: Example program SortedBinaryTree builds this tree of Employee objects.

A generic type’s where clause can include one or more of the following elements.

ElementMeaning
structThe type must be a value type.
classThe type must be a reference type.
new()The type must have a parameterless constructor.
«baseclass»The type must inherit from baseclass.
«interface»The type must implement interface.
«typeparameter»The type must inherit from typeparameter.

For example, the following code defines the StrangeGeneric class. This class takes three type parameters. Type T1 must implement the IComparable<T1> interface and must provide a parameterless constructor. Type T3 must inherit from the Control class. Type T2 must inherit from type T3.

public class StrangeGeneric<T1, T2, T3>
    where T1 : IComparable<T1>, new()
    where T3 : Control
    where T2 : T3
{

}

The following code creates an instance of the StrangeGeneric class.

StrangeGeneric<int, Panel, ScrollableControl> strange =
    new StrangeGeneric<int, Panel, ScrollableControl>();

The int class implements IComparable<int> and has a parameterless constructor. The ScrollableControl inherits from Control and Panel inherits from ScrollableControl. The full inheritance hierarchy for the Panel class is

System.Object 
  System.MarshalByRefObject
    System.ComponentModel.Component
      System.Windows.Forms.Control
        System.Windows.Forms.ScrollableControl
          System.Windows.Forms.Panel

Constraining a type gives C# more information about that type, so it lets you use any known properties and methods. In the previous code, for example, the StrangeGeneric class knows that type T3 inherits from the Control class so you can safely use Control properties and methods such as Anchor, BackColor, and Font.

Default Values

The new() constraint requires a generic type to provide a parameterless constructor so the class’s code can create a new instance of the type. For example, if the type’s name is T, the class could execute the following statement.

T newValue = new T();

In addition to making a new instance of the type T, it may also be useful to set a variable of type T to a default value. Unfortunately, you can’t know what a type’s default value is until you know the type. For example, the default value for int is 0, the default value for a struct is an uninitialized structure, and the default value for a string or other reference type is null.

Fortunately, C# provides the default keyword, to let generic classes assign default values. The following statement creates a new variable of type T and sets it equal to whatever is the default value for that type.

T newValue = default(T);

Instantiating Generic Classes

The previous sections have already shown a few examples of how to instantiate a generic class. The program declares the class and includes whatever data types are required inside brackets. The following code shows how a program might create a generic list of strings.

List<string> names = new List<string>();

To pass normal parameters to a generic class’s constructor, simply add them inside the parentheses after the brackets.

Generic Collection Classes

The System.Collections.Generic namespace defines several generic classes. These are basically collection classes that use generics to work with specific data types. See the section “Generic Collections” in Chapter 14, “Collection Classes,” for more information and a list of the more useful predefined generic collection classes.

Generic Methods

Generics are usually used to build classes that are not data type-specific such as the generic collection classes. You can also give a class (generic or otherwise) a generic method. Just as a generic class is not tied to a particular data type, the parameters of a generic method are not tied to a specific data type.

To make a generic method, include type parameters similar to those you would use for a generic class.

Example program Switcher uses the following code to define a generic Switch method.

public static class Switcher
{
    // Switch two values.
    public static void Switch<T>(ref T value1, ref T value2)
    {
        T temp = value1;
        value1 = value2;
        value2 = temp;
    }
}

The Switch method takes a generic type T. It also takes two parameters of type T. It creates a temporary variable of type T and uses it to swap the two values.

The following code shows how the main program uses the Switch method.

string value1 = value1TextBox.Text;
string value2 = value2TextBox.Text;
Switcher.Switch<string>(ref value1, ref value2);
value1TextBox.Text = value1;
value2TextBox.Text = value2;

This code gets two string values from TextBoxes. It uses the Switch method to swap their values and displays the results.

Note that the Switcher class is not generic but it contains a generic method. You can also create generic classes that contain both generic and nongeneric methods.

Generics and Extension Methods

Extension methods let you add new features to existing classes, whether they’re generic or nongeneric. For example, suppose you have an application that uses a List<Student>. This class is a generic collection class defined in the System.Collections.Generic namespace. It’s not defined in your code so you can’t modify it. However, you can add extension methods to it.

The following code adds an AddStudent method to List<Student> that takes as parameters a first and last name, uses those values to make a Student object, and adds it to the list.

public static class ListExtensions
{
    public static void AddStudent(this List<Student> students,
        string firstName,string lastName)
    {
        students.Add(new Student()
            { FirstName = firstName, LastName = lastName });
    }
}

This example works specifically with the Student type. It relies on the fact that this is the List<Student> class when it uses the Student class’s constructor.

Sometimes, you can make a generic extension method that works with more general classes. For example, the following code adds a generic NumDistinct method to the generic List<T> class.

public static int NumDistinct<T>(this List<T> list)
{
    return list.Distinct().Count();
}

This method doesn’t need to know what kind of objects the list contains. It just invokes the list’s Distinct method, calls the Count method on the result, and returns the value given by Count.

For more information on extension methods, see the section “Extension Methods” in Chapter 6.

Summary

A class is an abstraction that defines the properties, methods, and events that should be provided by instances of the class. After you define a class, you can make any number of instances of it, and they will all have the features defined by the class.

Generics take abstraction one level farther. A generic class abstracts the features of a set of classes. After you have defined a generic class, you can make any number of objects that have similar behaviors but that may work with different data types. Similarly, you can make generic structures, interfaces, methods, and delegates that can work with multiple data types.

Generics let you reuse the same code while working with different data types. They provide strong type checking, which lets you avoid boxing and unboxing. They also let Visual Studio provide IntelliSense support, which makes writing code easier and faster.

For more information on generics including some of their more esoteric syntax, see “An Introduction to C# Generics” at msdn.microsoft.com/library/ms379564.aspx.

The chapters so far have focused on programs that are relatively self-contained. They generate their own data or take input from the user, perform some calculations, and display the results on the program’s user interface or in the Console window.

The chapters in the next part of the book describe techniques a program can use to interact with the outside system. They explain how to print documents, save settings that persist when the program isn’t running, work with files and directories, and interact with networks. The next chapter starts the new focus by explaining how to generate output on a printer.

Exercises

  1. Make a generic PriorityQueue class that associates keys with objects. Its Dequeue method should return the key/object pair with the lowest key value and remove that value from the queue. Make a program that uses a PriorityQueue<int, string>. (Hint: Use a List<KeyValuePair> to hold the items. Make the Dequeue method loop through the list to find the lowest key value.)
  2. Make a generic IncreasingQueue class that stores objects in a queue and requires each object to be larger than the one before it in the queue. Make the class’s constructor take as a parameter a lower bound for all entries. (In other words, all entries must be larger than the lower bound and added in increasing order.) Make a test program that demonstrates an IncreasingQueue<float>.
  3. Write a generic BoundValues method that takes an array as a parameter and ensures that all its values are between a lower and upper bound. For example, if prices is an array of decimal, then BoundValues(prices, 0, 1000) would set any values in the array that are smaller than 0 to 0 and any values in the array that are larger than 1000 to 1000.
  4. Repeat Exercise 3 but this time make the BoundValues method process an IEnumerable instead of an array and return a List.
  5. Write a generic MiddleValue method that takes three values as parameters and returns the one in the middle.
  6. Create a CircularQueue class. The Enqueue method adds an item to the end of the queue. The NextItem method returns the next item in the queue. If the object reaches the end of its queue, it starts over at the beginning. For example, if the queue contains the values A, B, and C, then repeatedly calling NextItem will return the values A, B, C, A, B, C, A, B, C, and so forth. (Hint: Use a List to hold the queue’s values.)
  7. Create a Bundle class that uses a List to hold items. Create an Add method so that the program can add items to a Bundle. Override its ToString method to return a string holding the items in the bundle separated by semicolons. For example, if the bundle contains the values “hello” and 13, the ToString method should return hello;13.
  8. The Bundle class you built for Exercise 7 can delegate methods to the List object that it contains. At a minimum you need to give it an Add method so the program can put items in the Bundle. Unfortunately, the List<T> class supports more than 80 properties and methods that you could delegate. Delegating them all would be a huge amount of work.

    Fortunately, there’s an easier solution: Make Bundle<T> inherit from List<T>. Repeat Exercise 7 using this technique.

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

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