Making Generic Classes - Classes - C# 24-Hour Trainer (2015)

C# 24-Hour Trainer (2015)

Section IV

Classes

Lesson 28

Making Generic Classes

The section “Generic Classes” in Lesson 16 explained how to use generic collection classes. For example, the following code defines a list that holds Employee objects:

public List<Employee> Employees = new List<Employee>();

This list can only hold Employee objects, and when you get an object out of the list, it has the Employee type instead of the less-specific object type.

Lesson 16 also described the main advantages of generic classes: code reuse and specific type checking. You can use the same generic List<> class to hold a list of strings, doubles, or Person objects. By requiring a specific data type, the class prevents you from accidentally ­adding an Employee object to a list of Order objects, and when you get an object from the list you know it is an Order.

In this lesson, you learn how to build your own generic classes so you can raise code reuse to a whole new level.

NOTE

Many other things can be generic. You can probably guess that you can build generic structures because structures are so similar to classes. You can also create generic methods (in either generic or non-generic classes), generic interfaces, generic delegate types, and so on. This lesson focuses on generic classes.

Defining Generic Classes

A generic class declaration looks a lot like a normal class declaration with one or more generic type variables added in angled brackets. For example, the following code shows the basic ­declaration for a generic TreeNode class:

class TreeNode<T>

{

...

}

The <T> means the class takes one type parameter, T. Within the class's code, the type T means whatever type the program used when creating the instance of the class. For example, the following code declares a variable named rootNode that is a TreeNode that handlesstrings:

TreeNode<string> rootNode = new TreeNode<string>();

If you want the class to use multiple type parameters, separate them with commas. For example, suppose you want to make a Matcher class that takes two kinds of objects and matches objects in the two kinds. It might match Employee objects with Job objects to assign employees to jobs. The following code shows how you might declare the Matcher class:

public class Matcher<T1, T2>

{

...

}

The following code shows how you might create an instance of the class to match Employees with Jobs:

Matcher<Employee, Job> jobAssigner = new Matcher<Employee, Job>();

NOTE

Many developers use T for the name of the type in generic classes that take only one type.

If the class takes more than one type, you should use more descriptive names so it's easy to tell the types apart. For example, the generic Dictionary class has two type variables named TKey and TValue that represent the types of the keys and values that the Dictionary will hold.

Inside the class's code, you can use the types freely. For example, the following code shows more of the TreeNode class's code. A TreeNode object represents a node in a tree, with an associated piece of data attached to it. The places where the class uses the data type T are highlighted in bold.

class TreeNode<T>

{

// This node's data.

public T Data { get; set; }

// This node's children.

private List<TreeNode<T>> children = new List<TreeNode<T≫();

// Constructor.

public TreeNode(T data)

{

Data = data;

}

// Override ToString to display the data.

public override string ToString()

{

if (Data == null) return "";

return Data.ToString();

}

...

}

Notice how the class uses the type T throughout its code. The class starts by defining a Data field of type T. This is the data (of whatever data type) associated with the node.

Each node also has a list of child nodes. To hold the right kind of TreeNode objects, the children variable is a generic List<TreeNode<T≫, meaning it can hold only TreeNode<T> objects.

The class's constructor takes a parameter of type T and saves it in the object's Data property.

To make displaying a TreeNode easier, the class overrides its ToString method so it calls the ToString method provided by the Data object. For example, if the object is a TreeNode<string>, this simply returns the string's value.

Using Generic Constraints

The previous example overrides the TreeNode class's ToString method to make it call the Data object's ToString method. Fortunately, all objects have a ToString method so you know this is possible, but what if you want to call some other method provided by the object?

For example, suppose you want to create a new instance of type T. How do you know that type T provides a constructor that takes no parameters? What if you want to compare two objects of type T to see which is greater? Or what if you want to compare two type Tobjects to see if they are the same (an important test for the Dictionary class)? How do you know whether two type T objects are comparable?

You can use generic constraints to require that the types used by the program meet certain criteria such as comparability or providing a parameterless constructor.

To use a generic constraint, follow the normal class declaration with the keyword where, the name of the type parameter that you want to constrain, a colon, and the constraint. Some typical constraints include:

· A class from which the type must inherit

· An interface (or interfaces) that the type must implement

· new() to indicate that the type must provide a parameterless constructor

· struct to indicate that the type must be a value type such as the built-in value types (int, bool) or a structure

· class to indicate that the type must be a reference type

Separate multiple constraints for the same type parameter with commas. If you want to constrain more than one type parameter, use a new where clause.

For example, the following code defines the generic Matcher class, which takes two generic type parameters T1 and T2. (Note that this code skips important error handling such as checking for null values to keep things simple.)

public class Matcher<T1, T2>

where T1 : IComparable<T2>, new()

where T2 : new()

{

private void test()

{

T1 t1 = new T1();

T2 t2 = new T2();

...

if (t1.CompareTo(t2) < 0)

{

// t1 is "less than" t2.

...

}

}

...

}

