Collections, Comparisons, and Conversions
>> Aug 20, 2009
You ’ ve covered all the basic OOP techniques in C# now, but there are some more advanced techniques that are worth becoming familiar with. In this chapter, you look at the following:
. Collections — Collections enable you to maintain groups of objects. Unlike arrays, which you ’ ve used in earlier chapters, collections can include more advanced functionality, such as controlling access to the objects they contain, searching and sorting, and so on. You ’ ll learn how to use and create collection classes and learn about some powerful techniques for getting the most out of them.
. Comparisons — When dealing with objects, you often want to make comparisons between them. This is especially important in collections, because it is how sorting is achieved. You ’ ll look at how to compare objects in a number of ways, including operator overloading, and how to use the IComparableand IComparerinterface to sort collections.
. Conversions — Earlier chapters showed how to cast objects from one type into another.
In this chapter, you ’ ll learn how to customize type conversions to suit your needs.
Collections
In Chapter 5 , you learned how you can use arrays to create variable types that contain a number of objects or values. Arrays, however, have their limitations. The biggest is that once arrays have been created, they have a fixed size, so you can ’ t add new items to the end of an existing array without creating a new one. This often means that the syntax used to manipulate arrays can become overly complicated. OOP techniques enable you to create classes that perform much of this manipulation internally, simplifying the code that uses lists of items or arrays.
Arrays in C# are implemented as instances of the System.Array class and are just one type of what are known as collection classes. Collection classes in general are used for maintaining lists of objects and may expose more functionality than simple arrays. Much of this functionality comes through implementing interfaces from the System.Collections namespace, thus standardizing collection syntax. This namespace also contains some other interesting things, such as classes that implement these interfaces in ways other than System.Array.
Because the collection ’ s functionality (including basic functions such as accessing collection items by using [index]syntax) is available through interfaces, you aren ’ t limited to using basic collection classes such as System.Array. Instead, you can create your own customized collection classes. These can be made more specific to the objects you wish to enumerate (that is, the objects you want to maintain collections of). One advantage of doing this, as you will see, is that custom collection classes can be strongly typed. That is, when you extract items from the collection, you don ’ t need to cast them into the correct type. Another advantage is the capability to expose specialized methods. For example, you can provide a quick way to obtain subsets of items. In the deck of cards example, you could add a method to obtain all Carditems of a particular suit.
Several interfaces in the System.Collections namespace provide basic collection functionality:
. IEnumerable— Provides the capability to loop through items in a collection .
. ICollection— Provides the capability to obtain the number of items in a collection and copy items into a simple array type (inherits from IEnumerable).
. IList— Provides a list of items for a collection along with the capabilities for accessing these items, and some other basic capabilities related to lists of items (inherits from IEnumerableand ICollection).
. IDictionary— Similar to IList, but provides a list of items accessible via a key value, rather than an index (inherits from IEnumerableand ICollection).
The System.Arrayclass implements IList, ICollection, and IEnumerable. However, it doesn ’ t support some of the more advanced features of IList, and it represents a list of items by using a fixed size.
Using Collections
One of the classes in the Systems.Collectionsnamespace, System.Collections.ArrayList , also implements IList, ICollection, and IEnumerable, but does so in a more sophisticated way than System.Array. Whereas arrays are fixed in size (you can ’ t add or remove elements), this class may be used to represent a variable - length list of items. To give you more of a feel for what is possible with such a highly advanced collection, the following Try It Out uses this class, as well as a simple array.
Try It Out - Arrays versus More Advanced Collections
1. Create a new console application called Ch11Ex01 and save it in the directory C:\BegVCSharp\Chapter11.
2. Add three new classes, Animal, Cow, and Chicken, to the project by right - clicking on the project in the Solution Explorer window and selecting Add Class for each.
3. Modify the code in Animal.csas follows:
namespace Ch11Ex01
{
public abstract class Animal
{
protected string name;
public string Name
{
get
{
return name;
}
set
{
name = value;
}
}
public Animal()
{
name = “The animal with no name”;
}
public Animal(string newName)
{
name = newName;
}
public void Feed()
{
Console.WriteLine(“{0} has been fed.”, name);
}
}
}
4. Modify the code in Cow.csas follows:
namespace Ch11Ex01
{
public class Cow : Animal
{
public void Milk()
{
Console.WriteLine(“{0} has been milked.”, name);
}
public Cow(string newName) : base(newName)
{
}
}
}
5. Modify the code in Chicken.csas follows:
namespace Ch11Ex01
{
public class Chicken : Animal
{
public void LayEgg()
{
Console.WriteLine(“{0} has laid an egg.”, name);
}
public Chicken(string newName) : base(newName)
{
}
}
}
6. Modify the code in Program.csas follows:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Ch11Ex01
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine(“Create an Array type collection of Animal “ +
“objects and use it:”);
Animal[] animalArray = new Animal[2];
Cow myCow1 = new Cow(“Deirdre”);
animalArray[0] = myCow1;
animalArray[1] = new Chicken(“Ken”);
foreach (Animal myAnimal in animalArray)
{
Console.WriteLine(“New {0} object added to Array collection, “ +
“Name = {1}”, myAnimal.ToString(), myAnimal.Name);
}
Console.WriteLine(“Array collection contains {0} objects.”,
animalArray.Length);
animalArray[0].Feed();
((Chicken)animalArray[1]).LayEgg();
Console.WriteLine();
Console.WriteLine(“Create an ArrayList type collection of Animal “ +
“objects and use it:”);
ArrayList animalArrayList = new ArrayList();
Cow myCow2 = new Cow(“Hayley”);
animalArrayList.Add(myCow2);
animalArrayList.Add(new Chicken(“Roy”));
foreach (Animal myAnimal in animalArrayList)
{
Console.WriteLine(“New {0} object added to ArrayList collection,” +
“ Name = {1}”, myAnimal.ToString(), myAnimal.Name);
}
Console.WriteLine(“ArrayList collection contains {0} objects.”,
animalArrayList.Count);
((Animal)animalArrayList[0]).Feed();
((Chicken)animalArrayList[1]).LayEgg();
Console.WriteLine();
Console.WriteLine(“Additional manipulation of ArrayList:”);
animalArrayList.RemoveAt(0);
((Animal)animalArrayList[0]).Feed();
animalArrayList.AddRange(animalArray);
((Chicken)animalArrayList[2]).LayEgg();
Console.WriteLine(“The animal called {0} is at index {1}.”,
myCow1.Name, animalArrayList.IndexOf(myCow1));
myCow1.Name = “Janice”;
Console.WriteLine(“The animal is now called {0}.”,
((Animal)animalArrayList[1]).Name);
Console.ReadKey();
}
}
}
7. Run the application. The result is shown in Figure 11 - 1.
Figure 11-1
How It Works
This example creates two collections of objects: the first uses the System.Arrayclass (that is, a simple array), and the second uses the System.Collections.ArrayList class. Both collections are of Animal objects, which are defined in Animal.cs. The Animal class is abstract, so it can ’ t be instantiated, although you can have items in your collection that are instances of the Cowand Chicken classes, which are derived from Animal. You achieve this by using polymorphism, discussed in Chapter 8 .
Once created in the Main()method in Class1.cs, these arrays are manipulated to show their characteristics and capabilities. Several of the operations performed apply to both Arrayand ArrayList collections, although their syntax differs slightly. Some, however, are only possible by using the more advanced ArrayListtype.
We ’ ll cover the similar operations first, comparing the code and results for both types of collection. First, collection creation. With simple arrays you must initialize the array with a fixed size in order to use it. You do this to an array called animalArray by using the standard syntax shown in Chapter 5 :
Animal[] animalArray = new Animal[2];
ArrayList collections, conversely, don ’ t need a size to be initialized, so you can create your list (called animalArrayList) as follows:
ArrayList animalArrayList = new ArrayList();
You can use two other constructors with this class. The first copies the contents of an existing collection to the new instance by specifying the existing collection as a parameter; the other sets the capacity of the collection, also via a parameter. This capacity, specified as an intvalue, sets the initial number of items that can be contained in the collection. This is not an absolute capacity, however, because it is doubled automatically if the number of items in the collection ever exceeds this value.
With arrays of reference types (such as the Animaland Animal - derived objects), simply initializing the array with a size doesn ’ t initialize the items it contains. To use a given entry, that entry needs to be initialized, which means that you need to assign initialized objects to the items:
Cow myCow1 = new Cow(“Deirdre”);
animalArray[0] = myCow1;
animalArray[1] = new Chicken(“Ken”);
The preceding code does this in two ways: once by assignment using an existing Cowobject, and once by assignment through the creation of a new Chicken object. The main difference here is that the former method creates a reference to the object in the array — a fact that you make use of later in the code.
With the ArrayList collection, there are no existing items, not even null - referenced ones. This means you can ’ t assign new instances to indices in the same way. Instead, you use the Add()method of the ArrayListobject to add new items:
Cow myCow2 = new Cow(“Hayley”);
animalArrayList.Add(myCow2);
animalArrayList.Add(new Chicken(“Roy”));
Apart from the slightly different syntax, you can add new or existing objects to the collection in the same way. Once you have added items in this way, you can overwrite them by using syntax identical to that for arrays:
animalArrayList[0] = new Cow(“Alma”);
You won ’ t do that in this example, though.
Chapter 5 showed how the foreach structure can be used to iterate through an array. This is possible because the System.Arrayclass implements the IEnumerableinterface, and the only method on this interface, GetEnumerator(), allows you to loop through items in the collection. You ’ ll look at this in more depth a little later in the chapter. In your code, you write out information about each Animal object in the array:
foreach (Animal myAnimal in animalArray)
{
Console.WriteLine(“New {0} object added to Array collection, “ +
“Name = {1}”, myAnimal.ToString(), myAnimal.Name);
}
The ArrayListobject you use also supports the IEnumerableinterface and can be used with foreach. In this case, the syntax is identical:
foreach (Animal myAnimal in animalArrayList)
{
Console.WriteLine(“New {0} object added to ArrayList collection, “ +
“Name = {1}”, myAnimal.ToString(), myAnimal.Name);
}
Next, you use the Length property of the array to output to the screen the number of items in the array:
Console.WriteLine(“Array collection contains {0} objects.”,
animalArray.Length);
You can achieve the same thing with the ArrayListcollection, except that you use the Count property that is part of the ICollectioninterface:
Console.WriteLine(“ArrayList collection contains {0} objects.”,
animalArrayList.Count);
Collections — whether simple arrays or more complex collections — aren ’ t very useful unless they provide access to the items that belong to them. Simple arrays are strongly typed — that is, they allow direct access to the type of the items they contain. This means you can call the methods of the item directly:
animalArray[0].Feed();
The type of the array is the abstract type Animal; therefore, you can ’ t call methods supplied by derived classes directly. Instead you must use casting:
((Chicken)animalArray[1]).LayEgg();
The ArrayListcollection is a collection of System.Objectobjects (you have assigned Animalobjects via polymorphism). This means that you must use casting for all items:
((Animal)animalArrayList[0]).Feed();
((Chicken)animalArrayList[1]).LayEgg();
The remainder of the code looks at some of the ArrayList collection ’ s capabilities that go beyond those of the Array collection. First, you can remove items by using the Remove()and RemoveAt()methods, part of the IListinterface implementation in the ArrayList class. These remove items from an array based on an item reference or index, respectively. This example uses the latter method to remove the list ’ s first item, the Cowobject with a Name property of Hayley:
animalArrayList.RemoveAt(0);
Alternatively, you could use
animalArrayList.Remove(myCow2);
because you already have a local reference to this object — you added an existing reference to the array via Add(), rather than create a new object. Either way, the only item left in the collection is the Chickenobject, which you access as follows:
((Animal)animalArrayList[0]).Feed();
Any modifications to items in the ArrayList object resulting in Nitems being left in the array will be executed in such a way as to maintain indices from 0to N - 1. For example, removing the item with the index 0 results in all other items being shifted one place in the array, so you access the Chickenobject with the index 0, not 1. You no longer have an item with an index of 1(because you only had two items in the first place), so an exception would be thrown if you tried the following:
((Animal)animalArrayList[1]).Feed();
ArrayListcollections enable you to add several items at once with the AddRange() method. This method accepts any object with the ICollectioninterface, which includes the animalArrayarray created earlier in the code:
animalArrayList.AddRange(animalArray);
To check that this works, you can attempt to access the third item in the collection, which will be the second item in animalArray:
((Chicken)animalArrayList[2]).LayEgg();
The AddRange() method isn ’ t part of any of the interfaces exposed by ArrayList. This method is specific to the ArrayListclass and demonstrates the fact that you can exhibit customized behavior in your collection classes, beyond what is required by the interfaces you have looked at. This class exposes other interesting methods too, such as InsertRange(), for inserting an array of objects at any point in the list, and methods for tasks such as sorting and reordering the array.
Finally, you make use of the fact that you can have multiple references to the same object. Using the IndexOf()method (part of the IListinterface), you can see not only that myCow1(an object originally added to animalArray) is now part of the animalArrayListcollection, but also its index:
Console.WriteLine(“The animal called {0} is at index {1}.”,
myCow1.Name, animalArrayList.IndexOf(myCow1));
As an extension of this, the next two lines of code rename the object via the object reference and display the new name via the collection reference:
myCow1.Name = “Janice”;
Console.WriteLine(“The animal is now called {0}.”,
((Animal)animalArrayList[1]).Name);
Defining Collections
Now that you know what is possible using more advanced collection classes, it ’ s time to learn how to create your own strongly typed collection. One way of doing this is to implement the required methods manually, but this can be a time - consuming and complex process. Alternatively, you can derive your collection from a class, such as System.Collections.CollectionBase, an abstract class that supplies much of the implementation of a collection for you. This option is strongly recommended.
The CollectionBaseclass exposes the interfaces IEnumerable, ICollection, and IListbut only provides some of the required implementation — notably, the Clear()and RemoveAt() methods of IListand the Count property of ICollection. You need to implement everything else yourself if you want the functionality provided.
To facilitate this, CollectionBase provides two protected properties that provide access to the stored objects themselves. You can use List, which gives you access to the items through an IListinterface, and InnerList, which is the ArrayList object used to store items.
For example, the basics of a collection class to store Animal objects could be defined as follows (you ’ ll see a fuller implementation shortly):
public class Animals : CollectionBase
{
public void Add(Animal newAnimal)
{
List.Add(newAnimal);
}
public void Remove(Animal oldAnimal)
{
List.Remove(oldAnimal);
}
public Animals()
{
}
}
Here, Add()and Remove()have been implemented as strongly typed methods that use the standard Add()method of the IListinterface used to access the items. The methods exposed will now only work with Animal classes or classes derived from Animal, unlike the ArrayListimplementations shown earlier, which work with any object.
The CollectionBaseclass enables you to use the foreachsyntax with your derived collections. For example, you can use code such as this:
Console.WriteLine(“Using custom collection class Animals:”);
Animals animalCollection = new Animals();
animalCollection.Add(new Cow(“Sarah”));
foreach (Animal myAnimal in animalCollection)
{
Console.WriteLine(“New {0} object added to custom collection, “ +
“Name = {1}”, myAnimal.ToString(), myAnimal.Name);
}
You can ’ t, however, do the following:
animalCollection[0].Feed();
To access items via their indices in this way, you need to use an indexer.
Indexers
An indexer is a special kind of property that you can add to a class to provide array - like access. In fact, you can provide more complex access via an indexer, because you can define and use complex parameter types with the square bracket syntax as you wish. Implementing a simple numeric index for items, however, is the most common usage.
You can add an indexer to the Animalscollection of Animalobjects as follows:
public class Animals : CollectionBase
{
...
public Animal this[int animalIndex]
{
get
{
return (Animal)List[animalIndex];
}
set
{
List[animalIndex] = value;
}
}
}
The this keyword is used along with parameters in square brackets, but otherwise this looks much like any other property. This syntax is logical, because you access the indexer by using the name of the object followed by the index parameter(s) in square brackets (for example, MyAnimals[0]).
This code uses an indexer on the List property (that is, on the IList interface that provides access to the ArrayListin CollectionBase that stores your items):
return (Animal)List[animalIndex];
Explicit casting is necessary here, as the IList.List property returns a System.Objectobject. The important thing to note here is that you define a type for this indexer. This is the type that will be obtained when you access an item by using this indexer. This means that you can write code such as
animalCollection[0].Feed();
rather than
((Animal)animalCollection[0]).Feed();
This is another handy feature of strongly typed custom collections. In the following Try It Out, you expand the last example properly to put this into action.
Try It Out : Implementing an Animals Collection
1. Create a new console application called Ch11Ex02 and save it in the directory C:\BegVCSharp\Chapter11.
2. Right - click on the project name in the Solution Explorer window and select Add Existing Item.
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 Ch11Ex02
5. Add a new class called Animals.
6. Modify the code in Animals.csas follows:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
#endregion
namespace Ch11Ex02
{
public class Animals : CollectionBase
{
public void Add(Animal newAnimal)
{
List.Add(newAnimal);
}
public void Remove(Animal newAnimal)
{
List.Remove(newAnimal);
}
public Animals()
{
}
public Animal this[int animalIndex]
{
get
{
return (Animal)List[animalIndex];
}
set
{
List[animalIndex] = value;
}
}
}
}
7. Modify Program.csas follows:
static void Main(string[] args)
{
Animals animalCollection = new Animals();
animalCollection.Add(new Cow(“Jack”));
animalCollection.Add(new Chicken(“Vera”));
foreach (Animal myAnimal in animalCollection)
{
myAnimal.Feed();
}
Console.ReadKey();
}
8. Execute the application. The result is shown in Figure 11 - 2.
Figure 11-2
How It Works
This example uses code detailed in the last section to implement a strongly typed collection of Animal objects in a class called Animals. The code in Main()simply instantiates an Animalsobject called animalCollection, adds two items (an instance of Cowand Chicken), and uses a foreachloop to call the Feed()method that both objects inherit from their base class, Animal.
Adding a Cards Collection to CardLib
In the last chapter, you created a class library project called Ch10CardLib that contained a Cardclass representing a playing card, and a Deck class representing a deck of cards — that is, a collection of Card classes. This collection was implemented as a simple array.
In this chapter, you ’ ll add a new class to this library, renamed Ch11CardLib. This new class, Cards , will be a custom collection of Card objects, giving you all the benefits described earlier in this chapter. Create a new class library called Ch11CardLib in the C:\BegVCSharp\Chapter11 directory, select the Project Add Existing Item menu item, select the Card.cs, Deck.cs, Suit.cs, and Rank.csfiles from the C:\BegVCSharp\Chapter10\Ch10CardLib\Ch10CardLib directory, and add the files to your project. As with the previous version of this project, introduced in Chapter 10 , these changes are presented without using the standard Try It Out format. Should you want to jump straight to the code, feel free to open the version of this project included in the downloadable code for this chapter.
Don ’t forget that when copying the source files from Ch10CardLibto Ch11CardLib, you must change the namespace declarations to refer to Ch11CardLib. This also applies to the Ch10CardClientconsole
application that you will use for testing.
The downloadable code for this chapter includes a project that contains all the code you need for the various expansions to Ch11CardLib. The code is divided into regions, and you can uncomment the section you want to experiment with.
If you decide to create this project yourself, add a new class called Cardsand modify the code in Cards.csas follows:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Ch11CardLib
{
public class Cards : CollectionBase
{
public void Add(Card newCard)
{
List.Add(newCard);
}
public void Remove(Card oldCard)
{
List.Remove(oldCard);
}
public Cards()
{
}
public Card this[int cardIndex]
{
get
{
return (Card)List[cardIndex];
}
set
{
List[cardIndex] = value;
}
}
// Utility method for copying card instances into another Cards
// instance - used in Deck.Shuffle(). This implementation assumes that
// source and target collections are the same size.
public void CopyTo(Cards targetCards)
{
for (int index = 0; index < this.Count; index++)
{
targetCards[index] = this[index];
}
}
// Check to see if the Cards collection contains a particular card.
// This calls the Contains method of the ArrayList for the collection,
// which you access through the InnerList property.
public bool Contains(Card card)
{
return InnerList.Contains(card);
}
}
}
Next, modify Deck.csto use this new collection, rather than an array:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Ch11CardLib
{
public class Deck
{
private Cards cards = new Cards();
public Deck()
{
// Line of code removed here
for (int suitVal = 0; suitVal < 4; suitVal++)
{
for (int rankVal = 1; rankVal < 14; rankVal++)
{
cards.Add(new Card((Suit)suitVal, (Rank)rankVal));
}
}
}
public Card GetCard(int cardNum)
{
if (cardNum > = 0 & & cardNum < = 51)
return cards[cardNum];
else
throw (new System.ArgumentOutOfRangeException(“cardNum”, cardNum,
“Value must be between 0 and 51.”));
}
public void Shuffle()
{
Cards newDeck = new Cards();
bool[] assigned = new bool[52];
Random sourceGen = new Random();
for (int i = 0; i < 52; i++)
{
int sourceCard = 0;
bool foundCard = false;
while (foundCard == false)
{
sourceCard = sourceGen.Next(52);
if (assigned[sourceCard] == false)
foundCard = true;
}
assigned[sourceCard] = true;
newDeck.Add(cards[sourceCard]);
}
newDeck.CopyTo(cards);
}
}
}
Not many changes are necessary here. Most of them involve changing the shuffling logic to allow for the fact that cards are added to the beginning of the new Cardscollection newDeck from a random index in cards, rather than to a random index in newDeck from a sequential position in cards.
The client console application for the Ch10CardLib solution, Ch10CardClient, may be used with this new library with the same result as before, as the method signatures of Deck are unchanged. Clients of this class library can now make use of the Cards collection class, however, rather than rely on arrays of Card objects — for example, to define hands of cards in a card game application.
Keyed Collections and IDictionary
Instead of the IListinterface, it is also possible for collections to implement the similar IDictionary interface, which allows items to be indexed via a key value (such as a string name), rather than an index. This is also achieved using an indexer, although here the indexer parameter used is a key associated with a stored item, rather than an int index, which can make the collection a lot more user- friendly.
As with indexed collections, there is a base class you can use to simplify implementation of the IDictionaryinterface: DictionaryBase. This class also implements IEnumerableand ICollection, providing the basic collection manipulation capabilities that are the same for any collection.
DictionaryBase, like CollectionBase, implements some (but not all) of the members obtained through its supported interfaces. Like CollectionBase, the Clearand Count members are implemented, although RemoveAt() isn ’ t because it ’ s a method on the IList interface and doesn ’ t appear on the IDictionaryinterface. IDictionary does, however, have a Remove()method, which is one of the methods you should implement in a custom collection class based on DictionaryBase.
The following code shows an alternative version of the Animals class, this time derived from DictionaryBase. Implementations are included for Add(), Remove(), and a key - accessed indexer:
public class Animals : DictionaryBase
{
public void Add(string newID, Animal newAnimal)
{
Dictionary.Add(newID, newAnimal);
}
public void Remove(string animalID)
{
Dictionary.Remove(animalID);
}
public Animals()
{
}
public Animal this[string animalID]
{
get
{
return (Animal)Dictionary[animalID];
}
set
{
Dictionary[animalID] = value;
}
}
}
The differences in these members are as follows:
. Add() — Takes two parameters, a key and a value, to store together. The dictionary collection has a member called Dictionary inherited from DictionaryBase, which is an IDictionaryinterface. This interface has its own Add()method, which takes two object parameters. Your implementation takes a string value as a key and an Animal object as the data to store alongside this key.
. Remove() — Takes a key parameter, rather than an object reference. The item with the key value specified is removed.
. Indexer — Uses a string key value, rather than an index, which is used to access the stored item via the Dictionary inherited member. Again, casting is necessary here.
One other difference between collections based on DictionaryBaseand collections based on CollectionBaseis that foreach works slightly differently. The collection from the last section allowed you to extract Animal objects directly from the collection. Using foreachwith the DictionaryBase derived class gives you DictionaryEntry structs, another type defined in the System.Collections namespace. To get to the Animalobjects themselves, you must use the Value member of this struct, or you can use the Key member of the struct to get the associated key. To get code equivalent to the earlier
foreach (Animal myAnimal in animalCollection)
{
Console.WriteLine(“New {0} object added to custom collection, “ +
“Name = {1}”, myAnimal.ToString(), myAnimal.Name);
}
you need the following:
foreach (DictionaryEntry myEntry in animalCollection)
{
Console.WriteLine(“New {0} object added to custom collection, “ +
“Name = {1}”, myEntry.Value.ToString(),
((Animal)myEntry.Value).Name);
}
It is possible to override this behavior so that you can access Animal objects directly through foreach. There are several ways to do this, the simplest being to implement an iterator.
Iterators
Earlier in this chapter, you saw that the IEnumerableinterface enables you to use foreachloops. It ’ s often beneficial to use your classes in foreachloops, not just collection classes such as those shown in previous sections.
However, overriding this behavior, or providing your own custom implementation of it, is not always simple. To illustrate this, it ’ s necessary to take a detailed look at foreachloops. The following steps show what actually happens in a foreach loop iterating through a collection called collectionObject:
1. collectionObject.GetEnumerator()is called, which returns an IEnumeratorreference. This method is available through implementation of the IEnumerableinterface, although this is optional.
2. The MoveNext()method of the returned IEnumeratorinterface is called.
3. If MoveNext() returns true, then the Current property of the IEnumeratorinterface is used to get a reference to an object, which is used in the foreachloop.
4. The preceding two steps repeat until MoveNext() returns false, at which point the loop terminates.
To enable this behavior in your classes, you must override several methods, keep track of indices, maintain the Current property, and so on. This can be a lot of work to achieve very little.
A simpler alternative is to use an iterator. Effectively, using iterators generates a lot of the code for you behind the scenes and hooks it all up correctly. Moreover, the syntax for using iterators is much easier to come to grips with.
A good definition of an iterator is a block of code that supplies all the values to be used in a foreach block in sequence. Typically, this block of code is a method, although you can also use property accessors and other blocks of code as iterators. To keep things simple, you ’ ll just look at methods here.
Whatever the block of code is, its return type is restricted. Perhaps contrary to expectations, this return type isn ’ t the same as the type of object being enumerated. For example, in a class that represents a collection of Animal objects, the return type of the iterator block can ’ t be Animal. Two possible return types are the interface types mentioned earlier, IEnumerableor IEnumerator. You use these types as follows:
. To iterate over a class, use a method called GetEnumerator()with a return type of IEnumerator.
. To iterate over a class member, such as a method, use IEnumerable.
Within an iterator block, you select the values to be used in the foreachloop by using the yield keyword. The syntax for doing this is as follows:
yield return value;
That information is all you need to build a very simple example, as follows:
public static IEnumerable SimpleList()
{
yield return “string 1”;
yield return “string 2”;
yield return “string 3”;
}
public static void Main(string[] args)
{
foreach (string item in SimpleList())
Console.WriteLine(item);
Console.ReadKey();
}
To test this code yourself, remember to add a usingstatement for the System.Collections namespace or fully qualify the System.Collections.IEnumerable interface. Alternately, you can find this code in the SimpleIterators project in the downloadable code for this chapter.
Here, the static method SimpleList()is the iterator block. Because it is a method, you use a return type of IEnumerable. SimpleList()uses the yield keyword to supply three values to the foreachblock that uses it, each of which is written to the screen. The result is shown in Figure 11 - 3.
Figure 11-3
Obviously, this iterator isn ’ t a particularly useful one, but it does show how this works in action and how simple the implementation can be. Looking at the code, you might wonder how the code knows to return string type items. In fact, it doesn ’ t; it returns object type values. As you know, objectis the base class for all types, so you can return anything from the yieldstatements.
However, the compiler is intelligent enough to enable you to interpret the returned values as whatever type you want in the context of the foreach loop. Here, the code asks for stringtype values, so that is what the values you get to work with are. Should you change one of the yield lines so that it returns, say, an integer, you would get a bad cast exception in the foreachloop.
Note one more thing about iterators. It is possible to interrupt the return of information to the foreach loop by using the following statement:
yield break;
When this statement is encountered in an iterator, the iterator processing terminates immediately, as does the foreachloop using it.
Now it ’ s time for a more complicated — and useful! — example. In this Try It Out, you ’ ll implement an iterator that obtains prime numbers.
Try It Out : Implementing an Iterator
1. Create a new console application called Ch11Ex03 and save it in the directory C:\BegVCSharp\Chapter11.
2. Add a new class called Primesand modify the code as follows:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Ch11Ex03
{
public class Primes
{
private long min;
private long max;
public Primes() : this(2, 100)
{
}
public Primes(long minimum, long maximum)
{
if (min < 2)
min = 2;
else
min = minimum;
max = maximum;
}
public IEnumerator GetEnumerator()
{
for (long possiblePrime = min; possiblePrime < = max; possiblePrime++)
{
bool isPrime = true;
for (long possibleFactor = 2; possibleFactor < =
(long)Math.Floor(Math.Sqrt(possiblePrime)); possibleFactor++)
{
long remainderAfterDivision = possiblePrime % possibleFactor;
if (remainderAfterDivision == 0)
{
isPrime = false;
break;
}
}
if (isPrime)
{
yield return possiblePrime;
}
}
}
}
}
3. Modify the code in Program.csas follows:
static void Main(string[] args)
{
Primes primesFrom2To1000 = new Primes(2, 1000);
foreach (long i in primesFrom2To1000)
Console.Write(“{0} “, i);
Console.ReadKey();
}
4. Execute the application. The result is shown in Figure 11 - 4.
Figure 11-4
How It Works
This example consists of a class that enables you to enumerate over a collection of prime numbers between an upper and lower limit. The class that encapsulates the prime numbers uses an iterator to provide this functionality.
The code for Primes starts off with the basics: two fields to hold the maximum and minimum values to search between, and constructors to set these values. Note that the minimum value is restricted — it can ’ t be less than 2. This makes sense, because 2 is the lowest prime number. The interesting code is all in the GetEnumerator()method. The method signature fulfils the rules for an iterator block in that it returns an IEnumeratortype:
public IEnumerator GetEnumerator()
{
To extract prime numbers between limits, you need to test each number in turn, so you start with a forloop:
for (long possiblePrime = min; possiblePrime < = max; possiblePrime++)
{
Because you don ’ t know whether a number is prime or not, you first assume that it is and then check to see if it isn ’ t. That means checking whether any number between 2 and the square root of the number to be tested is a factor. If this is true, then the number isn ’ t prime, so you move on to the next one. If the number is indeed prime, then you pass it to the foreachloop using yield:
bool isPrime = true;
for (long possibleFactor = 2; possibleFactor < =
(long)Math.Floor(Math.Sqrt(possiblePrime)); possibleFactor++)
{
long remainderAfterDivision = possiblePrime % possibleFactor;
if (remainderAfterDivision == 0)
{
isPrime = false;
break;
}
}
if (isPrime)
{
yield return possiblePrime;
}
}
}
An interesting fact reveals itself through this code if you set the minimum and maximum limits to very big numbers. When you execute the application, the results appear one at a time, with pauses in between, rather than all at once. This is evidence that the iterator code returns results one at a time, despite the fact that there is no obvious place where the code terminates between yieldcalls. Behind the scenes, calling yield does interrupt the code, which resumes when another value is requested — that is, when the foreachloop using the iterator begins a new cycle.
Iterators and Collections
Earlier you were promised an explanation of how iterators can be used to iterate over the objects stored in a dictionary - type collection without having to deal with DictionaryItemobjects. Recall the collection class Animals:
public class Animals : DictionaryBase
{
public void Add(string newID, Animal newAnimal)
{
Dictionary.Add(newID, newAnimal);
}
public void Remove(string animalID)
{
Dictionary.Remove(animalID);
}
public Animals()
{
}
public Animal this[string animalID]
{
get
{
return (Animal)Dictionary[animalID];
}
set
{
Dictionary[animalID] = value;
}
}
}
You can add this simple iterator to the code to get the desired behavior:
public new IEnumerator GetEnumerator()
{
foreach (object animal in Dictionary.Values)
yield return (Animal)animal;
}
Now you can use the following code to iterate through the Animalobjects in the collection:
foreach (Animal myAnimal in animalCollection)
{
Console.WriteLine(“New {0} object added to custom collection, “ +
“Name = {1}”, myAnimal.ToString(), myAnimal.Name);
}
In the downloadable code for this chapter you will find this code in the DictionaryAnimals project.
Deep Copying
Chapter 9 described how you can perform shallow copying with the System.Object.MemberwiseClone() protected method, by using a method like the GetCopy()one shown here:
public class Cloner
{
public int Val;
public Cloner(int newVal)
{
Val = newVal;
}
public object GetCopy()
{
return MemberwiseClone();
}
}
Suppose you have fields that are reference types, rather than value types (for example, objects):
public class Content
{
public int Val;
}
public class Cloner
{
public Content MyContent = new Content();
public Cloner(int newVal)
{
MyContent.Val = newVal;
}
public object GetCopy()
{
return MemberwiseClone();
}
}
In this case, the shallow copy obtained though GetCopy()has a field that refers to the same object as the original object. The following code, which uses this Clonerclass, illustrates the consequences of shallow copying reference types:
Cloner mySource = new Cloner(5);
Cloner myTarget = (Cloner)mySource.GetCopy();
Console.WriteLine(“myTarget.MyContent.Val = {0}”, myTarget.MyContent.Val);
mySource.MyContent.Val = 2;
Console.WriteLine(“myTarget.MyContent.Val = {0}”, myTarget.MyContent.Val);
The fourth line, which assigns a value to mySource.MyContent.Val, the Valpublic field of the MyContentpublic field of the original object, also changes the value of myTarget.MyContent.Val . This is because mySource.MyContent refers to the same object instance as myTarget.MyContent . The output of the preceding code is as follows:
myTarget.MyContent.Val = 5
myTarget.MyContent.Val = 2
To get around this, you need to perform a deep copy. You could just modify the GetCopy() method used previously to do this, but it is preferable to use the standard .NET Framework way of doing things: Implement the ICloneableinterface, which has the single method Clone(). This method takes no parameters and returns an object type result, giving it a signature identical to the GetCopy()method used earlier.
To modify the preceding classes, try using the following deep copy code:
public class Content
{
public int Val;
}
public class Cloner : ICloneable
{
public Content MyContent = new Content();
public Cloner(int newVal)
{
MyContent.Val = newVal;
}
public object Clone()
{
Cloner clonedCloner = new Cloner(MyContent.Val);
return clonedCloner;
}
}
This created a new Clonerobject by using the Valfield of the Contentobject contained in the original Clonerobject (MyContent). This field is a value type, so no deeper copying is necessary.
Using code similar to that just shown to test the shallow copy but using Clone() instead of GetCopy()gives you the following result:
myTarget.MyContent.Val = 5
myTarget.MyContent.Val = 5
This time, the contained objects are independent. Note that sometimes calls to Clone() are made recursively, in more complex object systems. For example, if the MyContentfield of the Clonerclass also required deep copying, then you might need the following:
public class Cloner : ICloneable
{
public Content MyContent = new Content();
...
public object Clone()
{
Cloner clonedCloner = new Cloner();
clonedCloner.MyContent = MyContent.Clone();
return clonedCloner;
}
}
You ’ re calling the default constructor here to simplify the syntax of creating a new Clonerobject. For this code to work, you would also need to implement ICloneableon the Contentclass.
Adding Deep Copying to CardLib
You can put this into practice by implementing the capability to copy Card, Cards, and Deckobjects by using the ICloneable interface. This might be useful in some card games, where you might not necessarily want two decks with references to the same set of Cardobjects, although you might conceivably want to set up one deck to have the same card order as another.
Implementing cloning functionality for the Card class in Ch11CardLib is simple, because shallow copying is sufficient ( Card only contains value - type data, in the form of fields). Just make the following changes to the class definition:
public class Card : ICloneable
{
public object Clone()
{
return MemberwiseClone();
}
Note that this implementation of ICloneable is just a shallow copy. There is no rule determining what should happen in the Clone()method, and this is sufficient for your purposes.
Next, implement ICloneableon the Cards collection class. This is slightly more complicated because it involves cloning every Cardobject in the original collection — so you need to make a deep copy:
public class Cards : CollectionBase, ICloneable
{
public object Clone()
{
Cards newCards = new Cards();
foreach (Card sourceCard in List)
{
newCards.Add(sourceCard.Clone() as Card);
}
return newCards;
}
Finally, implement ICloneableon the Deck class. Note a slight problem here: The Deckclass has no way to modify the cards it contains, short of shuffling them. There is no way, for example, to modify a Deck instance to have a given card order. To get around this, define a new private constructor for the Deckclass that allows a specific Cardscollection to be passed in when the Deckobject is instantiated. Here ’ s the code to implement cloning in this class:
public class Deck : ICloneable
{
public object Clone()
{
Deck newDeck = new Deck(cards.Clone() as Cards);
return newDeck;
}
private Deck(Cards newCards)
{
cards = newCards;
}
Again, you can test this out with some simple client code (as before, place this code within the Main()method of a client project for testing):
Deck deck1 = new Deck();
Deck deck2 = (Deck)deck1.Clone();
Console.WriteLine(“The first card in the original deck is: {0}”,
deck1.GetCard(0));
Console.WriteLine(“The first card in the cloned deck is: {0}”,
deck2.GetCard(0));
deck1.Shuffle();
Console.WriteLine(“Original deck shuffled.”);
Console.WriteLine(“The first card in the original deck is: {0}”,
deck1.GetCard(0));
Console.WriteLine(“The first card in the cloned deck is: {0}”,
deck2.GetCard(0));
Console.ReadKey();
The output will be something like what is shown in Figure 11 - 5.
Figure 11-5
Comparisons
This section covers two types of comparisons between objects:
. Type comparisons
. Value comparisons
Type comparisons — that is, determining what an object is, or what it inherits from — are important in all areas of C# programming. Often when you pass an object — to a method, for example — what happens next depends on what type the object is. You ’ ve seen this in passing in this and earlier chapters, but here you will see some more useful techniques.
Value comparisons are also something you ’ ve seen a lot of, at least with simple types. When it comes to comparing values of objects, things get a little more complicated. You have to define what is meant by a comparison for a start, and what operators such as > mean in the context of your classes. This is especially important in collections, for which you might want to sort objects according to some condition, perhaps alphabetically or according to a more complicated algorithm.
Type Comparison
When comparing objects, you often need to know their type, which may enable you to determine whether a value comparison is possible. In Chapter 9 you saw the GetType()method, which all classes inherit from System.Object, and how this method can be used in combination with the typeof()operator to determine (and take action depending on) object types:
if (myObj.GetType() == typeof(MyComplexClass))
{
// myObj is an instance of the class MyComplexClass.
}
You ’ ve also seen how the default implementation of ToString(), also inherited from System.Object, will get you a string representation of an object ’ s type. You can compare these strings too, although that ’ s a rather messy way to accomplish this.
This section demonstrates a handy shorthand way of doing things: the is operator. This allows for much more readable code and, as you will see, has the advantage of examining base classes. Before looking at the is operator, though, you need to be aware of what often happens behind the scenes when dealing with value types (as opposed to reference types): boxing and unboxing.
Boxing and Unboxing
In Chapter 8 , you learned the difference between reference types and value types, which was illustrated in Chapter 9 by comparing structs (which are value types) with classes (which are reference types). Boxing is the act of converting a value type into the System.Objecttype or to an interface type that is implemented by the value type. Unboxing is the opposite conversion.
For example, suppose you have the following struct type:
struct MyStruct
{
public int Val;
}
You can box a struct of this type by placing it into an object - type variable:
MyStruct valType1 = new MyStruct();
valType1.Val = 5;
object refType = valType1;
Here, you create a new variable ( valType1) of type MyStruct, assign a value to the Valmember of this struct, and then box it into an object - type variable ( refType).
The object created by boxing a variable in this way contains a reference to a copy of the value - type variable, not a reference to the original value - type variable. You can verify this by modifying the original struct ’ s contents and then unboxing the struct contained in the object into a new variable and examining its contents:
valType1.Val = 6;
MyStruct valType2 = (MyStruct)refType;
Console.WriteLine(“valType2.Val = {0}”, valType2.Val);
This code gives you the following output:
valType2.Val = 5
When you assign a reference type to an object, however, you get a different behavior. You can illustrate this by changing MyStruct into a class (ignoring the fact that the name of this class isn ’ t appropriate now):
class MyStruct
{
public int Val;
}
With no changes to the client code shown previously (again ignoring the misnamed variables), you get the following output:
valType2.Val = 6
You can also box value types into interface types, so long as they implement that interface. For example, suppose the MyStructtype implements the IMyInterfaceinterface as follows:
interface IMyInterface
{
}
struct MyStruct : IMyInterface
{
public int Val;
}
You can then box the struct into an IMyInterfacetype as follows:
MyStruct valType1 = new MyStruct();
IMyInterface refType = valType1;
You can unbox it by using the normal casting syntax:
MyStruct ValType2 = (MyStruct)refType;
As you can see from these examples, boxing is performed without your intervention — that is, you don ’ t have to write any code to make this possible. Unboxing a value requires an explicit conversion, however, and requires you to make a cast (boxing is implicit and doesn ’ t have this requirement).
You might be wondering why you would actually want to do this. There are actually two very good reasons why boxing is extremely useful. First, it enables you to use value types in collections (such as ArrayList) where the items are of type object. Second, it ’ s the internal mechanism that enables you to call objectmethods on value types, such as ints and structs.
As a final note, it is worth remarking that unboxing is necessary before access to the value type contents is possible.
The is Operator
Despite its name, the is operator isn ’ t a way to tell whether an object is a certain type. Instead, the is operator enables you to check whether an object either is or can be converted into a given type. If this is the case, then the operator evaluates to true.
Earlier examples showed a Cowand a Chicken class, both of which inherit from Animal. Using the is operator to compare objects with the Animal type will return true for objects of all three of these types, not just Animal. This is something you ’ d have a hard time achieving with the GetType() method and typeof()operator shown previously.
The isoperator has the following syntax:
The possible results of this expression are as follows:
. If < type > is a class type, then the result is true if < operand > is of that type, if it inherits from that type, or if it can be boxed into that type.
. If < type > is an interface type, then the result is true if < operand > is of that type or it is a type that implements the interface.
. If < type > is a value type, then the result is true if < operand > is of that type or it is a type that can be unboxed into that type.
The following Try It Out shows how this works in practice.
Try It Out - Using the is Operator
1. Create a new console application called Ch11Ex04 in the directory C:\BegVCSharp\Chapter11.
2. Modify the code in Program.csas follows:
namespace Ch11Ex04
{
class Checker
{
public void Check(object param1)
{
if (param1 is ClassA)
Console.WriteLine(“Variable can be converted to ClassA.”);
else
Console.WriteLine(“Variable can’t be converted to ClassA.”);
if (param1 is IMyInterface)
Console.WriteLine(“Variable can be converted to IMyInterface.”);
else
Console.WriteLine(“Variable can’t be converted to IMyInterface.”);
if (param1 is MyStruct)
Console.WriteLine(“Variable can be converted to MyStruct.”);
else
Console.WriteLine(“Variable can’t be converted to MyStruct.”);
}
}
interface IMyInterface
{
}
class ClassA : IMyInterface
{
}
class ClassB : IMyInterface
{
}
class ClassC
{
}
class ClassD : ClassA
{
}
struct MyStruct : IMyInterface
{
}
class Program
{
static void Main(string[] args)
{
Checker check = new Checker();
ClassA try1 = new ClassA();
ClassB try2 = new ClassB();
ClassC try3 = new ClassC();
ClassD try4 = new ClassD();
MyStruct try5 = new MyStruct();
object try6 = try5;
Console.WriteLine(“Analyzing ClassA type variable:”);
check.Check(try1);
Console.WriteLine(“\nAnalyzing ClassB type variable:”);
check.Check(try2);
Console.WriteLine(“\nAnalyzing ClassC type variable:”);
check.Check(try3);
Console.WriteLine(“\nAnalyzing ClassD type variable:”);
check.Check(try4);
Console.WriteLine(“\nAnalyzing MyStruct type variable:”);
check.Check(try5);
Console.WriteLine(“\nAnalyzing boxed MyStruct type variable:”);
check.Check(try6);
Console.ReadKey();
}
}
}
3. Execute the code. The result is shown in Figure 11 - 6.
Figure 11-6
How It Works
This example illustrates the various results possible when using the is operator. Three classes, an interface, and a structure are defined and used as parameters to a method of a class that uses the is operator to determine whether they can be converted into the ClassAtype, the interface type, and the struct type.
Only ClassAand ClassD (which inherits from ClassA) types are compatible with ClassA. Types that don ’ t inherit from a class are not compatible with that class.
The ClassA, ClassB, and MyStructtypes all implement IMyInterface, so these are all compatible with the IMyInterfacetype. ClassD inherits from ClassA, so that it too is compatible. Therefore, only ClassCis incompatible.
Finally, only variables of type MyStruct itself and boxed variables of that type are compatible with MyStruct, because you can ’ t convert reference types to value types (although, of course, you can unbox previously boxed variables).
Value Comparison
Consider two Person objects representing people, each with an integer Age property. You might want to compare them to see which person is older. You can simply use the following code:
if (person1.Age > person2.Age)
{
...
}
This works fine, but there are alternatives. You might prefer to use syntax such as the following:
if (person1 > person2)
{
...
}
This is possible using operator overloading, which you ’ ll look at in this section. This is a powerful technique, but it should be used judiciously. In the preceding code, it is not immediately obvious that ages are being compared — it could be height, weight, IQ, or just general “ greatness. ”
Another option is to use the IComparableand IComparerinterfaces, which enable you to define how objects will be compared to each other in a standard way. This technique is supported by the various collection classes in the .NET Framework, making it an excellent way to sort objects in a collection.
Operator Overloading
Operator overloading enables you to use standard operators, such as +, >, and so on, with classes that you design. This is called “ overloading ” because you are supplying your own implementations for these operators when used with specific parameter types, in much the same way that you overload methods by supplying different parameters for methods with the same name.
Operator overloading is useful because you can perform whatever processing you want in the implementation of the operator overload, which might not be as simple as, for example, +, meaning “ add these two operands together. ” Later, you ’ ll see a good example of this in a further upgrade of the CardLib library, whereby you ’ ll provide implementations for comparison operators that compare two cards to see which would beat the other in a trick (one round of card game play).
Because a trick in many card games depends on the suits of the cards involved, this isn ’ t as straightforward as comparing the numbers on the cards. If the second card laid down is a different suit from the first, then the first card wins regardless of its rank. You can implement this by considering the order of the two operands. You can also take a trump suit into account, where trumps beat other suits even if that isn ’ t the first suit laid down. This means that calculating that card1> card2is true(that is, card1will beat card2if card1 is laid down first), doesn ’ t necessarily imply that card2> card1is false. If neither card1nor card2 are trumps and they belong to different suits, then both these comparisons will be true.
To start with, though, here ’ s a look at the basic syntax for operator overloading. Operators may be overloaded by adding operator type members (which must be static) to a class. Some operators have multiple uses (such as -, which has unary and binary capabilities); therefore, you also specify how many operands you are dealing with and what the types of these operands are. In general, you will have operands that are the same type as the class in which the operator is defined, although it is possible to define operators that work on mixed types, as you will see shortly.
As an example, consider the simple type AddClass1, defined as follows:
public class AddClass1
{
public int val;
}
This is just a wrapper around an int value but it illustrates the principles. With this class, code such as the following will fail to compile:
AddClass1 op1 = new AddClass1();
op1.val = 5;
AddClass1 op2 = new AddClass1();
op2.val = 5;
AddClass1 op3 = op1 + op2;
The error you get informs you that the + operator cannot be applied to operands of the AddClass1type. This is because you haven ’ t defined an operation to perform yet. Code such as the following works, but it won ’ t give you the result you might want:
AddClass1 op1 = new AddClass1();
op1.val = 5;
AddClass1 op2 = new AddClass1();
op2.val = 5;
bool op3 = op1 == op2;
Here, op1and op2 are compared by using the == binary operator to determine whether they refer to the same object, and not to verify whether their values are equal. op3will be false in the preceding code, even though op1.valand op2.val are identical.
To overload the + operator, use the following code:
public class AddClass1
{
public int val;
public static AddClass1 operator +(AddClass1 op1, AddClass1 op2)
{
AddClass1 returnVal = new AddClass1();
returnVal.val = op1.val + op2.val;
return returnVal;
}
}
As you can see, operator overloads look much like standard staticmethod declarations, except that they use the keyword operator and the operator itself, rather than a method name. You can now successfully use the + operator with this class, as in the previous example:
AddClass1 op3 = op1 + op2;
Overloading all binary operators fits the same pattern. Unary operators look similar but have only one parameter:
public class AddClass1
{
public int val;
public static AddClass1 operator +(AddClass1 op1, AddClass1 op2)
{
AddClass1 returnVal = new AddClass1();
returnVal.val = op1.val + op2.val;
return returnVal;
}
public static AddClass1 operator -(AddClass1 op1)
{
AddClass1 returnVal = new AddClass1();
returnVal.val = -op1.val;
return returnVal;
}
}
Both these operators work on operands of the same type as the class and have return values that are also of that type. Consider, however, the following class definitions:
public class AddClass1
{
public int val;
public static AddClass3 operator +(AddClass1 op1, AddClass2 op2)
{
AddClass3 returnVal = new AddClass3();
returnVal.val = op1.val + op2.val;
return returnVal;
}
}
public class AddClass2
{
public int val;
}
public class AddClass3
{
public int val;
}
This will allow the following code:
AddClass1 op1 = new AddClass1();
op1.val = 5;
AddClass2 op2 = new AddClass2();
op2.val = 5;
AddClass3 op3 = op1 + op2;
When appropriate, you can mix types in this way. Note, however, that if you added the same operator to AddClass2, then the preceding code would fail, because it would be ambiguous as to which operator to use. You should, therefore, take care not to add operators with the same signature to more than one class.
In addition, if you mix types, then the operands must be supplied in the same order as the parameters to the operator overload. If you attempt to use your overloaded operator with the operands in the wrong order, then the operation will fail. For example, you can ’ t use the operator like
AddClass3 op3 = op2 + op1;
unless, of course, you supply another overload with the parameters reversed:
public static AddClass3 operator +(AddClass2 op1, AddClass1 op2)
{
AddClass3 returnVal = new AddClass3();
returnVal.val = op1.val + op2.val;
return returnVal;
}
The following operators can be overloaded:
. Unary operators: +, - , !, ~, ++, - - , true, false
. Binary operators : +, - , *, /, %, & , |, ^, << , >>
. Comparison operators : ==, !=, < , > , < =, > =
If you overload the trueand false operators, then you can use classes in Boolean expressions, such as if (op1) {}.
You can ’ t overload assignment operators, such as +=, but these operators use their simple counterparts, such as +, so you don ’ t have to worry about that. Overloading + means that +=will function as expected. The = operator can ’ t be overloaded because it has such a fundamental usage, but this operator is related to the user- defined conversion operators, which you ’ ll look at in the next section.
You also can ’ t overload & & and ||, but these operators use the &and |operators to perform their calculations, so overloading these is enough.
Some operators, such as < and >, must be overloaded in pairs. That is to say, you can ’ t overload < unless you also overload >. In many cases, you can simply call other operators from these to reduce the code required (and the errors that might occur), as shown in this example:
public class AddClass1
{
public int val;
public static bool operator > =(AddClass1 op1, AddClass1 op2)
{
return (op1.val > = op2.val);
}
public static bool operator < (AddClass1 op1, AddClass1 op2)
{
return !(op1 > = op2);
}
// Also need implementations for < = and > operators.
}
In more complex operator definitions, this can reduce the lines of code. It also means that you have less code to change if you decide to change the implementation of these operators.
The same applies to ==and !=, but with these operators it is often worth overriding Object .Equals()and Object.GetHashCode(), because both of these functions may also be used to compare objects. By overriding these methods, you ensure that whatever technique users of the class use, they get the same result. This isn ’ t essential, but is worth adding for completeness. It requires the following nonstatic override methods:
public class AddClass1
{
public int val;
public static bool operator ==(AddClass1 op1, AddClass1 op2)
{
return (op1.val == op2.val);
}
public static bool operator !=(AddClass1 op1, AddClass1 op2)
{
return !(op1 == op2);
}
public override bool Equals(object op1)
{
return val == ((AddClass1)op1).val;
}
public override int GetHashCode()
{
return val;
}
}
GetHashCode()is used to obtain a unique int value for an object instance based on its state. Here, using valis fine, because it is also an intvalue.
Note that Equals()uses an object type parameter. You need to use this signature or you will be overloading this method, rather than overriding it, and the default implementation will still be accessible to users of the class. Instead, you must use casting to get the required result. It is often worth checking the object type using the is operator discussed earlier, in code such as this:
public override bool Equals(object op1)
{
if (op1 is AddClass1)
{
return val == ((AddClass1)op1).val;
}
else
{
throw new ArgumentException(
“Cannot compare AddClass1 objects with objects of type “
+ op1.GetType().ToString());
}
}
In this code, an exception is thrown if the operand passed to Equals is of the wrong type or cannot be converted into the correct type. Of course, this behavior may not be what you want. You may want to be able to compare objects of one type with objects of another type, in which case more branching would be necessary. Alternatively, you may want to restrict comparisons to those in which both objects are of exactly the same type, which would require the following change to the first ifstatement:
if (op1.GetType() == typeof(AddClass1))
Adding Operator Overloads to CardLib
Now you ’ ll upgrade your Ch11CardLib project again, adding operator overloading to the card class. First, though, you ’ ll add the extra fields to the Card class that allow for trump suits and a choice to place Aces high. You make these static, because when they are set, they apply to all Cardobjects:
public class Card
{
// Flag for trump usage. If true, trumps are valued higher
// than cards of other suits.
public static bool useTrumps = false;
// Trump suit to use if useTrumps is true.
public static Suit trump = Suit.Club;
// Flag that determines whether aces are higher than kings or lower
// than deuces.
public static bool isAceHigh = true;
Note that these rules apply to all Cardobjects in every Deck in an application. It ’ s not possible to have two decks of cards with cards contained in each that obey different rules. This is fine for this class library, however, as you can safely assume that if a single application wants to use separate rules, then it could maintain these itself, perhaps setting the static members of Card whenever decks are switched.
Because you have done this, it is worth adding a few more constructors to the Deckclass to initialize decks with different characteristics:
public Deck()
{
for (int suitVal = 0; suitVal < 4; suitVal++)
{
for (int rankVal = 1; rankVal < 14; rankVal++)
{
cards.Add(new Card((Suit)suitVal, (Rank)rankVal));
}
}
}
// Nondefault constructor. Allows aces to be set high.
public Deck(bool isAceHigh) : this()
{
Card.isAceHigh = isAceHigh;
}
// Nondefault constructor. Allows a trump suit to be used.
public Deck(bool useTrumps, Suit trump) : this()
{
Card.useTrumps = useTrumps;
Card.trump = trump;
}
// Nondefault constructor. Allows aces to be set high and a trump suit
// to be used.
public Deck(bool isAceHigh, bool useTrumps, Suit trump) : this()
{
Card.isAceHigh = isAceHigh;
Card.useTrumps = useTrumps;
Card.trump = trump;
}
Each of these constructors is defined by using the : this()syntax shown in Chapter 9 , so in all cases, the default constructor is called before the nondefault one, initializing the deck.
Now add your operator overloads (and suggested overrides) to the Cardclass:
public Card(Suit newSuit, Rank newRank)
{
suit = newSuit;
rank = newRank;
}
public static bool operator ==(Card card1, Card card2)
{
return (card1.suit == card2.suit) & & (card1.rank == card2.rank);
}
public static bool operator !=(Card card1, Card card2)
{
return !(card1 == card2);
}
public override bool Equals(object card)
{
return this == (Card)card;
}
public override int GetHashCode()
{
return 13*(int)rank + (int)suit;
}
public static bool operator > (Card card1, Card card2)
{
if (card1.suit == card2.suit)
{
if (isAceHigh)
{
if (card1.rank == Rank.Ace)
{
if (card2.rank == Rank.Ace)
return false;
else
return true;
}
else
{
if (card2.rank == Rank.Ace)
return false;
else
return (card1.rank > card2.rank);
}
}
else
{
return (card1.rank > card2.rank);
}
}
else
{
if (useTrumps & & (card2.suit == Card.trump))
return false;
else
return true;
}
}
public static bool operator < (Card card1, Card card2)
{
return !(card1 > = card2);
}
public static bool operator > =(Card card1, Card card2)
{
if (card1.suit == card2.suit)
{
if (isAceHigh)
{
if (card1.rank == Rank.Ace)
{
return true;
}
else
{
if (card2.rank == Rank.Ace)
return false;
else
return (card1.rank > = card2.rank);
}
}
else
{
return (card1.rank > = card2.rank);
}
}
else
{
if (useTrumps & & (card2.suit == Card.trump))
return false;
else
return true;
}
}
public static bool operator < =(Card card1, Card card2)
{
return !(card1 > card2);
}
There ’ s not much to note here, except perhaps the slightly lengthy code for the >and > =overloaded operators. If you step through the code for >, you can see how it works and why these steps are necessary.
You are comparing two cards, card1and card2, where card1is assumed to be the first one laid down on the table. As discussed earlier, this becomes important when you are using trump cards, because a trump will beat a nontrump even if the nontrump has a higher rank. Of course, if the suits of the two cards are identical, then whether the suit is the trump suit or not is irrelevant, so this is the first comparison you make:
public static bool operator > (Card card1, Card card2)
{
if (card1.suit == card2.suit)
{
If the static isAceHigh flag is true, then you can ’ t compare the cards ’ ranks directly via their value in the Rankenumeration, because the rank of ace has a value of 1 in this enumeration, which is less than that of all other ranks. Instead, use the following steps:
. If the first card is an ace, then check whether the second card is also an ace. If it is, then the first card won ’ t beat the second. If the second card isn ’ t an ace, then the first card wins:
if (isAceHigh)
{
if (card1.rank == Rank.Ace)
{
if (card2.rank == Rank.Ace)
return false;
else
return true;
}
. If the first card isn ’ t an ace, then you also need to check whether the second one is. If it is, then the second card wins; otherwise, you can compare the rank values because you know that aces aren ’ t an issue:
else
{
if (card2.rank == Rank.Ace)
return false;
else
return (card1.rank > card2.rank);
}
}
. If aces aren ’ t high, then you can just compare the rank values:
else
{
return (card1.rank > card2.rank);
}
The remainder of the code concerns the case where the suits of card1and card2 are different. Here, the static useTrumpsflag is important. If this flag is trueand card2 is of the trump suit, then you can say definitively that card1 isn ’ t a trump (because the two cards have different suits), and trumps always win, so card2 is the higher card:
else
{
if (useTrumps & & (card2.suit == Card.trump))
return false;
If card2 isn ’ t a trump (or useTrumpsis false), then card1 wins, because it was the first card laid down:
else
return true;
}
}
Only one other operator (> =) uses code similar to this, and the other operators are very simple, so there ’ s no need to go into more detail about them.
The following simple client code tests these operators (place it in the Main()function of a client project to test it, like the client code shown earlier in the earlier CardLibexamples):
Card.isAceHigh = true;
Console.WriteLine(“Aces are high.”);
Card.useTrumps = true;
Card.trump = Suit.Club;
Console.WriteLine(“Clubs are trumps.”);
Card card1, card2, card3, card4, card5;
card1 = new Card(Suit.Club, Rank.Five);
card2 = new Card(Suit.Club, Rank.Five);
card3 = new Card(Suit.Club, Rank.Ace);
card4 = new Card(Suit.Heart, Rank.Ten);
card5 = new Card(Suit.Diamond, Rank.Ace);
Console.WriteLine(“{0} == {1} ? {2}”,
card1.ToString(), card2.ToString(), card1 == card2);
Console.WriteLine(“{0} != {1} ? {2}”,
card1.ToString(), card3.ToString(), card1 != card3);
Console.WriteLine(“{0}.Equals({1}) ? {2}”,
card1.ToString(), card4.ToString(), card1.Equals(card4));
Console.WriteLine(“Card.Equals({0}, {1}) ? {2}”,
card3.ToString(), card4.ToString(), Card.Equals(card3, card4));
Console.WriteLine(“{0} > {1} ? {2}”,
card1.ToString(), card2.ToString(), card1 > card2);
Console.WriteLine(“{0} < = {1} ? {2}”,
card1.ToString(), card3.ToString(), card1 < = card3);
Console.WriteLine(“{0} > {1} ? {2}”,
card1.ToString(), card4.ToString(), card1 > card4);
Console.WriteLine(“{0} > {1} ? {2}”,
card4.ToString(), card1.ToString(), card4 > card1);
Console.WriteLine(“{0} > {1} ? {2}”,
card5.ToString(), card4.ToString(), card5 > card4);
Console.WriteLine(“{0} > {1} ? {2}”,
card4.ToString(), card5.ToString(), card4 > card5);
Console.ReadKey();
The results are as shown in Figure 11 - 7.
Figure 11-7
In each case, the operators are applied taking the specified rules into account. This is particularly apparent in the last four lines of output, demonstrating how trump cards always beat nontrumps.
The IComparable and IComparer Interfaces
The IComparableand IComparer interfaces are the standard way to compare objects in the .NET Framework. The difference between the interfaces is as follows:
. IComparable is implemented in the class of the object to be compared and allows comparisons between that object and another object.
. ICompareris implemented in a separate class, which allows comparisons between any two objects.
Typically, you give a class default comparison code by using IComparable, and nondefault comparisons using other classes.
IComparableexposes the single method CompareTo(), which accepts an object. You could, for example, implement it in a way that enables you to pass a Personobject to it and determine whether that person is older or younger than the current person. In fact, this method returns an int, so you could also determine how much older or younger the second person is:
if (person1.CompareTo(person2) == 0)
{
Console.WriteLine(“Same age”);
}
else if (person1.CompareTo(person2) > 0)
{
Console.WriteLine(“person 1 is Older”);
}
else
{
Console.WriteLine(“person1 is Younger”);
}
IComparerexposes the single method Compare(), which accepts two objects and returns an integer result just like CompareTo(). With an object supporting IComparer, you could use code like the
following:
if (personComparer.Compare(person1, person2) == 0)
{
Console.WriteLine(“Same age”);
}
else if (personComparer.Compare(person1, person2) > 0)
{
Console.WriteLine(“person 1 is Older”);
}
else
{
Console.WriteLine(“person1 is Younger”);
}
In both cases, the parameters supplied to the methods are of the type System.Object. This means that you can compare one object to another object of any other type, so you usually have to perform some type comparison before returning a result, and maybe even throw exceptions if the wrong types are used.
The .NET Framework includes a default implementation of the IComparerinterface on a class called Comparer, found in the System.Collectionsnamespace. This class is capable of performing culture - specific comparisons between simple types, as well as any type that supports the IComparable interface. You can use it, for example, with the following code:
string firstString = “First String”;
string secondString = “Second String”;
Console.WriteLine(“Comparing ‘{0}’ and ‘{1}’, result: {2}”,
firstString, secondString,
Comparer.Default.Compare(firstString, secondString));
int firstNumber = 35;
int secondNumber = 23;
Console.WriteLine(“Comparing ‘{0}’ and ‘{1}’, result: {2}”,
firstNumber, secondNumber,
Comparer.Default.Compare(firstNumber, secondNumber));
This uses the Comparer.Defaultstatic member to obtain an instance of the Comparerclass, and then uses the Compare()method to compare first two strings, and then two integers.
The result is as follows:
Comparing ‘First String’ and ‘Second String’, result: -1
Comparing ‘35’ and ‘23’, result: 1
Because F comes before S in the alphabet, it is deemed “ less than ” S, so the result of the first comparison is - 1. Similarly, 35 is greater than 23, hence the result of 1. Note that the results do not indicate the magnitude of the difference.
When using Comparer, you must use types that can be compared. Attempting to compare firstString with firstNumber, for example, will generate an exception.
Note a few more points about the behavior of this class:
. Objects passed to Comparer.Compare()are checked to determine whether they support IComparable. If they do, then that implementation is used.
. Null values are allowed, and are interpreted as being “ less than ” any other object.
. Strings are processed according to the current culture. To process strings according to a different culture (or language), the Comparer class must be instantiated using its constructor, which enables you to pass a System.Globalization.CultureInfo object specifying the culture to use.
. Strings are processed in a case - sensitive way. To process them in a non - case - sensitive way, you need to use the CaseInsensitiveComparerclass, which otherwise works exactly the same.
Sorting Collections Using the IComparable and IComparer Interfaces
Many collection classes allow sorting, either by default comparisons between objects or by custom methods. ArrayListis one example. It contains the method Sort(), which can be used without parameters, in which case default comparisons are used, or it can be passed an IComparerinterface to use to compare pairs of objects.
When you have an ArrayList filled with simple types, such as integers or strings, the default comparer is fine. For your own classes, you must either implement IComparable in your class definition or create a separate class supporting IComparerto use for comparisons.
Note that some classes in the System.Collectionnamespace, including CollectionBase , don ’ t expose a method for sorting. If you want to sort a collection you have derived from this class, then you have to do a bit more work and sort the internal Listcollection yourself.
The following Try It Out shows how to use a default and nondefault comparer to sort a list.
Try It Out - Sorting a List
1. Create a new console application called Ch11Ex05 in the directory C:\BegVCSharp\Chapter11.
2. Add a new class called Person, and modify the code as follows:
namespace Ch11Ex05
{
class Person : IComparable
{
public string Name;
public int Age;
public Person(string name, int age)
{
Name = name;
Age = age;
}
public int CompareTo(object obj)
{
if (obj is Person)
{
Person otherPerson = obj as Person;
return this.Age - otherPerson.Age;
}
else
{
throw new ArgumentException(
“Object to compare to is not a Person object.”);
}
}
}
}
3. Add another new class called PersonComparerNameand modify the code as follows:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Ch11Ex05
{
public class PersonComparerName : IComparer
{
public static IComparer Default = new PersonComparerName();
public int Compare(object x, object y)
{
if (x is Person & & y is Person)
{
return Comparer.Default.Compare(
((Person)x).Name, ((Person)y).Name);
}
else
{
throw new ArgumentException(
“One or both objects to compare are not Person objects.”);
}
}
}
}
4. Modify the code in Program.csas follows:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Ch11Ex05
{
class Program
{
static void Main(string[] args)
{
ArrayList list = new ArrayList();
list.Add(new Person(“Jim”, 30));
list.Add(new Person(“Bob”, 25));
list.Add(new Person(“Bert”, 27));
list.Add(new Person(“Ernie”, 22));
Console.WriteLine(“Unsorted people:”);
for (int i = 0; i < list.Count; i++)
{
Console.WriteLine(“{0} ({1})”,
(list[i] as Person).Name, (list[i] as Person).Age);
}
Console.WriteLine();
Console.WriteLine(
“People sorted with default comparer (by age):”);
list.Sort();
for (int i = 0; i < list.Count; i++)
{
Console.WriteLine(“{0} ({1})”,
(list[i] as Person).Name, (list[i] as Person).Age);
}
Console.WriteLine();
Console.WriteLine(
“People sorted with nondefault comparer (by name):”);
list.Sort(PersonComparerName.Default);
for (int i = 0; i < list.Count; i++)
{
Console.WriteLine(“{0} ({1})”,
(list[i] as Person).Name, (list[i] as Person).Age);
}
Console.ReadKey();
}
}
}
5. Execute the code. The result is shown in Figure 11 - 8.
Figure 11-8
How It Works
An ArrayListcontaining Person objects is sorted in two different ways here. By calling the ArrayList.Sort()method with no parameters, the default comparison is used, which is the CompareTo()method in the Personclass (because this class implements IComparable):
public int CompareTo(object obj)
{
if (obj is Person)
{
Person otherPerson = obj as Person;
return this.Age - otherPerson.Age;
}
else
{
throw new ArgumentException(
“Object to compare to is not a Person object.”);
}
}
This method first checks whether its argument can be compared to a Personobject — that is, whether the object can be converted into a Person object. If there is a problem, then an exception is thrown. Otherwise, the Age properties of the two Person objects are compared.
Next, a nondefault comparison sort is performed using the PersonComparerNameclass, which implements IComparer. This class has a public staticfield for ease of use:
public static IComparer Default = new PersonComparerName();
This enables you to get an instance using PersonComparerName.Default, just like the Comparer class shown earlier. The CompareTo()method of this class is as follows:
public int Compare(object x, object y)
{
if (x is Person & & y is Person)
{
return Comparer.Default.Compare(
((Person)x).Name, ((Person)y).Name);
}
else
{
throw new ArgumentException(
“One or both objects to compare are not Person objects.”);
}
}
Again, arguments are first checked to determine whether they are Person objects. If they aren ’ t, then an exception is thrown. If they are, then the default Comparer object is used to compare the two string Namefields of the Personobjects.
Conversions
Thus far, you have used casting whenever you have needed to convert one type into another, but this isn ’ t the only way to do things. Just as an intcan be converted into a longor a doubleimplicitly as part of a calculation, you can define how classes you have created may be converted into other classes (either implicitly or explicitly). To do this, you overload conversion operators, much like other operators were overloaded earlier in this chapter. You ’ ll see how in the first part of this section. You ’ ll also see another useful operator, the as operator, which in general is preferable to casting when using reference types.
Overloading Conversion Operators
As well as overloading mathematical operators, as shown earlier, you can define both implicit and explicit conversions between types. This is necessary if you want to convert between types that aren ’ t related — if there is no inheritance relationship between them and no shared interfaces, for example.
Suppose you define an implicit conversion between ConvClass1and ConvClass2. This means that you can write code such as the following:
ConvClass1 op1 = new ConvClass1();
ConvClass2 op2 = op1;
Alternatively, you can define an explicit conversion:
ConvClass1 op1 = new ConvClass1();
ConvClass2 op2 = (ConvClass2)op1;
As an example, consider the following code:
public class ConvClass1
{
public int val;
public static implicit operator ConvClass2(ConvClass1 op1)
{
ConvClass2 returnVal = new ConvClass2();
returnVal.val = op1.val;
return returnVal;
}
}
public class ConvClass2
{
public double val;
public static explicit operator ConvClass1(ConvClass2 op1)
{
ConvClass1 returnVal = new ConvClass1();
checked {returnVal.val = (int)op1.val;};
return returnVal;
}
}
Here, ConvClass1contains an intvalue and ConvClass2contains a doublevalue. Because intvalues may be converted into double values implicitly, you can define an implicit conversion between ConvClass1and ConvClass2. The reverse is not true, however, and you should define the conversion operator between ConvClass2and ConvClass1as explicit.
You specify this using the implicitand explicit keywords as shown. With these classes, the following code is fine:
ConvClass1 op1 = new ConvClass1();
op1.val = 3;
ConvClass2 op2 = op1;
A conversion in the other direction, however, requires the following explicit casting conversion:
ConvClass2 op1 = new ConvClass2();
op1.val = 3e15;
ConvClass1 op2 = (ConvClass1)op1;
Because you have used the checked keyword in your explicit conversion, you will get an exception in the preceding code, as the val property of op1 is too large to fit into the val property of op2.
The as Operator
The as operator converts a type into a specified reference type, using the following syntax:
< operand > as < type >
This is only possible in certain circumstances:
. If < operand > is of type < type > .
. If < operand > can be implicitly converted to type < type > .
. If < operand > can be boxed into type < type > .
If no conversion from < operand > to < type > is possible, then the result of the expression will be null.
Note that conversion from a base class to a derived class is possible by using an explicit conversion, but it won ’ t always work. Consider the two classes ClassAand ClassD from an earlier example, where ClassD inherits from ClassA:
class ClassA : IMyInterface
{
}
class ClassD : ClassA
{
}
The following code uses the as operator to convert from a ClassA instance stored in obj1into the ClassDtype:
ClassA obj1 = new ClassA();
ClassD obj2 = obj1 as ClassD;
This will result in obj2being null.
However, it is possible to store ClassDinstances in ClassA- type variables by using polymorphism. The following code illustrates this and uses the as operator to convert from a ClassA - type variable containing a ClassD- type instance into the ClassDtype:
ClassD obj1 = new ClassD();
ClassA obj2 = obj1;
ClassD obj3 = obj2 as ClassD;
This time the result is that obj3 ends up containing a reference to the same object as obj1, not null.
This functionality makes the asoperator very useful, because the following code (which uses simple casting) results in an exception being thrown:
ClassA obj1 = new ClassA();
ClassD obj2 = (ClassD)obj1;
The as equivalent of this code results in a nullvalue being assigned to obj2— no exception is thrown. This means that code such as the following (using two of the classes developed earlier in this chapter, Animal and a class derived from Animalcalled Cow) is very common in C# applications:
public void MilkCow(Animal myAnimal)
{
Cow myCow = myAnimal as Cow;
if (myCow != null)
{
myCow.Milk();
}
else
{
Console.WriteLine(“{0} isn’t a cow, and so can’t be milked.”,
myAnimal.Name);
}
}
This is much simpler than checking for exceptions!
Summary
This chapter covered many of the techniques that you can use to make your OOP applications far more powerful — and more interesting. Although these techniques take a little effort to accomplish, they can make your classes much easier to work with and therefore simplify the task of writing the rest of the code.
Each of the topics covered has many uses. You ’ re likely to come across collections of one form or another in almost any application, and creating strongly typed collections can make your life much easier if you need to work with a group of objects of the same type. You also learned how you can add indexers and iterators to get easy access to objects within the collection.
Comparisons and conversions are another topic that crops up repeatedly. You learned how to perform various comparisons, and saw some of the underlying functionality of boxing and unboxing. You also learned how to overload operators for both comparisons and conversions, and how to link things together with list sorting.
The next chapter covers something entirely new — generics. These enable you to create classes that automatically customize themselves to work with dynamically chosen types. This is especially useful with collections, and you ’ ll see how a lot of the code in this chapter can be simplified dramatically using generic collections.
Exercises
1. Create a collection class called Peoplethat is a collection of the Person class shown below. The items in the collection should be accessible via a string indexer that is the name of the person, identical to the Person.Nameproperty:
public class Person
{
private string name;
private int age;
public string Name
{
get
{
return name;
}
set
{
name = value;
}
}
public int Age
{
get
{
return age;
}
set
{
age = value;
}
}
}
2. Extend the Person class from the preceding exercise so that the >, <, > =, and < = operators are overloaded, and compare the Age properties of Personinstances.
3. Add a GetOldestmethod to the People class that returns an array of Personobjects with the greatest Age property (one or more objects, as multiple items may have the same value for this property), using the overloaded operators defined previously.
4. Implement the ICloneableinterface on the People class to provide deep copying capability.
5. Add an iterator to the Peopleclass that enables you to get the ages of all members in a foreach loop as follows:
foreach (int age in myPeople.Ages)
{
// Display ages.
}