C# 24-Hour Trainer (2015)
Section II
Variables and Calculations
Lesson 16
Using Arrays and Collections
Each of the data types described in previous lessons holds a single piece of data. A variable might hold an integer, string, or point in time.
Sometimes it's convenient to work with a group of related values all at once. For example, suppose you're the CEO of a huge company that just posted huge gains. In that case, you might want to give each hourly employee a certificate of appreciation and give each executive a 15 percent bonus.
In cases like this, it would be handy to be able to store all of the hourly employees' data in one variable so you could easily work with it. Similarly you might like to store the executives' data in a second variable so it's easy to manage.
In this lesson, you learn how to make variables that can hold more than one piece of data. You learn how to make arrays and different kinds of collections such as a List, Dictionary, Stack, and Queue.
This lesson explains how to build these objects and add and remove items from them. Lesson 19 explains how to get the full benefit of them by looping through them to perform some action on each of the items they contain.
Arrays
An array is a group of values that all have the same data type and that all share the same name. To pick a particular item in the array, the program uses an index, which is an integer greater than or equal to 0.
An array is similar to the mailboxes in an apartment building. The building has a single bank of mailboxes that all have the same street address (the array's name). You use the apartment numbers to pick a particular cubbyhole in the bank of mailboxes.
Figure 16.1 shows an array graphically. This array is named values. It contains eight entries with indexes 0 through 7.
Figure 16.1
NOTE
An array's smallest and largest indexes are called its lower bound and upper bound, respectively. In C#, the lower bound is always 0, and the upper bound is always one less than the length of the array.
Creating Arrays
The following code shows how you can declare an array of integers. The square brackets indicate an array so the first part of the statement int[] means the variable's data type is an array of integers:
int[] values;
After you declare an array variable, you can assign it to a new uninitialized array. The following code initializes the variable values to a new integer array that can hold eight elements:
values = new int[8];
Remember that an array's lower bound is always 0 in C# so this array has indexes 0 through 7.
As is the case with other variables, you can declare and initialize an array in a single step. The following code declares and creates the values array in a single statement:
int[] values = new int[8];
After you have created an array, you can access its members by using the array's name followed by an index inside square brackets. For example, the following code initializes the values array by setting the Nth entry equal to N2:
values[0] = 0 * 0;
values[1] = 1 * 1;
values[2] = 2 * 2;
values[3] = 3 * 3;
values[4] = 4 * 4;
values[5] = 5 * 5;
values[6] = 6 * 6;
values[7] = 7 * 7;
NOTE
Most programmers pronounce values[5] as “values of 5,” “values sub 5,” or “the 5th element of values.”
After you have placed values in an array, you can read the values using the same square bracket syntax. The following code displays a message box that uses one of the array's values:
MessageBox.Show("7 * 7 is " + values[7].ToString());
To make initializing arrays easier, C# provides an abbreviated syntax that lets you declare an array and set its values all in one statement. Simply set the variable equal to the values you want separated by commas and surrounded by braces as shown in the following code:
int[] values = { 0, 1, 1, 2, 3, 5, 8, 13, 21, 34 };
When you use this syntax, C# uses the number of values you supply to define the array's size. In the preceding code, C# would give the values array 10 entries because that's how many values the code supplies.
A Fibonacci Example
Here's a slightly more interesting example that uses an array. The Fibonacci sequence is defined by the following three rules:
Fibonacci[0] = 0
Fibonacci[1] = 1
Fibonacci[n] = Fibonacci[n - 1] + Fibonacci[n - 2]
NOTE
The Fibonacci sequence, which was described by the Italian mathematician Fibonacci, is the infinite sequence the numbers 0, 1, 1, 2, 3, 5, 8, 13, 21, … Each value in the sequence after the first two is the sum of the two previous values. For example, 3 + 5 = 8.
The Fibonacci sequence pops up in several strange and interesting mathematical and natural systems. For example, they appear in flower petal arrangements and the number of seeds in a sunflower. You can even use them to convert between miles and kilometers (although that's basically a coincidence). For more information, see www.mathsisfun.com/numbers/fibonacci-sequence.html, math.stackexchange.com/questions/381/applications-of-the-fibonacci-sequence ormathworld.wolfram.com/FibonacciNumber.html.
The Fibonacci program shown in Figure 16.2 (and available as part of this lesson's code download) uses an array to display Fibonacci numbers. Use the NumericUpDown control to select a number and click Calculate to see the corresponding Fibonacci number.
Figure 16.2
When the user clicks Calculate, the program executes the following code:
// Calculate and display the desired Fibonacci number.
private void calculateButton_Click(object sender, EventArgs e)
{
int[] values = new int[21];
values[0] = 0;
values[1] = 1;
values[2] = values[0] + values[1];
values[3] = values[1] + values[2];
values[4] = values[2] + values[3];
...
values[20] = values[18] + values[19];
int index = (int)numberNumericUpDown.Value;
resultsTextBox.Text = values[index].ToString();
}
The code starts by initializing the values array to hold the first 21 Fibonacci numbers.
After initializing the array, the program gets the value selected by the NumericUpDown control and converts it from a decimal to an int. It then uses that value as an index into the values array and displays the result in resultTextBox.
Multi-Dimensional Arrays
The arrays described in the previous section hold a single row of items, but C# also lets you define multi-dimensional arrays. You can think of these as higher-dimensional sequences of apartment mailboxes.
Figure 16.3 shows a graphic representation of a two-dimensional array with four rows and eight columns.
Figure 16.3
The following code shows how you could declare, allocate, and initialize this array to hold a multiplication table with values up to 4 times 7:
int[,] values = new int[5, 7];
values[0, 0] = 0 * 0;
values[0, 1] = 0 * 1;
values[0, 2] = 0 * 2;
...
values[1, 1] = 1 * 1;
values[1, 2] = 1 * 2;
...
values[4, 7] = 4 * 7;
The following code shows the C# syntax for quickly defining and initializing a two-dimensional array:
int[,] cell =
{
{0, 1, 2},
{3, 4, 5},
{6, 7, 8},
};
This syntax basically assigns the array variable equal to an array containing one-dimensional arrays of values.
NOTE
Notice that the definition of the array's final row of data ends with a comma. You don't need this comma because nothing follows this last row, but C# allows you to include it to give the rows a more uniform format. The commas after the other rows are required because more rows follow them.
You can use similar syntax to make and initialize higher-dimensional arrays, although they're harder to visualize graphically. For example, the following code makes a four-dimensional array of strings:
string[, , ,] employeeData = new string[10, 20, 30, 40];
Array Properties and Methods
All arrays have a Length property that your code can use to determine the number of items in the array. Arrays all have lower bound 0, so for one-dimensional arrays, Length – 1 gives an array's upper bound.
Arrays also have GetLowerBound and GetUpperBound methods that return the lower and upper bounds for a particular dimension in an array.
For example, the following code creates a 5-by-10 two-dimensional array. It then displays the lower and upper bounds for the first dimension. (Like an array's indexes, the dimension numbers start at 0.)
int[,] x = new int[5, 10];
MessageBox.Show("The first dimension runs from " +
x.GetLowerBound(0) + " to " + x.GetUpperBound(0));
The Array class also provides several useful static methods that you can use to manipulate arrays. For example, the following code sorts the array named salaries:
Array.Sort(salaries);
NOTE
To sort an array, the array must contain things that can be compared in a meaningful way. For example, int and string data have a natural order, so it's easy to say that the string “Jackson” should come before the string “Utah.”
If an array holds Employee objects, however, it's unclear how you would want to compare two items. In fact, it's likely that you couldn't define an order that would always work because sometimes you might want to sort employees by name and other times you might want to sort them by employee ID or salary.
You can solve this problem in a couple of ways including the IComparer interface (mentioned briefly in Lesson 27's Exercise 2) and making the Employee class implement IComparable (mentioned in Lesson 28). These are slightly more advanced topics, so they aren't covered in great depth here.
The Sort method has many overloaded versions that perform different kinds of sorting. For example, instead of passing it a single array you can pass it an array of keys and an array of items. In that case the method sorts the keys, moving the items so they remain matched up with their corresponding keys.
The Table 16.1 summarizes the most useful methods provided by the Array class.
Table 16.1
Method |
Purpose |
BinarySearch |
Uses binary search to find an item in a sorted array. |
Clear |
Resets a range of items in the array to the default value for the array's data type (0, false, or null). |
Copy |
Copies a range of items from one array to another. |
IndexOf |
Returns the index of the first occurrence of a particular item in the array. |
LastIndexOf |
Returns the index of the last occurrence of a particular item in the array. |
Resize |
Resizes the array, preserving any items that fit in the new size. |
Reverse |
Reverses the order of the items in the array. |
Sort |
Sorts the array's items. |
Collection Classes
An array holds a group of items and lets you refer to them by index. The .NET Framework used by C# also provides an assortment of collection classes that you can use to store and manipulate items in other ways. For example, a Dictionary stores items with keys and lets you very quickly locate an item from its key.
For example, you could use a Dictionary to make an employee phone book. It could store phone numbers using names as the keys. Then given someone's name, you could use the dictionary to very quickly look up that person's phone number.
Generic Classes
The following sections describe some particular kinds of classes that come pre-built by the .NET Framework. These are generic classes, so before you learn about them you should know a little about what a generic class is.
A generic class is one that is not tied to a particular data type. For example, suppose you build a StringList class that can store a list of strings. Now suppose you decide you wanted an IntegerList class to store lists of integers. The two classes would be practically identical; they would just work with different data types.
I've mentioned several times that duplicated code is a bad thing. Having two nearly identical classes means debugging and maintaining two different sets of code that are practically the same.
One solution to this situation is to make a more general AnythingList class that uses the general object data type to store items. An object can hold any kind of data, so this class could hold lists of integers, strings, or Customer objects. Unfortunately that has two big problems.
First, you would need to do a lot of work converting the items with the general object data type stored in the list into the int, string, or Customer type of the items that you put in there. This is annoying because it gives you more work to do and makes your code more complicated and harder to read.
A bigger problem is that a list that can hold anything can hold anything. If you make a list to hold customer data, it could still hold ints, strings, and PurchaseOrder objects. Your code would need to do a lot of work to prevent you from accidentally adding the wrong kind of item to the list.
A much better approach is to use generic classes. These classes take data type parameters in their declarations so they know what kind of data they will manipulate. That lets them automatically store and retrieve items using the correct data type. It also lets them perform type checking so you can't accidentally add a Bicycle object to a list of Employees.
Using this kind of class, you can build a list of integers, strings, or what have you.
List is one of the generic collection classes defined by the .NET Framework. The following code declares and initializes a List:
List<string> names = new List<string>();
The <string> part of the declaration indicates that the class will work with strings. You can put strings into the list and take strings out of it. You cannot add an integer to the list, just as you can't set a string variable equal to an integer. Visual Studio knows that the list works with strings and won't let you use anything else.
Note that IntelliSense knows about generic classes and provides help. If you begin a declaration with List, IntelliSense displays List<> to let you know that it is a generic class.
Now if you type the opening pointy bracket, IntelliSense displays a list of the class's type parameters and even describes them as you type. (The List class has only one type parameter but some, such as Dictionary, have more.) After you finish the declaration, the class knows what data types it will manipulate, and it can behave as if it were designed with that data type in mind.
Now, with some understanding of generic classes, you're ready to look at some generic collection classes.
Lists
A List is a simple ordered list of items. You can declare and initialize a List as in the following code:
List<string> names = new List<string>();
The List class provides several methods for manipulating the items it contains. The three most important are Add, Remove, and RemoveAt:
· The Add method adds a new item to the end of the list, automatically resizing the List if necessary. This is easier than adding an item to an array, which requires you to resize the array first.
· The Remove method removes a particular item from the list. Note that you pass the target item to Remove, not the index of the item that you want to remove. If you know that the string Zaphod is in the list names, the following code removes the first instance of that name from the list:
names.Remove("Zaphod");
NOTE
The Remove method removes only the first occurrence of an item from the List.
· The RemoveAt method removes an item from a particular position in the list. It then compacts the list to remove the hole where the item was. This is much easier than removing an item from an array, which requires you to shuffle items from one part of the array to another and then resize the array to reduce its size.
In addition to these methods, you can use square brackets to get and set a List's entries much as you can with an array. For example, the following code sets and then displays the value of the first entry in a list:
names[0] = "Mickey";
MessageBox.Show("The first name is " + names[0]);
Note that this works only if the index you use exists in the list. If the list holds 10 names and you try to set the 14th, the program crashes.
SortedLists
A SortedList stores a list of key/value pairs, keeping the list sorted by the keys. The types of the keys and values are generic parameters, so, for example, you could make a list that uses numbers (such as employee IDs) for keys and strings (such as names) for values.
Note that the list will not allow you to add two items with the same key. Multiple items can have the same value, but if you try to add two with the same key, the program crashes.
Table 16.2 summarizes useful methods provided by the SortedList class.
Table 16.2
Method |
Purpose |
Add |
Adds a key and value to the list. |
Clear |
Empties the list. |
Contains |
Returns true if the list contains a given value. |
ContainsKey |
Returns true if the list contains a given key. |
ContainsValue |
Returns true if the list contains a given value. |
GetKeyList |
Returns a list holding the keys. |
GetValueList |
Returns a list holding the values. |
Remove |
Removes the item with a specific key from the list. |
In addition to these methods, you can use square brackets to index into the list, using the items' keys as indexes.
The following code demonstrates a SortedList:
SortedList<string, string> addresses =
new SortedList<string, string>();
addresses.Add("Dan", "4 Deer Dr, Bugville VT, 01929");
addresses.Add("Bob", "8273 Birch Blvd, Bugville VT, 01928");
addresses["Cindy"] = "32878 Carpet Ct, Bugville VT, 01929";
addresses["Alice"] = "162 Ash Ave, Bugville VT, 01928";
addresses["Bob"] = "8273 Bash Blvd, Bugville VT, 01928";
MessageBox.Show("Bob's address is " + addresses["Bob"]);
The code starts by declaring and initializing a list to use keys and values that are both strings. It uses the Add method to add some entries and then uses square brackets to add some more.
Next the code uses the square bracket syntax to update Bob's address. Finally the code displays Bob's new address.
You can't see it from this example, but unlike the List class, SortedList actually stores its items ordered by key. For example, you could use the GetKeyList and GetValueList methods to get the list's keys and values in that order.
Dictionaries
The Dictionary and SortedDictionary classes provide features similar to the SortedList class, manipulating key/value pairs. The difference is in the data structures the three classes use to store their items.
Without getting into technical details, the results are that the three classes use different amounts of memory and work at different speeds. In general, SortedList is the slowest but takes the least memory. Dictionary is the fastest but takes the most memory.
For small programs, the difference is insignificant. For big programs that work with thousands of entries, you might need to be more careful about picking a class. (Personally I like Dictionary for most purposes because speed is nice, memory is relatively cheap, and the name is suggestive of the way you use the class: to look up something by key.)
Queues
A Queue is a collection that lets you add items at one end and remove them from the other. It's like the line at a bank where you stand at the back of the line and the teller helps the person at the front of the line until eventually it's your turn.
NOTE
Because a queue retrieves items in first-in-first-out order, queues are sometimes called FIFO lists or FIFOs. (“FIFO” is pronounced fife-o.)
Table 16.3 summarizes the Queue's most important methods.
Table 16.3
Method |
Purpose |
Clear |
Removes all items from the Queue. |
Dequeue |
Returns the item at the front of the Queue and removes it. |
Enqueue |
Adds an item to the back of the Queue. |
Peek |
Returns the item at the front of the Queue without removing it. |
Stacks
A Stack is a collection that lets you add items at one end and remove them from the same end. It's like a stack of books on the floor: you can add a book to the top of the stack and remove a book from the top, but you can't pull one out of the middle or bottom without risking a collapse.
NOTE
Because a stack retrieves items in last-in-first-out order, stacks are sometimes called LIFO lists or LIFOs. (“LIFO” is pronounced life-o.)
The top of a stack is also sometimes called its head. The bottom is sometimes called its tail.
Table 16.4 summarizes the Stack's most important methods.
Table 16.4
Method |
Purpose |
Clear |
Removes all items from the Stack. |
Peek |
Returns the item at the top of the Stack without removing it. |
Pop |
Returns the item at the top of the Stack and removes it. |
Push |
Adds an item to the top of the Stack. |
Try It
In this Try It, you use a Dictionary to build the order lookup program shown in Figure 16.4. When the user clicks the Add button, the program adds a new item with the given order ID and items. If the user enters an order ID and clicks Find, the program retrieves the corresponding items. If the user enters an order ID and some items and then clicks Update, the program updates the order's items.
Figure 16.4
Lesson Requirements
In this lesson, you:
· Create the form shown in Figure 16.4.
· Add code that creates a Dictionary field named Orders. Set its generic type parameters to int (for order ID) and string (for items).
· Add code to the Add button that creates the new entry in the dictionary.
· Add code to the Find button that retrieves the appropriate entry from the dictionary.
· Add code to the Update button to update the indicated entry.
WARNING 16.9
This program will be fairly fragile and will crash if you don't enter an order ID, enter an ID that is not an integer, try to enter the same ID twice, try to find a nonexistent ID, and so on. Don't worry about these problems. You learn how to handle them later, notably in Lessons 18 and 21.
NOTE
You can download the code and resources for this lesson from the website at www.wrox.com/go/csharp24hourtrainer2e.
Step-by-Step
· Create the form shown in Figure 16.4.
1. This is relatively straightforward. The only tricks are to set the Items TextBox's MultiLine and AcceptsReturn properties to true.
· Add code that creates a Dictionary named Orders. Set its generic type parameters to int (for order ID) and string (for items).
1. Use code similar to the following to make the Orders field:
2. // The dictionary to hold orders.
3. private Dictionary<int, string> Orders =
new Dictionary<int, string>();
· Add code to the Add button that creates the new entry in the dictionary.
1. This code should call the Dictionary's Add method passing it the order ID and items entered by the user. The Dictionary's order ID must be an integer so use int.Parse to convert the value entered by the user into an int.
Optionally you can add code to clear the TextBoxes to get ready for the next entry.
The code could be similar to the following:
// Add an order.
private void addButton_Click(object sender, EventArgs e)
{
// Add the order data.
Orders.Add(int.Parse(orderIdTextBox.Text), itemsTextBox.Text);
// Get ready for the next one.
orderIdTextBox.Clear();
itemsTextBox.Clear();
orderIdTextBox.Focus();
}
· Add code to the Find button that retrieves the appropriate entry from the dictionary.
1. Use code similar to the following:
2. // Look up an order.
3. private void findButton_Click(object sender, EventArgs e)
4. {
5. itemsTextBox.Text = Orders[int.Parse(orderIdTextBox.Text)];
}
· Add code to the Update button to update the indicated entry.
1. Use code similar to the following:
2. // Update an order.
3. private void updateButton_Click(object sender, EventArgs e)
4. {
5. Orders[int.Parse(orderIdTextBox.Text)] = itemsTextBox.Text;
}
Exercises
1. Make a program similar to the Fibonacci program that looks up factorials in an array. When the program starts, make it create the array to hold the first 20 factorials. Use the following definition for the factorial (where N! means the factorial of N):
2. 0! = 1
N! = N * (N - 1)!
Hint: For testing purposes, make sure the program can calculate 0! and 20! without crashing.
3. Make a program that demonstrates a Stack of Strings. The program should display a TextBox and two Buttons labeled Push and Pop. When the user clicks Push, add the current text to the stack. When the user clicks Pop, remove the next item from the stack and display it in the TextBox.
4. Copy the program you wrote for Exercise 2 and modify it so the Pop button is disabled when the Stack is empty. Hint: Use the Stack's Count property.
5. Copy the program you wrote for Exercise 3 and modify it so it displays the Stack's contents in a ListBox with the most recently added item at the top of the ListBox. Hint: Use the ListBox's Insert and RemoveAt methods to update its contents as you add and remove items from the Stack.
6. Make a program similar to the one you built for Exercise 4 except demonstrating a Queue instead of a Stack. Give the Buttons the captions Enqueue and Dequeue instead of Push and Pop. Make the ListBox display the items with the most recently added item at the bottom.
7. Make a program similar to the one you built for this lesson's Try It except make it store appointment information. The Dictionary should use the DateTime type for keys and the string type for values. Let the user pick dates from a DateTimePicker.
Hint: When the DateTimePicker first starts, it defaults to the current time, which may include fractional seconds. After the user changes the control's selection, however, the value no longer includes fractional seconds. That makes it hard to search for the exact same date and time later, at least if the user enters a value before changing the control's initial value.
To avoid this problem, when the form loads, initialize the DateTimePicker to a value that doesn't include fractional seconds. Use the properties provided by DateTime.Now to create a new DateTime and set the DateTimePicker's Value property to that.
8. Make a day planner application. The code should make an array of 31 strings to hold each day's plan. Initialize the array to show fake plans such as “Day 1.”
Use a ComboBox to let the user select a day of the month. When the ComboBox's value changes, display the corresponding day's plan in a large TextBox on the form.
Hint: Use the ComboBox's SelectedIndex property as an index into the array. Note that this program doesn't let the user enter or modify the plan, it just displays hardcoded values. To let the user modify the plan, you would need Find and Update buttons similar to those used in other exercises.
9. [Games] Copy the program you wrote for Exercise 6-13 (or download the version available on the book's website) and add a two-dimensional array of characters to track the board's position. Initially set the entries to a space character.
To test the code, set a breakpoint at the beginning of the code that handles the File menu's New command. Run the program and select all of the squares. Then invoke the New menu item and use the debugger to view the array.
Hint: Use code similar to the following to reset the array when the user starts a new game:
Board = new char[,]
{
{' ', ' ', ' ' },
{' ', ' ', ' ' },
{' ', ' ', ' ' },
};
10.[Games, WPF] Repeat Exercise 7 with the program you wrote for Exercise 6-14 (or the version downloaded from the book's website).
11.Use a Dictionary to make a simple phone book that lets the user add and look up name and phone number pairs.
12.[Hard] Make an image lookup program similar to the one shown in Figure 16.5. When the user clicks the PictureBox, let the user select an image file from an OpenFileDialog. Use code similar to the following to display the selected image:
imagePictureBox.Image = new Bitmap(imageOpenFileDialog.FileName);
Figure 16.5
Enable the Add Button when the TextBox and PictureBox are non-blank. When the user clicks Add, add the PictureBox's image to a Dictionary with the name as its key, add the name to the ListBox, and blank the TextBox and PictureBox. (Blank the PictureBox by setting its Image property to null.)
Finally, when the user clicks a name in the ListBox, display the corresponding name and picture.
13.[Hard] Make a simple bank account register like the one shown in Figure 16.6.
Figure 16.6
The program should have these features:
· Make a Dictionary to hold account balances with integer account numbers as keys.
· Enable the Buttons when both TextBoxes contain non-blank text.
· When the user clicks Create, add the account number and amount to the dictionary and display the new data in the ListBox.
· When the user clicks Credit:
§ Parse the account number and use the Dictionary to get the account's current balance.
§ Use the account number and balance to find the index of the account's entry in the ListBox.
§ Add the new amount to the account's balance in the Dictionary.
§ Remove the account's entry in the ListBox.
§ Insert a new entry for the account's new balance in the ListBox at the same position as the old entry.
· When the user clicks a ListBox entry, display the account number and balance in the TextBoxes.
14.Write a program that lets the user enter text in the following format:
1,200 Gummy slugs @ $0.02 = $24.00
When the user clicks Parse, the program should use the string class's Split method to get the item's name, price each, and total price. It should then add the values to a ListBox.
Hint: The Split method can take as a parameter an array of delimiters. (That makes parsing a lot easier.)
15.[Hard] Write a palindrome checker. Whenever the user modifies the text in a TextBox, the program should display a Label that indicates whether the text is a palindrome. Hints:
· Use two Labels, one that says “A Palindrome” and one that says “Not A Palindrome.” Use a boolean expression to set their Visible properties appropriately.
· To see if the string is a palindrome:
§ Remove commas, periods, and spaces.
§ Then convert the text into lowercase. (I'll call this the processed string.)
§ Use the string's ToCharArray method to get an array containing the string's characters.
§ Use Array.Reverse to reverse the array.
§ Use code similar to the following to convert the reversed characters into a string:
string reversed = new string(chars);
· Compare the processed string and the reversed string.
· Test the program on the two palindromes, “Able was I ere I saw Elba,” and “A man, a plan, a canal, Panama.”
NOTE
Please select the videos for Lesson 16 online at www.wrox.com/go/csharp24hourtrainer2evideos.