What’s in This Chapter
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.
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:
object
type. For example, a List<string>
can hold only string
values; its Add
method can add only string
s to the list; and its Item
method returns string
values. This makes it more difficult to accidentally add int
s, Employee
s, or other incorrect types of objects to the list.List<Employee>
, Visual Studio knows that the items in the collection are Employee
objects, so it can give you appropriate IntelliSense.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.PriorityQueue
class, you can make PriorityQueue
s 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.
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.
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.
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);
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 int
s or string
s, 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.
A generic type’s where
clause can include one or more of the following elements.
Element | Meaning |
struct | The type must be a value type. |
class | The 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
.
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);
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.
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.
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 TextBox
es. 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.
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.
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.
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.)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>
.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.BoundValues
method process an IEnumerable
instead of an array and return a List
.MiddleValue
method that takes three values as parameters and returns the one in the middle.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.)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
.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.