The first constraint requires that type parameter T1 implement the IComparable interface for the type T2 so the code can compare T1 objects to T2 objects. The next constraint requires that the T1 type also provide a parameterless constructor. You can see that the code creates a new T1 object and uses its CompareTo method (which is defined by IComparable).

The second where clause requires that the type T2 also provide a parameterless constructor. The code needs that because it also creates a new T2 instance.

In general, you should use as few constraints as possible because that makes your generic code usable in as many circumstances as possible. If your code won't need to create new instances of a data type, don't use the new constraint. If your code won't need to compare objects, don't use the IComparable constraint.

Making Generic Methods

In addition to building generic classes, you can also build generic methods inside either a generic class or a regular non-generic class.

For example, suppose you want to rearrange the items in a list so the new order alternately picks items from each end of the list. If the list originally contains the numbers 1, 2, 3, 4, 5, 6, then the alternated list contains 1, 6, 2, 5, 3, 4.

The following code shows how a program could declare an Alternate method to return an ­alternated list. The part of the code that defines the generic parameter T is shown in bold.

public List<T> Alternate<T>(List<T> list)

{

// Make a new list to hold the results.

List<T> newList = new List<T>();

...

return newList;

}

The Alternate method takes a generic type parameter T. It takes as a regular parameter a List that holds items of type T and it returns a new List containing items of type T.

The code creates a new List<T> to hold the results. (Note that it does not need to require the type T to have a default constructor because the code is creating a new List, not a new T.) The code then builds the new list (not shown here) and returns it.

The following code shows how a program might use this method:

List<string> strings = new List<string>(stringsTextBox.Text.Split(' '));

List<string> alternatedStrings = Alternate<string>(strings);

alternatedStringsTextBox.Text = string.Join(" ", alternatedStrings);

The first statement defines a List<string> and initializes it with the space-separated values in the TextBox named stringsTextBox.

The second statement calls Alternate<string> to create an alternated List<string>. Notice how the code uses <string> to indicate the data type that Alternate will manipulate. (This is actually optional and the program will figure out which version of Alternate to use if you omit it. However, this makes the code more explicit and may catch a bug if you try to alternate a list containing something unexpected such as Person objects.)

The third statement joins the values in the new list, separating them with spaces, and displays the result.

Generic methods can be quite useful for the same reasons that generic classes are. They allow code reuse without the extra hassle of converting values to and from the non-specific object class. They also perform type checking, so in this example, the program cannot try to alternate a List<int> by calling Alternate<string>.

Try It

In this Try It, you build a generic Randomize method that randomizes an array of objects of any type. To make it easy to add the method to any project, you add the method to an ArrayMethods class. To make the method easy to use, you make it static, so the main program doesn't need to instantiate the class to use it.

Lesson Requirements

In this lesson, you:

· Start a new project and give it an ArrayMethods class.

· Create a generic Randomize method with one generic type parameter T. The method should take as a parameter an array of T and randomize the items it contains.

· Make the main program test the method.

NOTE

You can download the code and resources for this lesson from the website at www.wrox.com/go/csharp24hourtrainer2e.

Hints

· Try to figure out the Randomize method's declaration yourself before you read the step-by-step instructions that follow.

Step-by-Step

· Start a new project and give it an ArrayMethods class.

1. This is reasonably straightforward. You don't need to make the ArrayMethods class generic.

· Create a generic Randomize method with one generic type parameter T. The method should take as a parameter an array of T and randomize the items it contains.

1. The following code shows how you can implement this method:

2. class ArrayMethods

3. {

4. // Make a Random object to use to pick random items.

5. private static Random Rand = new Random();

6. // Randomize the items in an array.

7. public static void Randomize<T>(T[] items)

8. {

9. // For each spot in the array, pick

10. // a random item to swap into that spot.

11. for (int i = 0; i < items.Length - 1; i++)

12. {

13. // Pick a random item j between i and the last item.

14. int j = Rand.Next(i, items.Length);

15. // Swap item j into position i.

16. T temp = items[i];

17. items[i] = items[j];

18. items[j] = temp;

19. }

20. }

}

· Make the main program test the method.

1. The program I wrote uses two TextBoxes, one to hold the original items and one to display the randomized items. When you click the Randomize button, the following code executes:

2. // Randomize the items and display the results.

3. private void randomizeButton_Click(object sender, EventArgs e)

4. {

5. // Get the items as an array of strings.

6. string[] items = itemsTextBox.Lines;

7. // Randomize the array.

8. ArrayMethods.Randomize<string>(items);

9. // Display the result.

10. randomizedTextBox.Lines = items;

}

Notice that the code uses the TextBox's Lines property to get the entered values. That property returns the lines in a multi-line TextBox as an array of strings.

Also notice that the code doesn't need to make an instance of the ArrayMethods class. That's the advantage of making the Randomize method static.

Exercises

1. [Hard] The Randomize method in the Try It doesn't actually need to work with an array. What it really needs is to access items by index. The IList interface requires that a class ­provide a Count property and indexes.

