Using Collections - Microsoft® Visual C#® 2012 Step by Step (2012)

Microsoft® Visual C#® 2012 Step by Step (2012)

Chapter 18. Using Collections

After completing this chapter, you will be able to

§ Explain the functionality provided in the different collection classes available with the .NET Framework.

§ Create type-safe collections.

§ Populate a collection with a set of data.

§ Manipulate and access the data items held in a collection.

§ Search a list-oriented collection for matching items by using a predicate.

Chapter 10, introduced you to arrays for holding sets of data. Arrays are very useful in this respect, but they have their limitations. Arrays provide only limited functionality; 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, for example. 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, then arrays may 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 and enable an application to 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, while 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 Microsoft .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, as 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.

The System.Generic.Concurrent namespace defines one other important set of collections. 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 remove 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("\nIterating using a foreach statement:");

foreach (int number in numbers)

{

Console.WriteLine(number);

}

The output of this code is shown here:

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, and 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 AddLastmethod 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, while 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 anull 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("\nIterating using a foreach statement:");

foreach (int number in numbers)

{

Console.WriteLine(number);

}

// Iterate backwards

Console.WriteLine("\nIterating list in reverse order:");

for (LinkedListNode<int> node = numbers.Last; node != null; node = node.Previous)

{

int number = node.Value;

Console.WriteLine(number);

}

The output generated by this code looks like this:

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("\nThe queue contains the following items:");

foreach (int number in numbers)

{

Console.WriteLine(number);

}

// empty the queue

Console.WriteLine("\nDraining the queue:");

while (numbers.Count > 0)

{

int number = numbers.Dequeue();

Console.WriteLine("{0} has left the queue", number);

}

The output from this code is shown here:

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, as it will be covered in grime!) Here’s an example—notice the order in which the items are listed by theforeach 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("\nThe stack now contains:");

foreach (int number in numbers)

{

Console.WriteLine(number);

}

// empty the stack

Console.WriteLine("\nPopping items from the stack:");

while (numbers.Count > 0)

{

int number = numbers.Pop();

Console.WriteLine("{0} has been popped off the stack", number);

}

The output from this program is shown here:

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 inside 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 where the type you map from 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 thekeys you’re mapping from and one for the values you’re mapping to. When you insert a key/value pair into a Dictionary<TKey, TValue> collection, it automatically tracks which key belongs to which value and enables 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 to work in. 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);

}

The output from this program is shown here:

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 it enables you 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 the 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 are kept synchronized, 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 get 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("\nCustomers:");

foreach (string name in customers)

{

Console.WriteLine(name);

}

Console.WriteLine("\nCustomers 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", 47}, {"Diana", 48}, {"James", 21}, {"Francesca", 18}};

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

The Find Methods, Predicates, and Lambda Expressions

The dictionary-oriented collections (Dictionary<TKey, TValue>, SortedDictionary<TKey, TValue>, and SortedList<TKey, TValue>) enable you to 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> andLinkedList<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. For the rest of you, fear not: lambda expressions are not particularly complicated, and after you have gotten used 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.

You saw in Chapter 3, 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 following example 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}\nName: {1}\nAge: {2}", match.ID, match.Name, match.Age);

The output generated by this code looks like this:

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, as 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 will 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 you developed in Chapter 10 to use collections rather than arrays.

Use collections to implement a card game

1. Start Microsoft Visual Studio 2012 if it is not already running.

2. Open the Cards project, located in the \Microsoft Press\Visual CSharp Step By Step\Chapter 18\Windows X\Cards folder in your Documents folder.

This project contains an updated version of the project from Chapter 10 that dealt hands of cards by using arrays. The PlayingCard class has been amended 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:

5. class Pack

6. {

7. ...

8. private Dictionary<Suit, List<PlayingCard>> cardPack;

9. ...

}

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.

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

11.public Pack()

12.{

13. this.cardPack = new Dictionary<Suit, List<PlayingCard>>(NumSuits);

14. ...

}

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 (NumSize is a constant with the value 4).

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

16.public Pack()

17.{

18. this.cardPack = new Dictionary<Suit, List<PlayingCard>>(NumSuits);

19.

20. for (Suit suit = Suit.Clubs; suit <= Suit.Spades; suit++)

21. {

22. List<PlayingCard> cardsInSuit = new List<PlayingCard>(CardsPerSuit);

23. for (Value value = Value.Two; value <= Value.Ace; value++)

24. {

25. ...

26. }

27. }

}

28.Change the code in the inner for loop to add new PlayingCard objects to this collection rather than the array, as shown in bold below:

29.for (Suit suit = Suit.Clubs; suit <= Suit.Spades; suit++)

30.{

31. List<PlayingCard> cardsInSuit = new List<PlayingCard>(CardsPerSuit);

32. for (Value value = Value.Two; value <= Value.Ace; value++)

33. {

34. cardsInSuit.Add(new PlayingCard(suit, value));

35. }

}

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

37.for (Suit suit = Suit.Clubs; suit <= Suit.Spades; suit++)

38.{

39. List<PlayingCard> cardsInSuit = new List<PlayingCard>(CardsPerSuit);

40. for (Value value = Value.Two; value <= Value.Ace; value++)

41. {

42. cardsInSuit.Add(new PlayingCard(suit, value));

43. }

44. this.cardPack.Add(suit, cardsInSuit);

}

45.Find the DealCardFromPack method. Recall that 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 Findmethod 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;

}

46.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 bold:

private bool IsCardAlreadyDealt(Suit suit, Value value)

{

List<PlayingCard> cardsInSuit = this.cardPack[suit];

return (!cardsInSuit.Exists(c => c.CardSuit == suit && c.CardValue == value));

}

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

48.The Hand class currently uses an array to hold the playing cards for the hand. Modify the definition of the cards array to use List<PlayingCard> collection, as shown in bold:

49.class Hand

50.{

51. public const int HandSize = 13;

52. private List<PlayingCard> cards = new List<PlayingCard>(HandSize);

53. ...

}

54.Find the AddCardToHand method. This method currently checks to see whether the hand is full, and if 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 instead.

Remove the playingCardCount variable from the class and modify the if statement that checks whether the hand is full to reference the Count property. 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);

}

55.On the DEBUG menu, click Start Debugging to build and run the application.

56.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 version of the application:

image with no caption

57.Return to Visual Studio 2012 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 2012 running, and turn to Chapter 19.

§ If you want to exit Visual Studio 2012 now

On the FILE menu, click Exit. If you see a Save dialog box, click Yes and save the project.

Chapter 18 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);

}