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 the System.Collections.Generic namespace, a name- space that you've seen several times in your code since it is included by default in console applications. You haven't as yet used any of the types in this namespace, but that's about to change. In this section, you'll look 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 which gets round a minor issue with value types: nullable types.

Nullable Types

In earlier chapters, you saw that one of the ways value types (most of the basic types like int and double as well as all structs) differ from reference types (string and 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 think, 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. For example:

System.Nullable<int> nullableInt;

This code declares a variable called nullableInt, which can have any value that an int variable can, plus the value null. This allows you to write code such as:

nullableInt = null;

If nullableInt were an int type variable, then the preceding code wouldn't compile.

The preceding assignment is equivalent to:

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 see if they are null just like you test reference types:

if (nullableInt == null) {    ... }

Alternatively, you can use the HasValue property:

 if (nullableInt.HasValue) {    ... }

This wouldn't work for reference types, even one with a HasValue property of its own, since 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 reference type using the Value property. If HasValue is true, then you are guaranteed a non-null value for Value, but if HasValue is false, that is, null has been assigned to the variable, then accessing Value will result in an exception of type System.InvalidOperationException.

One point to note about nullable types is that they are so useful that they have resulted in a modification of C# syntax. Rather than using 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. For example:

int? op1 = 5; int? result = op1 * 2;

Note that here the result variable is also of type int?. The following code will not compile:

int? op1 = 5; int result = op1 * 2; 

In order to get this to work you must perform an explicit conversion:

int? op1 = 5; int result = (int)op1 * 2; 

This will work fine as long as op1 has a value — if it is null, then you will get an exception of type System.InvalidOperationException.

This leads you straight on to the next 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 will be null, which you can interpret as "unable to compute." For structs you can define your own operators to deal with this situation (as you will see 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.

op1

op2

op1 & op2

op1 | op2

true

true

true

true

true

false

false

true

true

null

null

true

false

true

false

true

false

false

false

false

false

null

false

null

null

true

null

true

null

false

false

null

null

null

null

null

The results of these operators are as you would expect — 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 to deal with nullable types, and to make it easier to deal with variables that can be null, you can use the ?? operator. This operator enables you to supply default values to use if a nullable type is null and is used as follows:

int? op1 = null; int result = op1 * 2 ?? 5; 

Since in this example op1 is null, op1 * 2 will also be null. However, the ?? operator detects this and assigns the value 5 to result. A very important point to note here is that no explicit conversion is required to put the result in the int type variable result. The ?? operator handles this conversion for you. You can, however, 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 a block of code in an if structure.

In the following Try It Out, you'll experiment with a nullable Vector type.

Try It Out – Nullable Types

image from book
  1. Create a new console application project called Ch12Ex01 in the directory C:\BegVCSharp\ Chapter12.

  2. Add a new class called Vector using the VS shortcut, in the file Vector.cs.

  3. Modify the code in Vector.cs as 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.cs as follows:

    class Program { public 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();    }     public 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); } public 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.

    image from book
    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.

    image from book
    Figure 12-2

How It Works

In this example, you have 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.

image from book Figure 12-3

The coordinates r and _ are represented in code by the public fields R and Theta, where Theta is expressed in degrees. ThetaRad is supplied to obtain the value of Theta in radians — necessary because the Math class uses radians in its static methods. Both R and 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 Vector normalizes the initial values of R and Theta 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 Vector class is to add and subtract vectors using operator overloading, which requires some fairly basic trigonometry, which I won't go into here. The important thing about the code is that if an exception is thrown when obtaining the Value property of R or ThetaRadians, that is, if either are null, 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 are null, the vector is invalid, which is signified here by a Vector class with null values for both R and Theta.

The rest of the code in the Vector class overrides the other operators required to extend the addition functionality to subtraction and overrides ToString() to obtain a string representation of a Vector object.

The code in Program.cs tests the Vector class by enabling the user to initialize two vectors, 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.

image from book

The System.Collections.Generics Namespace

In practically every application you've seen so far in this book you have seen the following namespaces:

using System; using System.Collections.Generic; using System.Text;

The System namespace contains most of the basic types used in .NET applications. the System.Text namespace includes types relating to string processing and encoding, 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 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, and I can guarantee that they will make your life easier. They make it possible for you to create strongly typed collection classes with hardly any effort. The following table describes the types you look at in this section. You see more of these types later in this chapter.

Type

Description

List<T>

Collection of type T objects

Dictionary<K, V>

Collection if items of type V, associated with keys of type K

You also see various interfaces and delegates used with these classes.

List<T>

Rather than deriving a class from CollectionBase and implementing the required methods as you did in the last chapter, it can be quicker and easier simply to use this generic collection type. One 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>();

And that's it. No defining classes, implementing methods, or 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 will support the methods and properties shown in the following table (where the type supplied to the List<T> generic is T).

Member

Description

int Count

Property giving the number of items in the collection.

void Add(T item)

Adds item to the collection.

void AddRange(IEnumerable<T>)

Adds multiple items to the collection.

IList<T> AsReadOnly()

Returns a read-only interface to the collection.

int Capacity

Gets or sets the number of items that the collection can contain.

void Clear()

Removes all items from the collection.

bool Contains(T item)

Determines if item is contained in the collection.

void CopyTo(T[] array, int index)

