Collections


In Chapter 5, you saw how you can use arrays to create variable types that contain a number of objects or values. Arrays, however, have their limitations. The biggest of these is that once they 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 allow you to create classes that perform much of this manipulation internally, thus 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.

As the collection functionality (including basic functions such as accessing collection items 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. This means that 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 may provide a quick way to obtain subsets of items, such as all Card items of a particular suit.

There are a number of interfaces in the System.Collections namespace that provide basic collection functionality:

  • IEnumerable: Provides the capability to loop through items in a collection.

  • ICollection: Provides the ability to obtain the number of items in a collection and to 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 IEnumerable and ICollection).

  • IDictionary: Similar to IList, but provides a list of items accessible via a key value rather than an index (inherits from IEnumerable and ICollection).

The System.Array class implements IList, ICollection, and IEnumerable, but doesn't support some of the more advanced features of IList, and represents a list of items with a fixed size.

Using Collections

One of the classes in the Systems.Collections namespace, 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 provides an example that uses this class, as well as a simple array.

Try It Out – Arrays versus More Advanced Collections

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

    using System; using System.Collections; using System.Collections.Generic; 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.

    image from book
    Figure 11-1

How It Works

This example creates two collections of objects, the first using the System.Array class (that is, a simple array), and the second using 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 (through polymorphism, discussed in Chapter 8) have items in your collection that are instances of the Cow and Chicken classes, which are derived from Animal.

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 Array and ArrayList collections, although their syntax differs slightly. There are some, however, that are only possible using the more advanced ArrayList type.

I'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 using the standard syntax you saw in Chapter 5:

Animal[] animalArray = new Animal[2];

ArrayList collections, on the other hand, don't need a size to be initialized, so you can create your list (called animalArrayList) simply by using:

ArrayList animalArrayList = new ArrayList();

There are two other constructors you can use 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 int value, sets the initial number of items that can be contained in the collection. This is not an absolute capacity, however, because it will be doubled automatically if the number of items in the collection ever exceeds this value.

With arrays of reference types (such as the Animal and Animal-derived objects), simply initializing the array with a size doesn't initialize the items it contains. In order 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");

This code does this in two ways: once by assignment using an existing Cow object, and once by assignment through the creation of a new Chicken object. The main difference here is that the former method leaves you with 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 that you can't assign new instances to indices in the same way. Instead, you use the Add() method of the ArrayList object 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 using syntax identical to that for arrays, for example:

 animalArrayList[0] = new Cow("Alma"); 

You won't do this in this example though.

In Chapter 5, you saw how the foreach structure can be used to iterate through an array. This is possible as the System.Array class implements the IEnumerable interface, 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 ArrayList object you use also supports the IEnumerable interface and can also 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 ArrayList collection, except that you use the Count property that is part of the ICollection interface:

Console.WriteLine("ArrayList collection contains {0} objects.",                   animalArrayList.Count);

Collections — whether simple arrays or more complex collections — wouldn't be much use unless they provided 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 that 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 ArrayList collection is a collection of System.Object objects (you have assigned Animal objects 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 capabilities of the ArrayList collection that go beyond those of the Array collection.

First, you can remove items using the Remove() and RemoveAt() methods, part of the IList interface implementation in the ArrayList class. These remove items from an array based on an item reference or index, respectively. In this example, you use the latter method to remove the first item added to the list, the Cow object 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 creating a new object.

Either way, the only item left in the collection is the Chicken object, which you access in the following way:

((Animal)animalArrayList[0]).Feed();

Any modifications to the items in the ArrayList object resulting in N items being left in the array will be executed in such a way as to maintain indices from 0 to 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 Chicken object with the index 0, not 1. There is no longer 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(); 

ArrayList collections allow you to add several items at once with the AddRange() method. This method accepts any object with the ICollection interface, which includes the animalArray array you 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 ArrayList class and demonstrates the fact that you can exhibit customized behavior in your collection classes, above and 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 IList interface), you can see not only that myCow1 (an object originally added to animalArray) is now part of the animalArrayList collection but also what its index is:

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);
image from book

Defining Collections

Now that you've seen what is possible using more advanced collection classes, it's time to look at how you can create your own strongly typed collection. One way of doing this is to implement the required methods manually, but this can be quite time-consuming, and in some cases quite complex. 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. I strongly recommend this option.

The CollectionBase class exposes the interfaces IEnumerable, ICollection, and IList but only provides some of the required implementation, notably the Clear() and RemoveAt() methods of IList, and 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 give access to the stored objects themselves. You can use List, which gives you access to the items through an IList interface, 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 IList interface used to access the items. The methods exposed will now only work with Animal classes or classes derived from Animal, unlike the ArrayList implementations you saw earlier, which work with any object.

