Chapter 18. Using collections

After completing this chapter, you will be able to:

Chapter 10 introduces arrays for holding sets of data. Arrays are very useful in this respect, but they have their limitations. Arrays provide only limited functionality; for example, it is not easy to increase or reduce the size of an array, and neither is it a simple matter to sort the data held in an array. Another issue is that arrays only really provide a single means of accessing data, by using an integer index. If your application needs to store and retrieve data by using some other mechanism, such as the first-in, first-out queue mechanism described in Chapter 17 arrays might not be the most suitable data structure to use. This is where collections can prove useful.

What are collection classes?

The Microsoft .NET Framework provides several classes that collect elements together such that an application can access them in specialized ways. These are the collection classes mentioned in Chapter 17, and they live in the System.Collections.Generic namespace.

As the namespace implies, these collections are generic types; they all expect you to provide a type parameter indicating the kind of data that your application will be storing in them. Each collection class is optimized for a particular form of data storage and access, and each provides specialized methods that support this functionality. For example, the Stack<T> class implements a last-in, first-out model, where you add an item to the top of the stack by using the Push method, and you take an item from the top of the stack by using the Pop method. The Pop method always retrieves the most recently pushed item and removes it from the stack. In contrast, the Queue<T> type provides the Enqueue and Dequeue methods described in Chapter 17. The Enqueue method adds an item to the queue, whereas the Dequeue method retrieves items in the same order and removes them from the queue, implementing a first-in, first-out model. A variety of other collection classes are also available, and the following table provides a summary of the most commonly used ones.

Collection

Description

List<T>

A list of objects that can be accessed by index, like an array, but with additional methods to search the list and sort the contents of the list.

Queue<T>

A first-in, first-out data structure, with methods to add an item to one end of the queue, remove an item from the other end, and examine an item without removing it.

Stack<T>

A first-in, last-out data structure with methods to push an item onto the top of the stack, pop an item from the top of the stack, and examine the item at the top of the stack without removing it.

LinkedList<T>

A double-ended ordered list, optimized to support insertion and removal at either end. This collection can act like a queue or a stack, but it also supports random access like a list.

HashSet<T>

An unordered set of values that is optimized for fast retrieval of data. It provides set-oriented methods for determining whether the items it holds are a subset of those in another HashSet<T> object as well as computing the intersection and union of HashSet<T> objects.

Dictionary<TKey, TValue>

A collection of values that can be identified and retrieved by using keys rather than indexes.

SortedList<TKey, TValue>

A sorted list of key/value pairs. The keys must implement the IComparable<T> interface.

The following sections provide a brief overview of these collection classes. Refer to the.NET Framework class library documentation for more details on each class.

Note

The .NET Framework class library also provides another set of collection types in the System.Collections namespace. These are nongeneric collections, and they were designed before C# supported generic types (generics were added to the version of C# developed for the .NET Framework version 2.0). With one exception, these types all store object references, and you are required to perform the appropriate casts when storing and retrieving items. These classes are included for backward compatibility with existing applications, and it is not recommended that you use them when building new solutions. In fact, these classes are not available if you are building Windows Store apps.

The one exception that does not store object references is the BitArray class. This class implements a compact array of Boolean values by using an int; each bit indicates true (1) or false (0). If this sounds familiar, it should; this is very similar to the IntBits struct that you saw in the examples in Chapter 16. The BitArray class is available to Windows Store apps.

One other important set of collections is available, and these classes are defined in the System.Collections.Generic.Concurrent namespace. These are thread-safe collection classes that you can use when building multithreaded applications. Chapter 24 provides more information on these classes.

The List<T> collection class

The generic List<T> class is the simplest of the collection classes. You can use it much like an array—you can reference an existing element in a List<T> collection by using ordinary array notation, with square brackets and the index of the element, although you cannot use array notation to add new elements. However, in general, the List<T> class provides more flexibility than arrays and is designed to overcome the following restrictions exhibited by arrays:

  • If you want to resize an array, you have to create a new array, copy the elements (leaving out some if the new array is smaller), and then update any references to the original array so that they refer to the new array.

  • If you want to remove an element from an array, you have to move all the trailing elements up by one place. Even this doesn’t quite work, because you end up with two copies of the last element.

  • If you want to insert an element into an array, you have to move elements down by one place to make a free slot. However, you lose the last element of the array!

