Generics

>> Aug 19, 2009

One of the (admittedly few) criticisms leveled against the first version of C# was its lack of support for generics. Generics in C++ (known as templates in that language) had long been regarded as an excellent way of doing things, as it allowed a single type definition to spawn a multitude of specialized types at compile time and thus save an awful lot of time and effort. For whatever reason, generics didn ’ t quite make it into the first release of C#, and the language suffered because of it. Perhaps it was because generics are often seen as being quite difficult to get a handle on, or maybe it was decided that they weren ’ t necessary. Fortunately, with C# version 2.0 generics have joined the party. Even better, they aren ’ t very difficult to use, although they do require a slightly different way of looking at things.
In this chapter you do all of the following:
. Examine what a generic is. You learn about generics in fairly abstract terms at first, because learning the concepts behind generics is crucial to being able to use them effectively.
. See some of the generic types in the .NET Framework in action. This will help you to understand their functionality and power, as well as the new syntax required in your code.
. Define your own generic types, including generic classes, interfaces, methods, and delegates. You also learn additional techniques for further customizing generic types: the defaultkeyword and type constraints.

What Is a Generic?
To best illustrate what a generic is, and why they are so useful, recall the collection classes from the last chapter. You saw how basic collections can be contained in classes such as ArrayList, but that such collections suffer from being untyped, so you need to cast objectitems into whatever type of objects you actually stored in the collection. Because anything that inherits from System .Object (that is, practically anything) can be stored in an ArrayList, you need to be careful.
Assuming that certain types are all that is contained in a collection can lead to exceptions being thrown, and code logic breaking down. You learned some techniques to deal with this, including the code required to check the type of an object.
However, you discovered that a much better solution is to use a strongly typed collection class initially. By deriving from CollectionBase and providing your own methods for adding, removing, and otherwise accessing members of the collection, you learned how you could restrict collection members to those derived from a certain base type or supporting a certain interface. This is where you encounter a problem. Every time you create a new class that needs to be held in a collection, you must do one of the following:
. Use a collection class you ’ ve already made that can contain items of the new type.
. Create a new collection class that can hold items of the new type, implementing all the required methods.
Typically, with a new type you need extra functionality, so more often that not you need a new collection class anyway. Therefore, making collection classes may take up a fair amount of your time!
Generic classes, conversely, make things a lot simpler. A generic class is one that is built around whatever type, or types, you supply during instantiation, enabling you to strongly type an object with hardly any effort at all. In the context of collections, creating a “ collection of type T objects ” is as simple as saying it aloud — and achievable in a single line of code. Instead of code such as:
CollectionClass col = new CollectionClass();
col.Add(new ItemClass());


You can use:
CollectionClass < ItemClass > col = new CollectionClass < ItemClass > ();
col.Add(new ItemClass());


The angle bracket syntax is the way you pass variable types to generic types. In the preceding code, read CollectionClass < ItemClass > as CollectionClassof ItemClass. You will, of course, examine this syntax in more detail later in the chapter.
There ’ s more to the subject of generics than just collections, but they are particularly suited to this area, as you will see later in the chapter when you look at the System.Collections.Genericnamespace. By creating a generic class, you can generate methods that have a signature that can be strongly typed to any type you wish, even catering to the fact that a type may be a value or reference type, and deal with individual cases as they occur. You can even allow only a subset of types to be used, by restricting the types used to instantiate a generic class to those that support a given interface or are derived from a certain type. Moreover, you ’ re not restricted to generic classes — you can create generic interfaces, generic methods (which can be defined on nongeneric classes), and even generic delegates. All this adds a great deal of flexibility to your code, and judicious use of generics can eliminate hours of development time.
You ’ re probably wondering how all this is possible. Usually, when you create a class it is compiled into a type that you can then use in your code. You might think that when you create a generic class it would have to be compiled into a plethora of types, so that you could instantiate it. Fortunately, that ’ s not the case — and given the infinite amount of classes possible in .NET, that ’ s just as well. Behind the scenes, the .NET runtime allows generic classes to be dynamically generated as and when you need them. A given generic class A of B won ’ t even exist until you ask for it by instantiating it.
For those who are familiar with C++, or are interested, this is one difference between C++ templates and C# generic classes. In C++ the compiler detects where you used a specific type of template — for example, Aof B— and compiles the code necessary to create this type. In C# everything happens at runtime.
To summarize, generics enable you to create flexible types that process objects of one or more specific types, where these types are determined when you instantiate or otherwise use the generic. Now it ’ s time to see them in action.

Using Generics
Before you look at how to create your own generics, it ’ s worth looking at those that are supplied by the .NET Framework. These include the types in the System.Collections.Genericnamespace, a namespace that you ’ ve seen several times in your code because it is included by default in console applications. You haven ’ t yet used any of the types in this namespace, but that ’ s about to change. This section looks at the types in this namespace and how you can use them to create strongly typed collections and improve the functionality of your existing collections.
First, though, you ’ ll look at another, simpler generic type that gets around a minor issue with value types: nullable types.

Nullable Types
In earlier chapters, you saw that one of the ways in which value types (which include most of the basic types such as intand double as well as all structs) differ from reference types ( stringand any class) is that they must contain a value. They can exist in an unassigned state, just after they are declared and before a value is assigned, but you can ’ t make use of this in any way. Conversely, reference types may be null.
There are times, and they crop up more often than you might think (particularly when you work with databases), when it is useful to have a value type that can be null. Generics give you a way to do this using the System.Nullable < T > type, as shown in this example:
System.Nullable < int > nullableInt;

This code declares a variable called nullableInt, which can have any value that an intvariable can, plus the value null. This enables you to write code such as the following:
nullableInt = null;

If nullableInt were an int type variable, then the preceding code wouldn ’ t compile.
The preceding assignment is equivalent to the following:
nullableInt = new System.Nullable < int > ();

As with any other variable, you can ’ t just use it before some kind of initialization, whether to null (through either syntax shown above) or by assigning a value.
You can test nullable types to determine whether they are null, just like you test reference types:
if (nullableInt == null)
{
...
}


Alternatively, you can use the HasValueproperty:
if (nullableInt.HasValue)
{
...
}