The CollectionBase class allows you to use the foreach syntax with your derived collections. You can, for example, use code, such as:

 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(); 

In order 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 Animals collection of Animal objects 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'll access the indexer 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 gives you access to the ArrayList in CollectionBase that stores your items):

return (Animal)List[animalIndex];

Explicit casting is necessary here, as the IList.List property returns a System.Object object.

The important thing to note here is that you define a type for this indexer. This is the type that will be obtained when accessing an item 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

image from book
  1. Create a new console application called Ch11Ex02 in the directory C:\BegVCSharp\ Chapter11.

  2. Right-click on the project name in the Solution Explorer window, and select the 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 Ch11Ex02 
  5. Add a new class called Animals.

  6. Modify the code in Animals.cs as follows:

    using System; using System.Collections; using System.Collections.Generic; using System.Text;     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.cs as 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.

    image from book
    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 Animals object called animalCollection, adds two items (an instance each of Cow and Chicken), and uses a foreach loop to call the Feed() method that both these objects inherit from their base class Animal.

image from book

Adding a Cards Collection to CardLib

In the last chapter, you created a class library project called Ch10CardLib that contained a Card class 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, which you'll rename Ch11CardLib. This new class, Cards, will be a custom collection of Card objects, giving you all the benefits described earlier in this chapter. You may find it easier to create a new class library called Ch11CardLib in the C:\BegVCSharp\ Chapter11 directory, and from Project Add Existing Item..., select the Card.cs, Deck.cs, Suit.cs, and Rank.cs files from the C:\BegVCSharp\Chapter10\ Ch10CardLib\Ch10CardLib directory and add them to your project. As with the previous version of this project, introduced in Chapter 10, I'll present these changes without resorting to the standard Try It Out format. Should you wish to jump straight to the code feel free to open the version of this project included in the downloadable code for this chapter.

Note

Don't forget that when copying the source files from Ch10CardLib to Ch11CardLib, you must change the namespace declarations to refer to Ch11CardLib. This also applies to the Ch10CardClient console 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.

The code for your new class, in Cards.cs, is as follows (where code that is modified from that generated by the wizard is highlighted):

using System; using System.Collections; using System.Collections.Generic; 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, you need to modify Deck.cs to make use of this new collection, rather than an array:

using System;     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);       }    } }

There aren't that many changes necessary here. Most of those involve changing the shuffling logic to allow for the fact that cards are added to the beginning of the new Cards collection 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 relying on arrays of Card objects, for example in defining hands of cards in a card game application.

Keyed Collections and IDictionary

Instead of the IList interface, 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 by an index.

This is also achieved by using an indexer, although this time 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 that you can use to simplify implementation of the IDictionary interface: DictionaryBase. This class also implements IEnumerable and 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 Clear() and Count members are implemented, although RemoveAt() isn't. This is so because RemoveAt() is a method on the IList interface and doesn't appear on the IDictionary interface. 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 from the last section, 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:

  • 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 IDictionary interface. 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 DictionaryBase and collections based on CollectionBase is that foreach works slightly differently. The collection from the last section allowed you to extract Animal objects directly from the collection. Using foreach with the DictionaryBase derived class gives you DictionaryEntry structs, another type defined in the System.Collections namespace. To get to the Animal objects 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 get at Animal objects directly through foreach. There are a number of ways of doing this, the simplest being to implement an iterator. You'll see what iterators are, and how they can solve this problem, in the next section.

Iterators

Earlier in this chapter, you saw that the IEnumerable interface is responsible for allowing you to use foreach loops. Often, you will find it a great benefit to use your classes in foreach loops and not just collection classes such as those examined in previous sections of this chapter.

However, overriding this behavior, or providing your own custom implementation of it, is not necessarily a simple thing to do. To illustrate this, it's time to put on your best gloves and get under the hood of foreach loops. What actually happens in a foreach loop iterating through a collection called collectionObject is:

  1. collectionObject.GetEnumerator() is called, which returns an IEnumerator reference. This method is available through implementation of the IEnumerable interface, although this is optional.

  2. The MoveNext() method of the returned IEnumerator interface is called.

  3. If MoveNext() returns true then the Current property of the IEnumerator interface is used to get a reference to an object, which is used in the foreach loop.

  4. The preceding two steps repeat until MoveNext() returns false, at which point the loop terminates.

So, to allow this to happen in your classes, you have to 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 will generate a lot of the code for you behind the scenes and hook it all up correctly. And the syntax for using iterators is much easier to come to grips with.