Write a new version of the generic Randomize method that takes as a parameter an IList. (Hint: You'll also need a type parameter for the items inside the list.) Update the program to test both versions of the method. Note that C# cannot infer which version to use if you don't include type parameters when the main program invokes the method.

2. Finish building the generic Alternate method described earlier in this lesson. Add the code needed to make the alternating version of the list. To make using the method easy, make it static in the ArrayMethods class. Make the main program test the method with lists containing odd and even numbers of items.

3. [Hard] The solution to Exercise 1 rearranges the items in an IList randomly. The same approach would be tricky for the Alternate method in Exercise 28.2 because it's not obvious how you would shuffle the items around in the same array without losing track of where they all belong. (At least I couldn't think of a good way to do it.)

However, you can use a slightly different approach. Add an Alternate method to the ArrayMethods class that uses an intermediate array to arrange the items in an IList.

4. [Hard] Make the TreeNode class to represent a tree node associated with a piece of data of some generic type. In addition to the code shown earlier in this lesson, give the class:

· An AddChild method that adds a new child node to the node for which the method is invoked. Have the method take a piece of data of the class's generic type as a parameter and return a new TreeNode representing that piece of data.

· A private AddToListPreorder method that adds a node's subtree to a list in preorder format. The preorder format lists the node's data first and then recursively calls the method to add the data for the node's children. You can use code similar to the following:

· // Recursively add our subtree to an existing list in preorder.

· private void AddToListPreorder(List<TreeNode<T>> list)

· {

· // Add this node.

· list.Add(this);

· // Add the children.

· foreach (TreeNode<T> child in Children)

· child.AddToListPreorder(list);

}

· A public Preorder method that returns the node's subtree items in a list in preorder format. The method should call AddToListPreorder to do all of the work. You can use code similar to the following:

· // Return a list containing our subtree in preorder.

· public List<TreeNode<T>> Preorder()

· {

· List<TreeNode<T≫ list = new List<TreeNode<T≫();

· AddToListPreorder(list);

· return list;

}

· For extra credit, add similar methods to build lists in postorder and inorder. In ­postorder, a node recursively adds its children to the list and then adds its own data. In inorder, a node recursively adds the first half of its children to the list, then itself, and then the rest of its children.

Make the main program build the tree shown in Figure 28.1, although it doesn't need to display it graphically as in the figure. Make the program display the tree's preorder, ­postorder, and inorder representations, as shown in Figure 28.2.

A tree diagram of letters inside circles. Circle A breaks into B and E. B branches to C and D, while E to F, G, and I. Below G is H.

Figure 28.1

Generic Tree dialog box presenting three boxes labeled Preorder, Inorder, and Postorder listing letters alphabetically and randomly.

Figure 28.2

5. Make a generic PriorityQueue class. The class is basically a list holding generic items where each item has an associated priority. Give the class a nested ItemData structure similar to the following to hold an item:

6. // A structure to hold items.

7. private struct ItemData

8. {

9. public int Priority { get; set; }

10. public T Data { get; set; }

}

This structure is defined inside the PriorityQueue class and won't be used outside of the class, so it can be private. Note that this structure uses the class's generic type parameter T for the data it holds.

The class should store its ItemData objects in a generic List.

Give the PriorityQueue class a public Count property that returns the number of items in the list.

Give the class an AddItem method that takes as parameters a piece of data and a priority. It should make a new ItemData object to hold these values and add it to the list.

Finally, give the class a GetItem method that searches the list for the item with the smallest priority number (priority 1 means top priority), removes that item from the list, and returns the item and its priority via parameters passed for output. (If there's a tie for lowest priority number, return the first item you find with that priority.)

11.Make a generic Sack class that holds items with weights. Give the class the following features:

· A constructor that takes as a parameter the Sack's total capacity.

· An Add method that takes as parameters a data item and a weight. If the total weight in the Sack exceeds the Sack's capacity, the method should throw an ArgumentException.

· An Items method that returns a List holding the items in the Sack.

· A Weights method that returns a List holding the weights of the items in the Sack.

Build a user interface that lets the user add items with weights to a Sack with a capacity of 100. Use two ListBoxes to display the items in the Sack and their weights after each addition.

12.Make a program similar to the one you built for Exercise 28.6 except using a Box class. A Box should be similar to a Sack class except it should have a maximum total volume in addition to a maximum total weight.

13.Make a generic method that swaps its two parameters' values.

14.[Advanced] The Math.Min and Math.Max methods are very useful, but they have two big drawbacks. First, they take only two parameters. That means if you want to find the largest and smallest of more than two values, you need to use them repeatedly. (Other available methods, notably LINQ, are described in Lesson 36.)

The second drawback is that they only work with double parameters. If you pass ints or floats into the methods, the values are promoted to the double data type so the methods still work, but their results are doubles so you'll need to convert them if you want the results to have the original data types.

For this exercise, write generic Min and Max methods that can take any number of parameters and that return a value in the parameters' data type. Hints:

· To allow a method to take any number of parameters, you can use a parameter array. A parameter array should begin with the params keyword, should be an array, and must come last in the method's parameter list. For example, DoSomething(params string[] values).

· Obviously you'll need to be able to compare the parameters to each other.

NOTE

Please select the videos for Lesson 28 online at www.wrox.com/go/csharp24hourtrainer2evideos.