Copies the items in the collection into the array array, starting from index index in the array.

IEnumerator<T> GetEnumerator()

Obtains an IEnumerator<T> instance for iteration through the collection. Note that the interface returned is strongly typed to T, so no casting will be required in foreach loops.

int IndexOf(T item)

Obtains the index of item, or –1 if the item is not contained in the collection.

void Insert(int index, T item)

Inserts item into the collection at the specified index.

bool Remove(T item)

Removes the first occurrence of item from the collection and returns true. If item is not contained in the collection, returns false.

void RemoveAt(int index)

Removes the item at index index from the collection.

List<T> also has an Item property, allowing arraylike access such as:

 T itemAtIndex2 = myCollectionOfT[2]; 

There are also several other methods that this class supports, but the above is plenty to get you started.

In the following Try It Out, you'll see how to use Collection<T> in practice.

Try It Out – Using Collection<T>

image from book
  1. Create a new console application called Ch12Ex02 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 have added as follows:

     namespace Ch12Ex02 
  5. Modify Program.cs as 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 for Ch11Ex02 in the last chapter.

How It Works

There are only two differences between this example and Ch11Ex02. The first is that this 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 Animals collection class in the project. All that hard work you did earlier to create this class has been achieved in a single line of code, using a generic collection class.

An alternate way of getting the same result is to leave the code in Program.cs because 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 Animals class 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, I can't think of many situations where you would. It's certainly a good thing to know how things work internally, since List<T> works in much the same way, but really CollectionBase is there for backward compatibility. The only situation I can think of in which you might want to use CollectionBase is when you want much more control over the members exposed to users of the class. If you wanted a collection class with an internal access modifier on its Add() method, for example, then using CollectionBase might be the best option.

Note

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>.

image from book

Sorting and Searching Generic Lists

Sorting a generic list is much the same as sorting any other list. In the last chapter, you saw how you can use the IComparer and IComparable interfaces to compare two objects and, thus, 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.

Generic Method

Nongeneric Method

Difference

int IComparable<T>.CompareTo( T otherObj)

int IComparable. CompareTo( object otherObj)

Strongly typed in generic version

bool IComparable<T>.Equals( T otherObj)

N/A

Doesn't exist on nongeneric interface — can use inherited object.Equals() instead

int IComparer<T>.Compare( T objectA, T object B)

int IComparer.Compare( object objectA, object object B)

Strongly typed in generic version

bool IComparer<T>.Equals( T objectA, T object B)

N/A

Doesn't exist on nongeneric interface — can use inherited object.Equals() instead

int IComparer<T>.GetHashCode( T objectA)

N/A

Doesn't exist on nongeneric interface — can use inherited object .GetHashCode() instead

So, 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 point of view of seeing how things are done, this is far more interesting, since 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 for searching all you need is a method that checks an object of type T to see if it meets certain criteria. It is a simple matter to define such methods, and to aid you there are two generic delegates that you can use:

  • Comparison<T>: A delegate type for a method used for sorting, with the signature:

    int method(T objectA, T objectB)
  • Predicate<T>: A delegate type for a method used for searching, with the signature:

    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 following Try It Out illustrates this.

Try It Out – Sorting and Searching List<T>

image from book
  1. Create a new console application called Ch12Ex03 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 have added as follows:

     namespace Ch12Ex03 
  5. Add a new class called Vectors.

  6. Modify Vectors.cs as 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.cs as 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.cs as 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.

    image from book
    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 since you want additional functionality you use a new class, Vectors, and derive from List<Vector>, allowing 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 Vector class). Since each vector can be thought of as being 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 StringBuilder class, found in the System.Text namespace, to build the response string. This class has members such as Append() and AppendFormat() (used here), which make it easy to assemble a string — and 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 look through the code in Program.cs.

The code in Main() starts with the initialization of a Vectors collection, to which are added several Vector objects:

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 signature:

int method(Vector objectA, Vector objectB) 

This matched 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 allows 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, since the endpoint of following the "vector route" is the same whatever order you carry out the individual steps in.

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 true if 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 you saw earlier.

In main, 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 Vectors collection using an interface of IEnumerable<Vector>, necessary since 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.

image from book

Dictionary<K, V>

This type allows 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 two types to be instantiated, 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 using the Keys and Values properties:

foreach (string key in things.Keys) {    Console.WriteLine(key); }     foreach (int value in things.Values) {    Console.WriteLine(value); }

And you can iterate through items in the collection by obtaining each as a KeyValuePair<K, V> instance, much as with the DictionaryEntry objects you saw 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 ArgumentException exception 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 IComparable or IComparable<K> interface, or should you want to compare objects using a nondefault process. For example, in the example shown above 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:

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 up over recent chapters is to modify the Cards collection class to use a generic collection class, thus saving many lines of code. The required modification to the class definition for Cards is:

 public class Cards : List<Card>, ICloneable {    ... }

You can also remove all the methods of Cards apart from Clone(), which is required for ICloneable, and CopyTo(), since the version of CopyTo() supplied by List<Card> works with an array of Card objects, not a Cards collection.

Rather than showing 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.




Beginning Visual C# 2005
Beginning Visual C#supAND#174;/sup 2005
ISBN: B000N7ETVG
EAN: N/A
Year: 2005
Pages: 278

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net