This wouldn ’ t work for reference types, even one with a HasValue property of its own, because having a null valued reference type variable means that no object exists through which to access this property, and an exception would be thrown.
You can also look at the value of a nullable type by using the Value property. If HasValueis true , then you are guaranteed a non - nullvalue for Value. But if HasValueis false— that is, null has been assigned to the variable — then accessing Value will result in an exception of type System.InvalidOperationException.
Note that nullable types are so useful that they have resulted in a modification of C# syntax. Rather than use the syntax shown above to declare a nullable type variable, you can instead use the following:
int? nullableInt;

int? is simply a shorthand for System.Nullable < int > but is much more readable. In subsequent sections, you ’ ll use this syntax.

Operators and Nullable Types
With simple types, such as int, you can use operators such as +, -, and so on to work with values. With nullable type equivalents, there is no difference: The values contained in nullable types are implicitly converted to the required type and the appropriate operators are used. This also applies to structs with operators that you have supplied:
int? op1 = 5;
int? result = op1 * 2;


Note that here the resultvariable is also of type int?. The following code will not compile:
int? op1 = 5;
int result = op1 * 2;


To get this to work you must perform an explicit conversion:
int? op1 = 5;
int result = (int)op1 * 2;


This works fine as long as op1has a value — if it is null, then you will get an exception of type System.InvalidOperationException.
This raises the obvious question: What happens when one or both values in an operator evaluation are null, such as op1 in the preceding code? The answer is that for all simple nullable types other than bool?, the result of the operation is null, which you can interpret as “ unable to compute. ” For structs you can define your own operators to deal with this situation (as shown later in the chapter), and for bool? there are operators defined for &and | that may result in non - null return values. These are shown in the following table:The results in the table make perfect sense logically — if there is enough information to work out the answer of the computation without needing to know the value of one of the operands, then it doesn ’ t matter if that operand is null.

The ?? Operator
To further reduce the amount of code you need in order to deal with nullable types, and to make it easier to deal with variables that can be null, you can use the ?? operator. Known as the null coalescing operator, it is a binary operator that enables you to supply an alternative value to use for expressions that might evaluate to null. The operator evaluates to its first operand if the first operand is not null, or to its second operator if the first operand is null. Functionally, the following two expressions are equivalent:
op1 ?? op2
op1 == null ? op2 : op1


In this code, op1 can be any nullable expression, including a reference type and, importantly, a nullable type. This means that you can use the ?? operator to provide default values to use if a nullable type is null, as shown here:
int? op1 = null;
int result = op1 * 2 ?? 5;


Because in this example op1is null, op1 * 2will also be null. However, the ??operator detects this and assigns the value 5to result. Importantly, note here that no explicit conversion is required to put the result in the inttype variable result. The ??operator handles this conversion for you. Alternatively, you can pass the result of a ??evaluation into an int? with no problems:
int? result = op1 * 2 ?? 5;

This makes the ??operator a versatile one to use when dealing with nullable variables, and a handy way to supply defaults without using either a block of code in an if structure or the often confusing tertiary operator.
Use the following Try It Out to experiment with a nullable Vectortype.

Try It Out : Nullable Types
1. Create a new console application project called Ch12Ex01 and save it in the directory C:\BegVCSharp\Chapter12.
2. Add a new class called Vectorin the file Vector.cs.
3. Modify the code in Vector.csas follows:
public class Vector
{
public double? R = null;
public double? Theta = null;
public double? ThetaRadians
{
get
{
// Convert degrees to radians.
return (Theta * Math.PI / 180.0);
}
}
public Vector(double? r, double? theta)
{
// Normalize.
if (r < 0)
{
r = -r;
theta += 180;
}
theta = theta % 360;
// Assign fields.
R = r;
Theta = theta;
}
public static Vector operator +(Vector op1, Vector op2)
{
try
{
// Get (x, y) coordinates for new vector.
double newX = op1.R.Value * Math.Sin(op1.ThetaRadians.Value)
+ op2.R.Value * Math.Sin(op2.ThetaRadians.Value);
double newY = op1.R.Value * Math.Cos(op1.ThetaRadians.Value)
+ op2.R.Value * Math.Cos(op2.ThetaRadians.Value);
// Convert to (r, theta).
double newR = Math.Sqrt(newX * newX + newY * newY);
double newTheta = Math.Atan2(newX, newY) * 180.0 / Math.PI;
// Return result.
return new Vector(newR, newTheta);
}
catch
{
// Return “null” vector.
return new Vector(null, null);
}
}
public static Vector operator -(Vector op1)
{
return new Vector(-op1.R, op1.Theta);
}
public static Vector operator -(Vector op1, Vector op2)
{
return op1 + (-op2);
}
public override string ToString()
{
// Get string representation of coordinates.
string rString = R.HasValue ? R.ToString() : “null”;
string thetaString = Theta.HasValue ? Theta.ToString() : “null”;
// Return (r, theta) string.
return string.Format(“({0}, {1})”, rString, thetaString);
}
}


4. Modify the code in Program.csas follows:
class Program
{
static void Main(string[] args)
{
Vector v1 = GetVector(“vector1”);
Vector v2 = GetVector(“vector1”);
Console.WriteLine(“{0} + {1} = {2}”, v1, v2, v1 + v2);
Console.WriteLine(“{0} - {1} = {2}”, v1, v2, v1 - v2);
Console.ReadKey();
}
static Vector GetVector(string name)
{
Console.WriteLine(“Input {0} magnitude:”, name);
double? r = GetNullableDouble();
Console.WriteLine(“Input {0} angle (in degrees):”, name);
double? theta = GetNullableDouble();
return new Vector(r, theta);
}
static double? GetNullableDouble()
{
double? result;
string userInput = Console.ReadLine();
try
{
result = double.Parse(userInput);
}
catch
{
result = null;
}
return result;
}
}


5. Execute the application and enter values for two vectors. Sample output is shown in Figure 12 - 1 . Figure 12-1
6. Execute the application again, but this time skip at least one of the four values. Sample output is shown in Figure 12 - 2 . Figure 12-2

How It Works
This example created a class called Vector that represents a vector with polar coordinates (that is, with a magnitude and an angle), as shown in Figure 12 - 3. Figure 12-3