The List<T> collection class provides the following features that preclude these limitations:

  • You don’t need to specify the capacity of a List<T> collection when you create it; it can grow and shrink as you add elements. There is an overhead associated with this dynamic behavior, and if necessary you can specify an initial size. However, if you exceed this size, then the List<T> collection will simply grow as necessary.

  • You can remove a specified element from a List<T> collection by using the Remove method. The List<T> collection automatically reorders its elements and closes the gap. You can also remove an item at a specified position in a List<T> collection by using the RemoveAt method.

  • You can add an element to the end of a List<T> collection by using its Add method. You supply the element to be added. The List<T> collection resizes itself automatically.

  • You can insert an element into the middle of a List<T> collection by using the Insert method. Again, the List<T> collection resizes itself.

  • You can easily sort the data in a List<T> object by calling the Sort method.

Note

As with arrays, if you use foreach to iterate through a List<T> collection, you cannot use the iteration variable to modify the contents of the collection. Additionally, you cannot call the Remove, Add, or Insert method in a foreach loop that iterates through a List<T> collection; any attempt to do so results in an InvalidOperationException exception.

Here’s an example that shows how you can create, manipulate, and iterate through the contents of a List<int> collection:

using System;
using System.Collections.Generic;
...
List<int> numbers = new List<int>();
// Fill the List<int> by using the Add method
foreach (int number in new int[12]{10, 9, 8, 7, 7, 6, 5, 10, 4, 3, 2, 1})
{
    numbers.Add(number);
}
// Insert an element in the penultimate position in the list, and move the last item up
// The first parameter is the position; the second parameter is the value being inserted
numbers.Insert(numbers.Count-1, 99);
// Remove first element whose value is 7 (the 4th element, index 3)
numbers.Remove(7);
// Remove the element that's now the 7th element, index 6 (10)
numbers.RemoveAt(6);
// Iterate remaining 11 elements using a for statement
Console.WriteLine("Iterating using a for statement:");
for (int i = 0; i < numbers.Count; i++)
{
    int number = numbers[i];  // Note the use of array syntax
    Console.WriteLine(number);
}
// Iterate the same 11 elements using a foreach statement
Console.WriteLine("
Iterating using a foreach statement:");
foreach (int number in numbers)
{
    Console.WriteLine(number);
}

Here is the output of this code:

Iterating using a for statement:
10
9
8
7
6
5
4
3
2
99
1
Iterating using a foreach statement:
10
9
8
7
6
5
4
3
2
99
1

Note

The way you determine the number of elements for a List<T> collection is different from querying the number of items in an array. When using a List<T> collection, you examine the Count property; when using an array, you examine the Length property.

The LinkedList<T> collection class

The LinkedList<T> collection class implements a doubly linked list. Each item in the list holds the value for the item together with a reference to the next item in the list (the Next property) and the previous item (the Previous property). The item at the start of the list has the Previous property set to null, and the item at the end of the list has the Next property set to null.

Unlike the List<T> class, LinkedList<T> does not support array notation for inserting or examining elements. Instead, you can use the AddFirst method to insert an element at the start of the list, moving the first item up and setting its Previous property to refer to the new item, or the AddLast method to insert an element at the end of the list, setting the Next property of the previously last item to refer to the new item. You can also use the AddBefore and AddAfter methods to insert an element before or after a specified item in the list (you have to retrieve the item first).

You can find the first item in a LinkedList<T> collection by querying the First property, whereas the Last property returns a reference to the final item in the list. To iterate through a linked list, you can start at one end and step through the Next or Previous references until you find an item with a null value for this property. Alternatively, you can use a foreach statement, which iterates forward through a LinkedList<T> object and stops automatically at the end.

You delete an item from a LinkedList<T> collection by using the Remove, RemoveFirst, and RemoveLast methods.

The following example shows a LinkedList<T> collection in action. Notice how the code that iterates through the list by using a for statement steps through the Next (or Previous) references, only stopping when it reaches a null reference, which is the end of the list:

using System;
using System.Collections.Generic;
...
LinkedList<int> numbers = new LinkedList<int>();
// Fill the List<int> by using the AddFirst method
foreach (int number in new int[] { 10, 8, 6, 4, 2 })
{
    numbers.AddFirst(number);
}
// Iterate using a for statement
Console.WriteLine("Iterating using a for statement:");
for (LinkedListNode<int> node = numbers.First; node != null; node = node.Next)
{
    int number = node.Value;
    Console.WriteLine(number);
}
// Iterate using a foreach statement
Console.WriteLine("
Iterating using a foreach statement:");
foreach (int number in numbers)
{
    Console.WriteLine(number);
}
// Iterate backwards
Console.WriteLine("
Iterating list in reverse order:");
for (LinkedListNode<int> node = numbers.Last; node != null; node = node.Previous)
{
    int number = node.Value;
    Console.WriteLine(number);
}

Here is the output generated by this code:

Iterating using a for statement:
2
4
6
8
10
Iterating using a foreach statement:
2
4
6
8
10
Iterating list in reverse order:
10
8
6
4
2

The Queue<T> collection class

The Queue<T> class implements a first-in, first-out mechanism. An element is inserted into the queue at the back (the Enqueue operation) and is removed from the queue at the front (the Dequeue operation).

The following code is an example showing a Queue<int> collection and its common operations:

using System;
using System.Collections.Generic;
...
Queue<int> numbers = new Queue<int>();
// fill the queue
Console.WriteLine("Populating the queue:");
foreach (int number in new int[4]{9, 3, 7, 2})
{
    numbers.Enqueue(number);
    Console.WriteLine("{0} has joined the queue", number);
}
// iterate through the queue
Console.WriteLine("
The queue contains the following items:");
foreach (int number in numbers)
{
    Console.WriteLine(number);
}
// empty the queue
Console.WriteLine("
Draining the queue:");
while (numbers.Count > 0)
{
    int number = numbers.Dequeue();
    Console.WriteLine("{0} has left the queue", number);
}

Here is thee output from this code:

Populating the queue:
9 has joined the queue
3 has joined the queue
7 has joined the queue
2 has joined the queue
The queue contains the following items:
9
3
7
2
Draining the queue:
9 has left the queue
3 has left the queue
7 has left the queue
2 has left the queue

The Stack<T> collection class

The Stack<T> class implements a last-in, first-out mechanism. An element joins the stack at the top (the push operation) and leaves the stack at the top (the pop operation). To visualize this, think of a stack of dishes: new dishes are added to the top and dishes are removed from the top, making the last dish to be placed on the stack the first one to be removed. (The dish at the bottom is rarely used and will inevitably require washing before you can put any food on it, because it will be covered in grime!) Here’s an example—notice the order in which the items are listed by the foreach loop:

using System;
using System.Collections.Generic;
...
Stack<int> numbers = new Stack<int>();
// fill the stack
Console.WriteLine("Pushing items onto the stack:");
foreach (int number in new int[4]{9, 3, 7, 2})
{
    numbers.Push(number);
    Console.WriteLine("{0} has been pushed on the stack", number);
}
// iterate through the stack
Console.WriteLine("
The stack now contains:");
foreach (int number in numbers)
{
    Console.WriteLine(number);
}
// empty the stack
Console.WriteLine("
Popping items from the stack:");
while (numbers.Count > 0)
{
    int number = numbers.Pop();
    Console.WriteLine("{0} has been popped off the stack", number);
}

Here is the output from this program:

Pushing items onto the stack:
9 has been pushed on the stack
3 has been pushed on the stack
7 has been pushed on the stack
2 has been pushed on the stack
The stack now contains:
2
7
3
9
Popping items from the stack:
2 has been popped off the stack
7 has been popped off the stack
3 has been popped off the stack
9 has been popped off the stack

The Dictionary<TKey, TValue> collection class

The array and List<T> types provide a way to map an integer index to an element. You specify an integer index within square brackets (for example, [4]), and you get back the element at index 4 (which is actually the fifth element). However, sometimes you might want to implement a mapping in which the type from which you map is not an int but rather some other type, such as string, double, or Time. In other languages, this is often called an associative array. The Dictionary<TKey, TValue> class implements this functionality by internally maintaining two arrays, one for the keys from which you’re mapping and one for the values to which you’re mapping. When you insert a key/value pair into a Dictionary<TKey, TValue> collection, it automatically tracks which key belongs to which value and makes it possible for you to retrieve the value that is associated with a specified key quickly and easily. The design of the Dictionary<TKey, TValue> class has some important consequences:

  • A Dictionary<TKey, TValue> collection cannot contain duplicate keys. If you call the Add method to add a key that is already present in the keys array, you’ll get an exception. You can, however, use the square brackets notation to add a key/value pair (as shown in the following example), without danger of an exception, even if the key has already been added; any existing value with the same key will be overwritten by the new value. You can test whether a Dictionary<TKey, TValue> collection already contains a particular key by using the ContainsKey method.

  • Internally, a Dictionary<TKey, TValue> collection is a sparse data structure that operates most efficiently when it has plenty of memory with which to work. The size of a Dictionary<TKey, TValue> collection in memory can grow quite quickly as you insert more elements.

  • When you use a foreach statement to iterate through a Dictionary<TKey, TValue> collection, you get back a KeyValuePair<TKey, TValue> item. This is a structure that contains a copy of the key and value elements of an item in the Dictionary<TKey, TValue> collection, and you can access each element through the Key property and the Value properties. These elements are read-only; you cannot use them to modify the data in the Dictionary<TKey, TValue> collection.

Here is an example that associates the ages of members of my family with their names and then prints the information:

using System;
using System.Collections.Generic;
...
Dictionary<string, int> ages = new Dictionary<string, int>();
// fill the Dictionary
ages.Add("John", 47);    // using the Add method
ages.Add("Diana", 46);
ages["James"] = 20;      // using array notation
ages["Francesca"] = 18;
// iterate using a foreach statement
// the iterator generates a KeyValuePair item
Console.WriteLine("The Dictionary contains:");
foreach (KeyValuePair<string, int> element in ages)
{
    string name = element.Key;
    int age = element.Value;
    Console.WriteLine("Name: {0}, Age: {1}", name, age);
}

Here is the output from this program:

The Dictionary contains:
Name: John, Age: 47
Name: Diana, Age: 46
Name: James, Age: 20
Name: Francesca, Age: 18

Note

The System.Collections.Generic namespace also includes the SortedDictionary<TKey, TValue> collection type. This class maintains the collection in order, sorted by the keys.

The SortedList<TKey, TValue> collection class

The SortedList<TKey, TValue> class is very similar to the Dictionary<TKey, TValue> class in that you can use it to associate keys with values. The main difference is that the keys array is always sorted. (It is called a SortedList, after all.) It takes longer to insert data into a SortedList<TKey, TValue> object than a SortedDictionary<TKey, TValue> object in most cases, but data retrieval is often quicker (or at least as quick), and SortedList<TKey, TValue> class uses less memory.

When you insert a key/value pair into a SortedList<TKey, TValue> collection, the key is inserted into the keys array at the correct index to keep the keys array sorted. The value is then inserted into the values array at the same index. The SortedList<TKey, TValue> class automatically ensures that keys and values maintain synchronization, even when you add and remove elements. This means that you can insert key/value pairs into a SortedList<TKey, TValue> in any sequence; they are always sorted based on the value of the keys.

Like the Dictionary<TKey, TValue> class, a SortedList<TKey, TValue> collection cannot contain duplicate keys. When you use a foreach statement to iterate through a SortedList<TKey, TValue>, you receive back a KeyValuePair<TKey, TValue> item. However, the KeyValuePair<TKey, TValue> items will be returned sorted by the Key property.

Here is the same example that associates the ages of members of my family with their names and then prints the information, but this version has been adjusted to use a SortedList<TKey, TValue> object rather than a Dictionary<TKey, TValue> collection:

using System;
using System.Collections.Generic;
...
SortedList<string, int> ages = new SortedList<string, int>();
// fill the SortedList
ages.Add("John", 47);    // using the Add method
ages.Add("Diana", 46);
ages["James"] = 20;      // using array notation
ages["Francesca"] = 18;
// iterate using a foreach statement
// the iterator generates a KeyValuePair item
Console.WriteLine("The SortedList contains:");
foreach (KeyValuePair<string, int> element in ages)
{
    string name = element.Key;
    int age = element.Value;
    Console.WriteLine("Name: {0}, Age: {1}", name, age);
}

The output from this program is sorted alphabetically by the names of my family members:

The SortedList contains:
Name: Diana, Age: 46
Name: Francesca, Age: 18
Name: James, Age: 20
Name: John, Age: 47

Important

The SortedList<TKey, TValue> type is not available in Windows Store apps. If you require this functionality, you should use the SortedDictionary<TKey, TValue> type.

The HashSet<T> collection class

The HashSet<T> class is optimized for performing set operations such as determining set membership and generating the union and intersect of sets.

You insert items into a HashSet<T> collection by using the Add method, and you delete items by using the Remove method. However, the real power of the HashSet<T> class is provided by the IntersectWith, UnionWith, and ExceptWith methods. These methods modify a HashSet<T> collection to generate a new set that either intersects with, has a union with, or does not contain the items in a specified HashSet<T> collection. These operations are destructive inasmuch as they overwrite the contents of the original HashSet<T> object with the new set of data. You can also determine whether the data in one HashSet<T> collection is a superset or subset of another by using the IsSubsetOf, IsSupersetOf, IsProperSubsetOf, and IsProperSupersetOf methods. These methods return a Boolean value and are nondestructive.

Internally, a HashSet<T> collection is held as a hash table, enabling fast lookup of items. However, a large HashSet<T> collection can require a significant amount of memory to operate quickly.

The following example shows how to populate a HashSet<T> collection and illustrates the use of the IntersectWith method to find data that overlaps two sets:

using System;
using System.Collections.Generic;
...
HashSet<string> employees = new HashSet<string>(new string[] {"Fred","Bert","Harry","John"});
HashSet<string> customers = new HashSet<string>(new string[] {"John","Sid","Harry","Diana"});
employees.Add("James");
customers.Add("Francesca");
Console.WriteLine("Employees:");
foreach (string name in employees)
{
    Console.WriteLine(name);
}
Console.WriteLine("
Customers:");
foreach (string name in customers)
{
    Console.WriteLine(name);
}
Console.WriteLine("
Customers who are also employees:");
customers.IntersectWith(employees);
foreach (string name in customers)
{
    Console.WriteLine(name);
}

This code generates the following output:

Employees:
Fred
Bert
Harry
John
James
Customers:
John
Sid
Harry
Diana
Francesca
Customers who are also employees:
John
Harry

Note

The System.Collections.Generic namespace also provides the SortedSet<T> collection type, which operates in a similar manner to the HashSet<T> class. The primary difference, as the name implies, is that the data is maintained in a sorted order. The SortedSet<T> and HashSet<T> classes are interoperable; you can take the union of a SortedSet<T> collection with a HashSet<T> collection, for example.

Using collection initializers

The examples in the preceding subsections have shown you how to add individual elements to a collection by using the method most appropriate to that collection (Add for a List<T> collection, Enqueue for a Queue<T> collection, Push for a Stack<T> collection, and so on). You can also initialize some collection types when you declare them, using a syntax similar to that supported by arrays. For example, the following statement creates and initializes the numbers List<int> object shown earlier, demonstrating an alternate technique to repeatedly calling the Add method:

List<int> numbers = new List<int>(){10, 9, 8, 7, 7, 6, 5, 10, 4, 3, 2, 1};

Internally, the C# compiler actually converts this initialization to a series of calls to the Add method. Consequently, you can use this syntax only for collections that actually support the Add method. (The Stack<T> and Queue<T> classes do not.)

For more complex collections that take key/value pairs, such as the Dictionary<TKey, TValue> class, you can specify each key/value pair as an anonymous type in the initializer list, like this:

Dictionary<string, int> ages =
    new Dictionary<string, int>(){{"John", 44}, {"Diana", 45}, {"James", 17}, {"Francesca", 15}};

The first item in each pair is the key, and the second is the value.

The Find methods, predicates, and lambda expressions

Using the dictionary-oriented collections (Dictionary<TKey, TValue>, SortedDictionary<TKey, TValue>, and SortedList<TKey, TValue>), you can quickly find a value by specifying the key to search for, and you can use array notation to access the value, as you have seen in earlier examples. Other collections that support nonkeyed random access, such as the List<T> and LinkedList<T> classes, do not support array notation but instead provide the Find method to locate an item. For these classes, the argument to the Find method is a predicate that specifies the search criteria to use. The form of a predicate is a method that examines each item in the collection and returns a Boolean value indicating whether the item matches. In the case of the Find method, as soon as the first match is found, the corresponding item is returned. Note that the List<T> and LinkedList<T> classes also support other methods such as FindLast, which returns the last matching object, and the List<T> class additionally provides the FindAll method, which returns a List<T> collection of all matching objects.

The easiest way to specify the predicate is to use a lambda expression. A lambda expression is an expression that returns a method. This sounds rather odd because most expressions that you have encountered so far in C# actually return a value. If you are familiar with functional programming languages such as Haskell, you are probably comfortable with this concept. If you are not, fear not: lambda expressions are not particularly complicated, and after you have become accustomed to a new bit of syntax, you will see that they are very useful.

Note

If you are interested in finding out more about functional programming with Haskell, visit the Haskell programming language website at http://www.haskell.org/haskellwiki/Haskell.

Chapter 3 instructs that a typical method consists of four elements: a return type, a method name, a list of parameters, and a method body. A lambda expression contains two of these elements: a list of parameters and a method body. Lambda expressions do not define a method name, and the return type (if any) is inferred from the context in which the lambda expression is used. In the case of the Find method, the predicate processes each item in the collection in turn; the body of the predicate must examine the item and return true or false depending on whether it matches the search criteria. The example that follows shows the Find method (highlighted in bold) on a List<Person> collection, where Person is a struct. The Find method returns the first item in the list that has the ID property set to 3:

struct Person
{
    public int ID { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
}
...
// Create and populate the personnel list
List<Person> personnel = new List<Person>()
{
    new Person() { ID = 1, Name = "John", Age = 47 },
    new Person() { ID = 2, Name = "Sid", Age = 28 },
    new Person() { ID = 3, Name = "Fred", Age = 34 },
    new Person() { ID = 4, Name = "Paul", Age = 22 },
};
// Find the member of the list that has an ID of 3
Person match = personnel.Find((Person p) => { return p.ID == 3; });
Console.WriteLine("ID: {0}
Name: {1}
Age: {2}", match.ID, match.Name, match.Age);

Here is the output generated by this code:

ID: 3
Name: Fred
Age: 34

In the call to the Find method, the argument (Person p) => { return p.ID == 3; } is a lambda expression that actually does the work. It has the following syntactic items:

  • A list of parameters enclosed in parentheses. As with a regular method, if the method you are defining (as in the preceding example) takes no parameters, you must still provide the parentheses. In the case of the Find method, the predicate is provided with each item from the collection in turn, and this item is passed as the parameter to the lambda expression.

  • The => operator, which indicates to the C# compiler that this is a lambda expression.

  • The body of the method. The example shown here is very simple, containing a single statement that returns a Boolean value indicating whether the item specified in the parameter matches the search criteria. However, a lambda expression can contain multiple statements, and you can format it in whatever way you feel is most readable. Just remember to add a semicolon after each statement, as you would in an ordinary method.

Strictly speaking, the body of a lambda expression can be a method body containing multiple statements or it can actually be a single expression. If the body of a lambda expression contains only a single expression, you can omit the braces and the semicolon (but you still need a semicolon to complete the entire statement). Additionally, if the expression takes a single parameter, you can omit the parentheses that surround the parameter. Finally, in many cases, you can actually omit the type of the parameters because the compiler can infer this information from the context from which the lambda expression is invoked. A simplified form of the Find statement shown previously looks like this (this statement is much easier to read and understand):

Person match = personnel.Find(p => p.ID == 3);

Lambda expressions are very powerful constructs, and you can learn more about them in Chapter 20.

Comparing arrays and collections

Here’s a summary of the important differences between arrays and collections:

  • An array instance has a fixed size and cannot grow or shrink. A collection can dynamically resize itself as required.

  • An array can have more than one dimension. A collection is linear. However, the items in a collection can be collections themselves, so you can imitate a multidimensional array as a collection of collections.

  • You store and retrieve an item in an array by using an index. Not all collections support this notion. For example, to store an item in a List<T> collection, you use the Add or Insert methods, and to retrieve an item, you use the Find method.

  • Many of the collection classes provide a ToArray method that creates and populates an array containing the items in the collection. The items are copied to the array and are not removed from the collection. Additionally, these collections provide constructors that can populate a collection directly from an array.

Using collection classes to play cards

In the next exercise, you will convert the card game that is developed in Chapter 10 to use collections rather than arrays.

Use collections to implement a card game
  1. Start Microsoft Visual Studio 2013 if it is not already running.

  2. Open the Cards project, which is located in the Microsoft PressVisual CSharp Step By StepChapter 18Windows XCards folder in your Documents folder.

    This project contains an updated version of the project from Chapter 10 that deals hands of cards by using arrays. The PlayingCard class is modified to expose the value and suit of a card as read-only properties.

  3. Display the Pack.cs file in the Code and Text Editor window. Add the following using directive to the top of the file:

    using System.Collections.Generic;
  4. In the Pack class, change the definition of the cardPack two-dimensional array to a Dictionary<Suit, List< PlayingCard>> object, as shown here in bold:

    class Pack
    {
        ...
        private Dictionary<Suit, List<PlayingCard>> cardPack;
        ...
    }

    The original application used a two-dimensional array for representing a pack of cards. This code replaces the array with a Dictionary, where the key specifies the suit, and the value is a list of cards in that suit.

  5. Locate the Pack constructor. Modify the first statement in this constructor to instantiate the cardPack variable as a new Dictionary collection rather than an array, as shown here in bold:

    public Pack()
    {
        this.cardPack = new Dictionary<Suit, List<PlayingCard>>(NumSuits);
        ...
    }

    Although a Dictionary collection will resize itself automatically as items are added, if the collection is unlikely to change in size, you can specify an initial size when you instantiate it. This helps to optimize the memory allocation, although the Dictionary collection can still grow if this size is exceeded. In this case, the Dictionary collection will contain a collection of four lists (one list for each suit), so it is allocated space for four items (NumSuits is a constant with the value 4).

  6. In the outer for loop, declare a List<PlayingCard> collection object called cardsInSuit that is big enough to hold the number of cards in each suit (use the CardsPerSuit constant), as follows in bold:

    public Pack()
    {
        this.cardPack = new Dictionary<Suit, List<PlayingCard>>(NumSuits);
        for (Suit suit = Suit.Clubs; suit <= Suit.Spades; suit++)
        {
            List<PlayingCard> cardsInSuit = new List<PlayingCard>(CardsPerSuit);
            for (Value value = Value.Two; value <= Value.Ace; value++)
            {
                ...
            }
        }
    }
  7. Change the code in the inner for loop to add new PlayingCard objects to this collection rather than the array, as shown in bold in the following code:

    for (Suit suit = Suit.Clubs; suit <= Suit.Spades; suit++)
    {
        List<PlayingCard> cardsInSuit = new List<PlayingCard>(CardsPerSuit);
        for (Value value = Value.Two; value <= Value.Ace; value++)
        {
            cardsInSuit.Add(new PlayingCard(suit, value));
        }
    }
  8. After the inner for loop, add the List object to the cardPack Dictionary collection, specifying the value of the suit variable as the key to this item:

    for (Suit suit = Suit.Clubs; suit <= Suit.Spades; suit++)
    {
        List<PlayingCard> cardsInSuit = new List<PlayingCard>(CardsPerSuit);
        for (Value value = Value.Two; value <= Value.Ace; value++)
        {
            cardsInSuit.Add(new PlayingCard(suit, value));
        }
        this.cardPack.Add(suit, cardsInSuit);
    }
  9. Find the DealCardFromPack method.

    This method picks a card at random from the pack, removes the card from the pack, and returns this card. The logic for selecting the card does not require any changes, but the statements at the end of the method that retrieve the card from the array must be updated to use the Dictionary collection, instead. Additionally, the code that removes the card from the array (it has now been dealt) must be modified; you need to search for the card in the list and then remove it from the list. To locate the card, use the Find method and specify a predicate that finds a card with the matching value. The parameter to the predicate should be a PlayingCard object (the list contains PlayingCard items).

    The updated statements occur after the closing brace of the second while loop, as shown in bold in the following code:

    public PlayingCard DealCardFromPack()
    {
        Suit suit = (Suit)randomCardSelector.Next(NumSuits);
        while (this.IsSuitEmpty(suit))
        {
            suit = (Suit)randomCardSelector.Next(NumSuits);
        }
        Value value = (Value)randomCardSelector.Next(CardsPerSuit);
        while (this.IsCardAlreadyDealt(suit, value))
        {
            value = (Value)randomCardSelector.Next(CardsPerSuit);
        }
        List<PlayingCard> cardsInSuit = this.cardPack[suit];
        PlayingCard card = cardsInSuit.Find(c => c.CardValue == value);
        cardsInSuit.Remove(card);
        return card;
    }
  10. Locate the IsCardAlreadyDealt method.

    This method determines whether a card has already been dealt by checking whether the corresponding element in the array has been set to null. You need to modify this method to determine whether a card with the specified value is present in the list for the suit in the cardPack Dictionary collection.

    To determine whether an item exists in a List<T> collection, you use the Exists method. This method is similar to Find inasmuch as it takes a predicate as its argument. The predicate is passed each item from the collection in turn, and it should return true if the item matches some specified criteria, and false otherwise. In this case, the List<T> collection holds PlayingCard objects, and the criteria for the Exists predicate should return true if it is passed a PlayingCard item with a suit and value that matches the parameters passed to the IsCardAlreadyDealt method.

    Update the method, as shown in the following example, in bold:

    private bool IsCardAlreadyDealt(Suit suit, Value value)
    {
        List<PlayingCard> cardsInSuit = this.cardPack[suit];
        return (!cardsInSuit.Exists(c => c.CardSuit == suit && c.CardValue == value));
    }
  11. Display the Hand.cs file in the Code and Text Editor window. Add the following using directive to the list at the top of the file:

    using System.Collections.Generic;
  12. The Hand class currently uses an array called cards to hold the playing cards for the hand. Modify the definition of the cards variable to be a List<PlayingCard> collection, as shown here in bold:

    class Hand
    {
        public const int HandSize = 13;
        private List<PlayingCard> cards = new List<PlayingCard>(HandSize);
        ...
    }
  13. Find the AddCardToHand method.

    This method currently checks to see whether the hand is full; if it is not, it adds the card provided as the parameter to the cards array at the index specified by the playingCardCount variable.

    Update this method to use the Add method of the List<PlayingCard> collection, instead.

    This change also removes the need to explicitly keep track of how many cards the collection holds because you can use the Count property of the cards collection, instead. Therefore, remove the playingCardCount variable from the class and modify the if statement that checks whether the hand is full to reference the Count property of the cards collection.

    The completed method should look like this, with the changes highlighted in bold:

    public void AddCardToHand(PlayingCard cardDealt)
    {
        if (this.cards.Count >= HandSize)
        {
            throw new ArgumentException("Too many cards");
        }
        this.cards.Add(cardDealt);
    }
  14. On the Debug menu, click Start Debugging to build and run the application.

  15. When the Card Game form appears, click Deal.

    Note

    Remember that in the Windows Store apps version of this application, the Deal button is located on the app bar.

    Verify that the cards are dealt and that the populated hands appear as before. Click Deal again to generate another random set of hands.

    The following image shows the Windows 8.1 version of the application:

    A screenshot of the Cards application running in Windows 8.1.

    Return to Visual Studio 2013 and stop debugging.

Summary

In this chapter, you learned how to create and use arrays to manipulate sets of data. You also saw how to use some of the common collection classes to store and access data.

  • If you want to continue to the next chapter, keep Visual Studio 2013 running, and turn to Chapter 19.

  • If you want to exit Visual Studio 2013 now, on the File menu, click Exit. If you see a Save dialog box, click Yes and save the project.

Quick reference

To

Do this

Create a new collection

Use the constructor for the collection class. For example:

List<PlayingCard> cards = new
List<PlayingCard>();

Add an item to a collection

Use the Add or Insert methods (as appropriate) for lists, hash sets, and dictionary-oriented collections. Use the Enqueue method for Queue<T> collections. Use the Push method for Stack<T> collections. For example:

HashSet<string> employees = new
HashSet<string>();
employees.Add("John");
...
LinkedList<int> data = new LinkedList<int>();
data.AddFirst(101);
...
Stack<int> numbers = new Stack<int>();
numbers.Push(99);

Remove an item from a collection

Use the Remove method for lists, hash sets, and dictionary-oriented collections. Use the Dequeue method for Queue<T> collections. Use the Pop method for Stack<T> collections. For example:

HashSet<string> employees = new
HashSet<string>();
employees.Remove("John");
...
LinkedList<int> data = new LinkedList<int>();
data.Remove(101);
...
Stack<int> numbers = new Stack<int>();
int item = numbers.Pop();

Find the number of elements in a collection

Use the Count property. For example:

List<PlayingCard> cards = new
List<PlayingCard>();
...
int noOfCards = cards.Count;

Locate an item in a collection

For dictionary-oriented collections, use array notation. For lists, use the Find methods. For example:

Dictionary<string, int> ages =
    new Dictionary<string, int>();
ages.Add("John", 47);
int johnsAge = ages["John"];
...
List<Person> personnel = new List<Person>();
Person match = personnel.Find(p => p.ID == 3);

Note: The Stack<T>, Queue<T>, and hash set collection classes do not support searching, although you can test for membership of an item in a hash set by using the Contains method.

Iterate through the elements of a collection

Use a for statement or a foreach statement. For example:

LinkedList<int> numbers = new LinkedList<int>();
...
for (LinkedListNode<int> node = numbers.First;
     node != null; node = node.Next)
{
    int number = node.Value;
    Console.WriteLine(number);
}
...
foreach (int number in numbers)
{
    Console.WriteLine(number);
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.33.235