A good definition of an iterator is that it is a block of code that supplies all the values to be used in a foreach block in sequence. Typically, this block of code will be a method, although it is also possible to use property accessors and other blocks of code as iterators. However, to keep things simple here you'll just look at methods.

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, IEnumerable or IEnumerator. You use these types:

  • If you want to iterate over a class, use a method called GetEnumerator() with a return type of IEnumerator.

  • If you want 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 foreach loop using the yield keyword. The syntax for doing this is:

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(); } 

Here, the static method SimpleList() is the iterator block. It is a method, so you use a return type of IEnumerable. SimpleList() uses the yield keyword to supply three values to the foreach block that uses it, each of which is written to the screen. The result is shown in Figure 11-3.

image from book
Figure 11-3

Obviously, this iterator isn't a particularly useful one, but it does enable you to see things in action and to see how simple the implementation can be. Looking at the code, you might be wondering how the code knows to return string type items. In fact, it doesn't; it returns object type values. Since, as you know, object is the base class for all types this means that you can return anything from the yield statements.

However, the compiler is intelligent enough so that you can interpret the returned values as whatever type you want in the context of the foreach loop. Here, the code asks for string type 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'll get a bad cast exception in the foreach loop.

There's one more thing to note about iterators. It is possible to interrupt the return of information to the foreach loop using the following statement:

yield break;

When this statement is encountered in an iterator, the iterator processing terminates immediately, as does the foreach loop 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

image from book
  1. Create a new console application called Ch11Ex03 in the directory C:\BegVCSharp\Chapter11.

  2. Add a new class called Primes, and modify the code as follows:

    using System; using System.Collections; using System.Collections.Generic; 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; } 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.cs as 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.

    image from book
    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 makes use of an iterator to provide this functionality.

The code for Primes starts off with the basics, with 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 there is. The interesting code is all in the GetEnumerator() method. The method signature fulfils the rules for an iterator block in that it returns an IEnumerator type:

public IEnumerator GetEnumerator() {

To extract prime numbers between limits, you need to test each number in turn, so you start with a for loop:

for (long possiblePrime = min; possiblePrime <= max; possiblePrime++) {

Since you don't know whether a number is prime or not, you assume that it is to start off with and then check to see if it isn't. Checking to see if it isn't means checking to see if any number between 2 and the square root of the number to be tested is a factor. If this turns out to be true then the number isn't prime, so you move on to the next one. If the number is indeed prime, you pass it to the foreach loop 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, you'll notice that the results appear one at a time, with pauses in between, rather than in one go. 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 yield calls. Behind the scenes, calling yield does interrupt the code, which resumes when another value is requested, that is, when the foreach loop using the iterator begins a new cycle.

image from book

Iterators and Collections

Earlier you received a promise — that you'd see how iterators can be used to iterate over the objects stored in a dictionary-type collection without having to deal with DictionaryItem objects. You saw the following 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 the following simple iterator to this code to get the desired behavior:

 public new IEnumerator GetEnumerator() { foreach (object animal in Dictionary.Values) yield return (Animal)animal; } 

Now, you can use code as follows to iterate through the Animal objects in the collection:

 foreach (Animal myAnimal in animalCollection) { Console.WriteLine("New {0} object added to custom collection, " + "Name = {1}", myAnimal.ToString(), myAnimal.Name); } 

Deep Copying

In Chapter 9, you saw how you can perform shallow copying with the System.Object .MemberwiseClone() protected method, 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 that 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() will have a field that refers to the same object as the original object.

The following code demonstrates this using this class:

 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 Val public field of the MyContent public 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:

myTarget.MyContent.Val = 5 myTarget.MyContent.Val = 2 

To get round 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. To do this, you implement the ICloneable interface, 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.

Modifying the preceding classes, you might use 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; } }

Here, you create a new Cloner object using the Val field of the Content object contained in the original Cloner object (MyContent). This field is a value type, so no deeper copying is necessary.

Using code similar to that shown above 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 there are times where calls to Clone() will be made recursively, in more complex object systems. For example, if the MyContent field of the Cloner class also required deep copying, 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 Cloner object. For this code to work, you would also need to implement ICloneable on the Content class.

Adding Deep Copying to CardLib

You can put this into practice by implementing the capability to copy Card, Cards, and Deck objects 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 Card objects, 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). You just need to 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, you need to implement ICloneable on the Cards collection class. This is slightly more complicated because it involves cloning every Card object 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, you need to implement ICloneable on the Deck class. There is a slight problem here: the Deck class has no way of modifying 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, you define a new private constructor for the Deck class that allows a specific Cards collection to be passed in when the Deck object is instantiated. The code to implement cloning in this class is:

 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, this code should be placed within the Main() method of a client project to test this out):

 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 the screenshot shown in Figure 11-5.

image from book
Figure 11-5




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