The coordinates r and theta are represented in code by the public fields Rand Theta, where Theta is expressed in degrees. ThetaRadis supplied to obtain the value of Thetain radians — this is necessary because the Mathclass uses radians in its static methods. Both Rand Theta are of type double? , so they can be null:
public class Vector
{
public double? R = null;
public double? Theta = null;
public double? ThetaRadians
{
get
{
// Convert degrees to radians.
return (Theta * Math.PI / 180.0);
}
}


The constructor for Vectornormalizes the initial values of Rand Thetaand then assigns the public fields:
public Vector(double? r, double? theta)
{
// Normalize.
if (r < 0)
{
r = -r;
theta += 180;
}
theta = theta % 360;
// Assign fields.
R = r;
Theta = theta;
}


The main functionality of the Vectorclass is to add and subtract vectors using operator overloading, which requires some fairly basic trigonometry not covered here. The important thing about the code is that if an exception is thrown when obtaining the Value property of Ror ThetaRadians— that is, if either is null— then a “ null ” vector is returned:
public static Vector operator +(Vector op1, Vector op2)
{
try
{
// Get (x, y) coordinates for new vector.
...
}
catch
{
// Return “null” vector.
return new Vector(null, null);
}
}


If either of the coordinates making up a vector is null, then the vector is invalid, which is signified here by a Vectorclass with nullvalues for both Rand Theta. The rest of the code in the Vector class overrides the other operators required to extend the addition functionality to include subtraction, and overrides ToString()to obtain a string representation of a Vectorobject.
The code in Program.cstests the Vectorclass by enabling the user to initialize two vectors, and then adds and subtracts them to and from one another. Should the user omit a value, it will be interpreted as null, and the rules mentioned previously apply.

The System.Collections.Generics Namespace
In practically every application used so far in this book, you have seen the following namespaces:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;


The Systemnamespace contains most of the basic types used in .NET applications. The System.Text namespace includes types relating to string processing and encoding. The System.Linqnamespace you ’ ll look at later in this book, from Chapter 26 onward. But what about System.Collections .Generic, and why is it included by default in console applications?
The answer is that this namespace contains generic types for dealing with collections, and it is likely to be used so often that it is configured with a using statement, ready for you to use without qualification.
As promised earlier in the chapter, you ’ ll now look at these types, which are guaranteed to make your life easier. They make it possible for you to create strongly typed collection classes with hardly any effort. The following table lists two types from the System.Collections.Generics namespace that are covered in this section. More of the types in this namespace are covered later in this chapter. This section also describes various interfaces and delegates used with these classes.

List < T >
Rather than derive a class from CollectionBase and implement the required methods as you did in the last chapter, it can be quicker and easier simply to use the List < T > generic collection type. An added bonus here is that many of the methods you ’ d normally have to implement, such as Add() , are implemented for you.
Creating a collection of type T objects requires the following code:
List < T > myCollection = new List < T > ();

That ’ s it. You don ’ t have to define any classes, implement any methods, or do anything else. You can also set a starting list of items in the collection by passing a List < T > object to the constructor. An object instantiated using this syntax supports the methods and properties shown in the following table (where the type supplied to the List < T > generic is T):
List < T > also has an Item property, enabling array - like access:
T itemAtIndex2 = myCollectionOfT[2];

This class supports several other methods, but that ’ s plenty to get you started. The following Try It Out demonstrates how to use Collection < T > in practice.

Try It Out : Using List < T >
1. Create a new console application called Ch12Ex02 and save it in the directory C:\BegVCSharp\Chapter12.
2. Right - click on the project name in the Solution Explorer window, and select the Add > Add Existing Item option.
3. Select the Animal.cs, Cow.cs, and Chicken.cs files from the C:\BegVCSharp\Chapter11\ Ch11Ex01\Ch11Ex01 directory and click Add.
4. Modify the namespace declaration in the three files you added as follows:
namespace Ch12Ex02
5. Modify Program.csas follows:
static void Main(string[] args)
{
List < Animal > animalCollection = new List < Animal > ();
animalCollection.Add(new Cow(“Jack”));
animalCollection.Add(new Chicken(“Vera”));
foreach (Animal myAnimal in animalCollection)
{
myAnimal.Feed();
}
Console.ReadKey();
}


6. Execute the application. The result is exactly the same as the result for Ch11Ex02 in the last chapter.

How It Works
There are only two differences between this example and Ch11Ex02. The first is that the line of code
Animals animalCollection = new Animals();

has been replaced with
List < Animal > animalCollection = new List < Animal > ();

The second, and more crucial, difference is that there is no longer an Animalscollection class in the project. All that hard work you did earlier to create this class was achieved in a single line of code by using a generic collection class.
An alternate way to get the same result is to leave the code in Program.cs as it was in the last chapter, and use the following definition of Animals:
public class Animals : List < Animal >
{
}


Doing this has the advantage that the code in Program.cs is slightly easier to read, plus you can add additional members to the Animalsclass as you see fit.
You may, of course, be wondering why you ’ d ever want to derive classes from CollectionBase, which is a good question. In fact, there aren ’ t many situations where you would. It ’ s certainly a good thing to know how things work internally because List < T > works in much the same way, but CollectionBase is basically there for backward compatibility. The only situation in which you might want to use CollectionBase is when you want much more control over the members exposed to users of the class. For example, if you wanted a collection class with an internal access modifier on its Add()method, then using CollectionBasemight be the best option.
You can also pass an initial capacity to use to the constructor of List < T > (as an int), or an initial list of items using an IEnumerable < T > interface. Classes supporting this interface include List < T > .

Sorting and Searching Generic Lists
Sorting a generic list is much the same as sorting any other list. The last chapter described how you can use the IComparerand IComparable interfaces to compare two objects and thereby sort a list of that type of object. The only difference here is that you can use the generic interfaces IComparer < T > and IComparable < T > , which expose slightly different, type - specific methods. The following table explains these differences:To sort a List < T > you can supply an IComparable < T > interface on the type to be sorted, or supply an IComparer < T > interface. Alternatively, you can supply a generic delegate as a sorting method. From the perspective of seeing how things are done, this is far more interesting because implementing the interfaces shown above is really no more effort than implementing their nongeneric cousins.
In general terms, all you need to sort a list is a method that compares two objects of type T; and to search, all you need is a method that checks an object of type Tto determine whether it meets certain criteria. It is a simple matter to define such methods, and to aid you there are two generic delegate types that you can use:
. Comparison < T > — A delegate type for a method used for sorting, with the following return type and parameters:
int method(T objectA, T objectB)
. Predicate < T > — A delegate type for a method used for searching, with the following return type and parameters:
bool method(T targetObject)
You can define any number of such methods, and use them to “ snap - in ” to the searching and sorting methods of List < T > . The next Try It Out illustrates this.

Try It Out : Sorting and Searching List < T >
1. Create a new console application called Ch12Ex03 and save it in the directory C:\BegVCSharp\Chapter12.
2. Right - click on the project name in the Solution Explorer window and select the Add > Add Existing Item option.
3. Select the Vector.cs file from the C:\BegVCSharp\Chapter12\Ch12Ex01\Ch12Ex01 directory and click Add.
4. Modify the namespace declaration in the file you added as follows:
namespace Ch12Ex03
5. Add a new class called Vectors.
6. Modify Vectors.csas follows:
public class Vectors : List < Vector >
{
public Vectors()
{
}
public Vectors(IEnumerable < Vector > initialItems)
{
foreach (Vector vector in initialItems)
{
Add(vector);
}
}
public string Sum()
{
StringBuilder sb = new StringBuilder();
Vector currentPoint = new Vector(0.0, 0.0);
sb.Append(“origin”);
foreach (Vector vector in this)
{
sb.AppendFormat(“ + {0}”, vector);
currentPoint += vector;
}
sb.AppendFormat(“ = {0}”, currentPoint);
return sb.ToString();
}
}


7. Add a new class called VectorDelegates.
8. Modify VectorDelegates.csas follows:
public static class VectorDelegates
{
public static int Compare(Vector x, Vector y)
{
if (x.R > y.R)
{
return 1;
}
else if (x.R < y.R)
{
return -1;
}
return 0;
}
public static bool TopRightQuadrant(Vector target)
{
if (target.Theta > = 0.0 & & target.Theta < = 90.0)
{
return true;
}
else
{
return false;
}
}
}


9. Modify Program.csas follows:
static void Main(string[] args)
{
Vectors route = new Vectors();
route.Add(new Vector(2.0, 90.0));
route.Add(new Vector(1.0, 180.0));
route.Add(new Vector(0.5, 45.0));
route.Add(new Vector(2.5, 315.0));
Console.WriteLine(route.Sum());
Comparison < Vector > sorter = new Comparison < Vector > (VectorDelegates.Compare);
route.Sort(sorter);
Console.WriteLine(route.Sum());
Predicate < Vector > searcher =
new Predicate < Vector > (VectorDelegates.TopRightQuadrant);
Vectors topRightQuadrantRoute = new Vectors(route.FindAll(searcher));
Console.WriteLine(topRightQuadrantRoute.Sum());
Console.ReadKey();
}


10. Execute the application. The result is shown in Figure 12 - 4. Figure 12-4

How It Works
In this example, you have created a collection class, Vectors, for the Vector class created in Ch12Ex01. You could just use a variable of type List < Vector > , but because you want additional functionality you use a new class, Vectors, and derive from List < Vector > , which enables you to add whatever additional members you want.
One member, Sum(), returns a string listing each vector in turn, along with the result of summing them all together (using the overloaded + operator from the original Vectorclass). Because each vector can be thought of as a direction and a distance, this effectively constitutes a route with an endpoint:
public string Sum()
{
StringBuilder sb = new StringBuilder();
Vector currentPoint = new Vector(0.0, 0.0);
sb.Append(“origin”);
foreach (Vector vector in this)
{
sb.AppendFormat(“ + {0}”, vector);
currentPoint += vector;
}
sb.AppendFormat(“ = {0}”, currentPoint);
return sb.ToString();
}


This method uses the handy StringBuilderclass, found in the System.Textnamespace, to build the response string. This class has members such as Append()and AppendFormat() (used here), which make it easy to assemble a string — the performance is better than concatenating individual strings. You use the ToString()method of this class to obtain the resultant string.
You also create two methods to be used as delegates, as static members of VectorDelegates. Compare()is used for comparison (sorting), and TopRightQuadrant() for searching. You ’ ll look at these as you review the code in Program.cs.
The code in Main()starts with the initialization of a Vectors collection, to which are added several Vectorobjects:
Vectors route = new Vectors();
route.Add(new Vector(2.0, 90.0));
route.Add(new Vector(1.0, 180.0));
route.Add(new Vector(0.5, 45.0));
route.Add(new Vector(2.5, 315.0));


The Vectors.Sum()method is used to write out the items in the collection as noted earlier, this time in their initial order:
Console.WriteLine(route.Sum());

Next, you create the first of your delegates, sorter. This delegate is of type Comparison < Vector > and, therefore, can be assigned a method with the following return type and parameters:
int method(Vector objectA, Vector objectB)

This matches VectorDelegates.Compare(), which is the method you assign to the delegate:
Comparison < Vector > sorter = new Comparison < Vector > (VectorDelegates.Compare);

Compare()compares the magnitudes of two vectors as follows:
public static int Compare(Vector x, Vector y)
{
if (x.R > y.R)
{
return 1;
}
else if (x.R < y.R)
{
return -1;
}
return 0;
}


This enables you to order the vectors by magnitude:
route.Sort(sorter);
Console.WriteLine(route.Sum());


The output of the application gives the result you ’ d expect — the result of the summation is the same because the endpoint of following the “ vector route ” is the same regardless of the order in which you carry out the individual steps.
Next, you obtain a subset of the vectors in the collection by searching. This uses VectorDelegates .TopRightQuadrant():
public static bool TopRightQuadrant(Vector target)
{
if (target.Theta > = 0.0 & & target.Theta < = 90.0)
{
return true;
}
else
{
return false;
}
}


This method returns trueif its Vector argument has a value of Theta between 0 and 90 degrees — that is, it points up and/or right in a diagram of the sort shown earlier.
In the Main()method, you use this method via a delegate of type Predicate < Vector > as follows:
Predicate < Vector > searcher =
new Predicate < Vector > (VectorDelegates.TopRightQuadrant);
Vectors topRightQuadrantRoute = new Vectors(route.FindAll(searcher));
Console.WriteLine(topRightQuadrantRoute.Sum());


This requires the constructor defined in Vectors:
public Vectors(IEnumerable < Vector > initialItems)
{
foreach (Vector vector in initialItems)
{
Add(vector);
}
}


Here, you initialize a new Vectorscollection using an interface of IEnumerable < Vector > , which is necessary because List < Vector > .FindAll() returns a List < Vector > instance, not a Vectors instance.
The result of the searching is that only a subset of Vector objects is returned, so (again, as you ’ d expect) the result of the summation is different. The use of these generic delegate types to sort and search generic collections can take a little while to get used to, but the result is code that is streamlined and efficient, and which has a highly logical structure. It is well worth investing the time to learn the techniques presented in this section.

Dictionary < K, V >
The Dictionary < K, V > type enables you to define a collection of key - value pairs. Unlike the other generic collection types you ’ ve looked at in this chapter, this class requires instantiating two types: the types for both the key and value that represent each item in the collection.
Once a Dictionary < K, V > object is instantiated, you can perform much the same operations on it as you can on a class that inherits from DictionaryBase, but with type - safe methods and properties already in place. You can, for example, add key - value pairs using a strongly typed Add()method:
Dictionary < string, int > things = new Dictionary < string, int > ();
things.Add(“Green Things”, 29);
things.Add(“Blue Things”, 94);
things.Add(“Yellow Things”, 34);
things.Add(“Red Things”, 52);
things.Add(“Brown Things”, 27);


You can iterate through keys and values in the collection by using the Keysand Valuesproperties:
foreach (string key in things.Keys)
{
Console.WriteLine(key);
}
foreach (int value in things.Values)
{
Console.WriteLine(value);
}


In addition, you can iterate through items in the collection by obtaining each as a KeyValuePair < K, V > instance, much like you can with the DictionaryEntryobjects shown in the last chapter:
foreach (KeyValuePair < string, int > thing in things)
{
Console.WriteLine(“{0} = {1}”, thing.Key, thing.Value);
}


One thing to note about Dictionary < K, V > is that the key for each item must be unique. Attempting to add an item with an identical key to one already added will cause an ArgumentExceptionexception to be thrown. Because of this, Dictionary < K, V > allows you to pass an IComparer < K > interface to its constructor. This may be necessary if you use your own classes as keys and they don ’ t support an IComparableor IComparable < K > interface, or should you want to compare objects using a nondefault process. For instance, in the preceding example shown, you could use a case - insensitive method to compare string keys:
Dictionary < string, int > things =
new Dictionary < string, int > (StringComparer.CurrentCultureIgnoreCase);


Now you ’ ll get an exception if you use keys such as this:
things.Add(“Green Things”, 29);
things.Add(“Green things”, 94);


You can also pass an initial capacity (with an int) or set of items (with an IDictionary < K,V > interface) to the constructor.

Modifying CardLib to Use a Generic Collection Class
One simple modification you can make to the CardLib project you ’ ve been building over recent chapters is to change the Cardscollection class to use a generic collection class, thus saving many lines of code. The required modification to the class definition for Cardsis as follows:
public class Cards : List < Card > , ICloneable
{
...
}


You can also remove all the methods of Cardsexcept Clone(), which is required for ICloneable , and CopyTo(), because the version of CopyTo()supplied by List < Card > works with an array of Card objects, not a Cardscollection. Clone()requires a minor modification because the List < T > class does not define a List property to use:
public object Clone()
{
Cards newCards = new Cards();
foreach (Card sourceCard in this)
{
newCards.Add(sourceCard.Clone() as Card);
}
return newCards;
}


Rather than show the code here for what is a very simple modification, the updated version of CardLib, called Ch12CardLib, is included in the downloadable code for this chapter, along with the client code from the last chapter.

Defining Generics
You ’ ve now learned enough about generics to create your own. You ’ ve seen plenty of code involving generic types and have had plenty of practice using generic syntax. In this section, you ’ ll look at defining the following:
. Generic classes
. Generic interfaces
. Generic methods
. Generic delegates

You ’ ll also look at the following more advanced techniques for dealing with the issues that come up when defining generic types:
. The default keyword
. Constraining types
. Inheriting from generic classes
. Generic operators

Defining Generic Classes
To create a generic class, merely include the angle bracket syntax in the class definition:
class MyGenericClass < T >
{
...
}


Here, T can be any identifier you like, following the usual C# naming rules, such as not starting with a number and so on. Typically, though, you can just use T. A generic class can have any number of types in its definition, separated by commas:
class MyGenericClass < T1, T2, T3 >
{
...
}


Once these types are defined, you can use them in the class definition just like any other type. You can use them as types for member variables, return types for members such as properties or methods, and parameter types for method arguments:
class MyGenericClass < T1, T2, T3 >
{
private T1 innerT1Object;
public MyGenericClass(T1 item)
{
innerT1Object = item;
}
public T1 InnerT1Object
{
get
{
return innerT1Object;
}
}
}


Here, an object of type T1 can be passed to the constructor, and read - only access is permitted to this object via the property InnerT1Object. Note that you can make practically no assumptions as to what the types supplied to the class are. The following code, for example, will not compile:
class MyGenericClass < T1, T2, T3 >
{
private T1 innerT1Object;
public MyGenericClass()
{
innerT1Object = new T1();
}
public T1 InnerT1Object
{
get
{
return innerT1Object;
}
}
}


Because you don ’ t know what T1 is, you can ’ t use any of its constructors — it might not even have any, or it may have no publicly accessible default constructor. Without more complicated code involving the techniques shown later in this section, you can pretty much make only the following assumption about T1: You can treat it as a type that either inherits from or can be boxed into System.Object.
Obviously, this means that you can ’ t really do anything very interesting with instances of this type, or any of the other types supplied to the generic class MyGenericClass. Without using reflection, which is an advanced technique used to examine types at runtime (and not covered in this chapter), you ’ re limited pretty much to code as complicated as the following:
public string GetAllTypesAsString()
{
return “T1 = “ + typeof(T1).ToString()
+ “, T2 = “ + typeof(T2).ToString()
+ “, T3 = “ + typeof(T3).ToString();
}


There is a bit more that you can do, particularly in terms of collections, because dealing with groups of objects is a pretty simple process and doesn ’ t need any assumptions about the object types — which is one good reason why the generic collection classes you ’ ve seen in this chapter exist.
Another limitation that you need to be aware of is that using the operator ==or !=is only permitted when comparing a value of a type supplied to a generic type to null. That is, the following code
works fine:
public bool Compare(T1 op1, T1 op2)
{
if (op1 != null & & op2 != null)
{
return true;
}
else
{
return false;
}
}


Here, if T1 is a value type, then it is always assumed to be non - null, so in the preceding code Compare will always return true. However, attempting to compare the two arguments op1and op2 fails to compile:
public bool Compare(T1 op1, T1 op2)
{
if (op1 == op2)
{
return true;
}
else
{
return false;
}
}


This is because this code assumes that T1 supports the == operator. In short, to do anything really interesting with generics, you need to know a bit more about the types used in the class.

The default Keyword
One of the most basic things you might want to know about types used to create generic class instances is whether they are reference types or value types. Without knowing this you can ’ t even assign null values with code such as this:
public MyGenericClass()
{
innerT1Object = null;
}


If T1is a value type, then innerT1Object can ’ t have the value null, so this code won ’ t compile. Luckily, this problem has been addressed, resulting in a new use for the default keyword (which you ’ ve seen being used in switch structures earlier in the book). This is used as follows:
public MyGenericClass()
{
innerT1Object = default(T1);
}


The result of this is that innerT1Objectis assigned a value of null if it is a reference type, or a default value if it is a value type. This default value is 0 for numeric types, while structs have each of their members initialized to 0 or null in the same way. The default keyword gets you a bit further in terms of doing a little more with the types you are forced to use, but to truly get ahead you need to constrain the types that are supplied.

Constraining Types
The types you have used with generic classes until now are known as unbounded types because no restrictions are placed on what they can be. By constraining types, it is possible to restrict the types that can be used to instantiate a generic class. There are a number of ways to do this. For example, it ’ s possible to restrict a type to one that inherits from a certain type. Referring back to the Animal, Cow , and Chicken classes used earlier, you could restrict a type to one that was or inherited from Animal, so this code would be fine:
MyGenericClass < Cow > = new MyGenericClass < Cow > ();

The following, however, would fail to compile:
MyGenericClass < string > = new MyGenericClass < string > ();

In your class definitions this is achieved using the wherekeyword:
class My GenericClass < T > where T : constraint
{
...
}


Here, constraint defines what the constraint is. You can supply a number of constraints in this way by separating them with commas:
class MyGenericClass < T > where T : constraint1, constraint2
{
...
}


You can define constraints on any or all of the types required by the generic class by using multiple wherestatements:
class MyGenericClass < T1, T2 > where T1 : constraint1 where T2 : constraint2
{
...
}


Any constraints that you use must appear after the inheritance specifiers:
class MyGenericClass < T1, T2 > : MyBaseClass, IMyInterface
where T1 : constraint1 where T2 : constraint2
{
...
}


The available constraints are shown in the following table:If new()is used as a constraint, it must be the last constraint specified for a type.
It is possible to use one type parameter as a constraint on another through the base - classconstraint as follows:
class MyGenericClass < T1, T2 > where T2 : T1
{
...
}


Here, T2must be the same type as T1 or inherit from T1. This is known as a naked type constraint, meaning that one generic type parameter is used as a constraint on another.
Circular type constraints, as shown here, are forbidden:
class MyGenericClass < T1, T2 > where T2 : T1 where T1 : T2
{
...
}


This code will not compile. In the following Try It Out, you ’ ll define and use a generic class that uses the Animalfamily of classes shown in earlier chapters.

Try It Out : Defining a Generic Class
1. Create a new console application called Ch12Ex04 and save it in the directory C:\BegVCSharp\Chapter12.
2. Right - click on the project name in the Solution Explorer window and select the Add > Add Existing Item option.
3. Select the Animal.cs, Cow.cs, and Chicken.cs files from the C:\BegVCSharp\Chapter12\ Ch12Ex02\Ch12Ex02 directory, and click Add.
4. Modify the namespace declaration in the file you have added as follows:
namespace Ch12Ex04

5. Modify Animal.csas follows:
public abstract class Animal
{
...
public abstract void MakeANoise();
}


6. Modify Chicken.csas follows:
public class Chicken : Animal
{
...
public override void MakeANoise()
{
Console.WriteLine(“{0} says ‘cluck!’”, name);
}
}


7. Modify Cow.csas follows:
public class Cow : Animal
{
...
public override void MakeANoise()
{
Console.WriteLine(“{0} says ‘moo!’”, name);
}
}


8. Add a new class called SuperCowand modify the code in SuperCow.csas follows:
public class SuperCow : Cow
{
public void Fly()
{
Console.WriteLine(“{0} is flying!”, name);
}
public SuperCow(string newName) : base(newName)
{
}
public override void MakeANoise()
{
Console.WriteLine(“{0} says ‘here I come to save the day!’”, name);
}
}


9. Add a new class called Farmand modify the code in Farm.csas follows:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Ch12Ex04
{
public class Farm < T > : IEnumerable < T >
where T : Animal
{
private List < T > animals = new List < T > ();
public List < T > Animals
{
get
{
return animals;
}
}
public IEnumerator < T > GetEnumerator()
{
return animals.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return animals.GetEnumerator();
}
public void MakeNoises()
{
foreach (T animal in animals)
{
animal.MakeANoise();
}
}
public void FeedTheAnimals()
{
foreach (T animal in animals)
{
animal.Feed();
}
}
public Farm < Cow > GetCows()
{
Farm < Cow > cowFarm = new Farm < Cow > ();
foreach (T animal in animals)
{
if (animal is Cow)
{
cowFarm.Animals.Add(animal as Cow);
}
}
return cowFarm;
}
}
}


10. Modify Program.csas follows:
static void Main(string[] args)
{
Farm < Animal > farm = new Farm < Animal > ();
farm.Animals.Add(new Cow(“Jack”));
farm.Animals.Add(new Chicken(“Vera”));
farm.Animals.Add(new Chicken(“Sally”));
farm.Animals.Add(new SuperCow(“Kevin”));
farm.MakeNoises();
Farm < Cow > dairyFarm = farm.GetCows();
dairyFarm.FeedTheAnimals();
foreach (Cow cow in dairyFarm)
{
if (cow is SuperCow)
{
(cow as SuperCow).Fly();
}
}
Console.ReadKey();
}


11. Execute the application. The result is shown in Figure 12 - 5. Figure 12-5

How It Works
In this example, you have created a generic class called Farm < T > , which, rather than inheriting from a generic list class, exposes a generic list class as a public property. The type of this list is determined by the type parameter Tthat is passed to Farm < T > and is constrained to be or inherit from Animal:
public class Farm < T > : IEnumerable < T >
where T : Animal
{
private List < T > animals = new List < T > ();
public List < T > Animals
{
get
{
return animals;
}
}


Farm < T > also implements IEnumerable < T > , where Tis passed into this generic interface and is therefore also constrained in the same way. You implement this interface to make it possible to iterate through the items contained in Farm < T > without needing to explicitly iterate over Farm < T > .Animals. This is simple to achieve: You simply return the enumerator exposed by Animals, which is a List < T > class that also implements IEnumerable < T > :
public IEnumerator < T > GetEnumerator()
{
return animals.GetEnumerator();
}


Because IEnumerable < T > inherits from IEnumerable, you also need to implement IEnumerable.GetEnumerator():
IEnumerator IEnumerable.GetEnumerator()
{
return animals.GetEnumerator();
}


Next, Farm < T > includes two methods that make use of methods of the abstract Animalclass:
public void MakeNoises()
{
foreach (T animal in animals)
{
animal.MakeANoise();
}
}
public void FeedTheAnimals()
{
foreach (T animal in animals)
{
animal.Feed();
}
}


Because Tis constrained to Animal, this code compiles fine — you are guaranteed to have access to these methods whatever Tactually is.
The next method, GetCows(), is more interesting. This method simply extracts all the items in the collection that are of type Cow (or that inherit from Cow, such as the new SuperCowclass):
public Farm < Cow > GetCows()
{
Farm < Cow > cowFarm = new Farm < Cow > ();
foreach (T animal in animals)
{
if (animal is Cow)
{
cowFarm.Animals.Add(animal as Cow);
}
}
return cowFarm;
}


What is interesting here is that this method seems a bit wasteful. If you wanted other methods of the same sort, such as GetChickens()and so on, you ’ d need to implement them explicitly too. In a system with many more types, you ’ d need many more methods. A far better solution here would be to use a generic method, which you ’ ll implement a little later in the chapter.
The client code in Program.cssimply tests the various methods of Farm and doesn ’ t really contain much you haven ’ t already seen, so there ’ s no need to examine this code in any greater detail — despite the flying cow.

Inheriting from Generic Classes
The Farm < T > class in the preceding example, as well as several other classes you ’ ve seen in this chapter, inherit from a generic type. In the case of Farm < T > , this type was an interface: IEnumerable < T > . Here, the constraint on Tsupplied by Farm < T > resulted in an additional constraint on Tused in IEnumerable < T > . This can be a useful technique for constraining otherwise unbounded types. However, some rules need to be followed.
First, you can ’ t “ unconstrain ” types that are constrained in a type you are inheriting from. In other words, a type T that is used in a type you are inheriting from must be constrained at least as much as it is in that type. For example, the following code is fine:
class SuperFarm < T > : Farm < T >
where T : SuperCow
{
}


This works because Tis constrained to Animalin Farm < T > , and constraining it to SuperCowis constraining T to a subset of these values. However, the following won ’ t compile:
class SuperFarm < T > : Farm < T >
where T : struct
{
}


Here, you can say definitively that the type Tsupplied to SuperFarm < T > cannot be converted into a T usable by Farm < T > , so the code won ’ t compile. Even situations where the constraint is a superset have the same problem:
class SuperFarm < T > : Farm < T >
where T : class
{
}


Even though types such as Animalwould be allowed by SuperFarm < T > , other types that satisfy the class constraint won ’ t be allowed in Farm < T > . Again, compilation will fail. This rule applies to all the constraint types shown earlier in this chapter.
Also note that if you inherit from a generic type, then you must supply all the required type information, either in the form of other generic type parameters, as shown above, or explicitly. This also applies to nongeneric classes that inherit from generic types, as you ’ ve seen elsewhere. Here ’ s an example:
public class Cards : List < Card > , ICloneable
{
}


This is fine, but attempting the following will fail:
public class Cards : List < T > , ICloneable
{
}


Here, no information is supplied for T, so no compilation is possible.
If you supply a parameter to a generic type, as in List < Card > above, then you can refer to the type as closed . Similarly, inheriting from List < T > is inheriting from an open generic type.

Generic Operators
Operator overrides are implemented in C# just like other methods and can be implemented in generic classes. For example, you could define the following implicit conversion operator in Farm < T > :
public static implicit operator List < Animal > (Farm < T > farm)
{
List < Animal > result = new List < Animal > ();
foreach (T animal in farm)
{
result.Add(animal);
}
return result;
}


This allows the Animalobjects in a Farm < T > to be accessed directly as a List < Animal > should you require it. This comes in handy if you want to add two Farm < T > instances together, such as with the following operators:
public static Farm < T > operator +(Farm < T > farm1, List < T > farm2)
{
Farm < T > result = new Farm < T > ();
foreach (T animal in farm1)
{
result.Animals.Add(animal);
}
foreach (T animal in farm2)
{
if (!result.Animals.Contains(animal))
{
result.Animals.Add(animal);
}
}
return result;
}
public static Farm < T > operator +(List < T > farm1, Farm < T > farm2)
{
return farm2 + farm1;
}


You could then add instances of Farm < Animal > and Farm < Cow > as follows:
Farm < Animal > newFarm = farm + dairyFarm;

In this code, dairyFarm(an instance of Farm < Cow > ) is implicitly converted into List < Animal > , which is usable by the overloaded + operator in Farm < T > .
You might think that this could be achieved simply by using the following:
public static Farm < T > operator +(Farm < T > farm1, Farm < T > farm2)
{
Farm < T > result = new Farm < T > ();
foreach (T animal in farm1)
{
result.Animals.Add(animal);
}
foreach (T animal in farm2)
{
if (!result.Animals.Contains(animal))
{
result.Animals.Add(animal);
}
}
return result;
}


However, because Farm < Cow > cannot be converted into Farm < Animal > , the summation will fail. To take this a step further, you could solve this using the following conversion operator:
public static implicit operator Farm < Animal > (Farm < T > farm)
{
Farm < Animal > result = new Farm < Animal > ();
foreach (T animal in farm)
{
result.Animals.Add(animal);
}
return result;
}


With this operator, instances of Farm < T > , such as Farm < Cow > , can be converted into instances of Farm < Animal > , solving the problem. You can use either of the methods shown, although the latter is preferable for its simplicity.

Generic Structs
You learned in earlier chapters that structs are essentially the same as classes, barring some minor differences and the fact that a struct is a value type, not a reference type. Because this is the case, generic structs can be created in the same way as generic classes, as shown here:
public struct MyStruct < T1, T2 >
{
public T1 item1;
public T2 item2;
}


Defining Generic Interfaces
You ’ ve now seen several generic interfaces in use — namely, those in the Systems.Collections.Genericnamespace such as IEnumerable < T > used in the last example. Defining a generic interface involves the same techniques as defining a generic class:
interface MyFarmingInterface < T >
where T : Animal
{
bool AttemptToBreed(T animal1, T animal2);
T OldestInHerd
{
get;
}
}


Here, the generic parameter T is used as the type of the two arguments of AttemptToBreed() and the type of the OldestInHerdproperty.
The same inheritance rules apply as for classes. If you inherit from a base generic interface, you must obey the rules, such as keeping the constraints of the base interface generic type parameters.

Defining Generic Methods
The last Try It Out used a method called GetCows(), and in the discussion of the example it was stated that you could make a more general form of this method using a generic method. In this section you ’ ll see how this is possible. A generic method is one in which the return and/or parameter types are determined by a generic type parameter or parameters:
public T GetDefault < T > ()
{
return default(T);
}


This trivial example uses the default keyword you looked at earlier in the chapter to return a default value for a type T. This method is called as follows:
int myDefaultInt = GetDefault < T > ();

The type parameter T is provided at the time the method is called.
This T is quite separate from the types used to supply generic type parameters to classes. In fact, generic methods can be implemented by nongeneric classes:
public class Defaulter
{
public T GetDefault < T > ()
{
return default(T);
}
}


If the class is generic, though, then you must use different identifiers for generic method types. The following code won ’ t compile:
public class Defaulter < T >
{
public T GetDefault < T > ()
{
return default(T);
}
}


The type T used by either the method or the class must be renamed.
Constraints can be used by generic method parameters in the same way that they are for classes, and in this case you can make use of any class type parameters:
public class Defaulter < T1 >
{
public T2 GetDefault < T2 > ()
where T2 : T1
{
return default(T2);
}
}


Here, the type T2 supplied to the method must be the same as, or inherit from, T1supplied to the class. This is a common way to constrain generic methods.
In the Farm < T > class shown earlier, you could include the following method (included, but commented out, in the downloadable code for Ch12Ex04):
public Farm < U > GetSpecies < U > () where U : T
{
Farm < U > speciesFarm = new Farm < U > ();
foreach (T animal in animals)
{
if (animal is U)
{
speciesFarm.Animals.Add(animal as U);
}
}
return speciesFarm;
}


This can replace GetCows()and any other methods of the same type. The generic type parameter used here, U, is constrained by T, which is in turn constrained by the Farm < T > class to Animal. This enables you to treat instances of Tas instances of Animal, should you wish to do so.
In the client code for Ch12Ex04, in Program.cs, using this new method requires one modification:
Farm < Cow > dairyFarm = farm.GetSpecies < Cow > ();

You could equally write:
Farm < Chicken > dairyFarm = farm.GetSpecies < Chicken > ();

or any other class that inherits from Animal.
Note here that having generic type parameters on a method changes the signature of the method. This means you can have several overloads of a method differing only in generic type parameters, as shown in this example:
public void ProcessT < T > (T op1)
{
...
}
public void ProcessT < T, U > (T op1)
{
...
}


Which method should be used is determined by the amount of generic type parameters specified when the method is called.

Defining Generic Delegates
The last generic type to consider is the generic delegate. You saw these in action earlier in the chapter when you learned how to sort and search generic lists. You used the Comparison < T > and Predicate < T > delegates, respectively, for this.
Chapter 7 described how to define delegates using the parameters and return type of a method, the delegate keyword, and a name for the delegate:
public delegate int MyDelegate(int op1, int op2);

To define a generic delegate, you simply declare and use one or more generic type parameters:
public delegate T1 MyDelegate < T1, T2 > (T2 op1, T2 op2) where T1 : T2;

As you can see, constraints can be applied here too, where the same rules apply to the constraints. You ’ ll learn a lot more about delegates in the next chapter, including how you can use them in a common C# programming technique — events.

Summary
In this chapter, you examined how to use generic types in C# and learned how to create your own generic types, including classes, interfaces, methods, and delegates. You also looked at how to use
structs, including how to create nullable types, and how to use the classes in the System.Collecitons .Genericnamespace.
Generics, as you saw, are an extremely powerful new technique in C#. You can use them to create classes that satisfy several purposes at the same time, and they can be used in a variety of situations. Even if you don ’ t have any reason to create your own generic types, you ’ re almost certain to use the generic collection classes repeatedly.
In the next chapter, you ’ ll continue your examination of the basic C# language by tying up a few loose ends and looking at events.

Exercises
1. Which of the following can be generic?
a. classes
b. methods
c. properties
d. operator overloads
e. structs
f. enumerations

2. Extend the Vectorclass in Ch12Ex01 such that the * operator returns the dot product of two vectors. The dot product of two vectors is defined as the product of their magnitudes multiplied by the cosine of the angle between them.

3. What is wrong with the following code? Fix it.
public class Instantiator < T >
{
public T instance;
public Instantiator()
{
instance = new T();
}
}


4. What is wrong with the following code? Fix it.
public class StringGetter < T >
{
public string GetString < T > (T item)
{
return item.ToString();
}
}


5. Create a generic class called ShortCollection < T > that implements IList < T > and consists of a collection of items with a maximum size. This maximum size should be an integer that can be supplied to the constructor of ShortCollection < T > or defaults to 10. The constructor should also be able to take an initial list of items via a List < T > parameter. The class should function exactly like Collection < T > but throw an exception of type IndexOutOfRangeExceptionif an attempt is made to add too many items to the collection, or if the List < T > passed to the constructor contains too many items.

Post a Comment

About This Blog

  © Blogger template Simple n' Sweet by Ourblogtemplates.com 2009

Back to TOP