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 string
s, double
s, 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.
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 handles string
s:
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 Employee
s with Job
s:
Matcher<Employee, Job> jobAssigner = new Matcher<Employee, Job>();
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.
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 T
objects 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:
new()
to indicate that the type must provide a parameterless constructorstruct
to indicate that the type must be a value type such as the built-in value types (int
, bool
) or a structureclass
to indicate that the type must be a reference typeSeparate 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.
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>
.
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.
In this lesson, you:
ArrayMethods
class.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.Randomize
method's declaration yourself before you read the step-by-step instructions that follow.ArrayMethods
class.
ArrayMethods
class 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.
class ArrayMethods
{
// Make a Random object to use to pick random items.
private static Random Rand = new Random();
// Randomize the items in an array.
public static void Randomize<T>(T[] items)
{
// For each spot in the array, pick
// a random item to swap into that spot.
for (int i = 0; i < items.Length - 1; i++)
{
// Pick a random item j between i and the last item.
int j = Rand.Next(i, items.Length);
// Swap item j into position i.
T temp = items[i];
items[i] = items[j];
items[j] = temp;
}
}
}
TextBox
es, one to hold the original items and one to display the randomized items. When you click the Randomize button, the following code executes:
// Randomize the items and display the results.
private void randomizeButton_Click(object sender, EventArgs e)
{
// Get the items as an array of strings.
string[] items = itemsTextBox.Lines;
// Randomize the array.
ArrayMethods.Randomize<string>(items);
// Display the result.
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.
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.
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.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
.
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:
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.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);
}
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;
}
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.
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:
// A structure to hold items.
private struct ItemData
{
public int Priority { get; set; }
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.)
Sack
class that holds items with weights. Give the class the following features:
Sack
's total capacity.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
.Items
method that returns a List
holding the items in the Sack
.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 ListBox
es to display the items in the Sack
and their weights after each addition.
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.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 int
s or float
s into the methods, the values are promoted to the double
data type so the methods still work, but their results are double
s 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:
params
keyword, should be an array, and must come last in the method's parameter list. For example, DoSomething(params string[] values)
.