Using Arrays in Microsoft® Visual C#® 2013

  • 11/15/2013

Using multidimensional arrays

The arrays shown so far have contained a single dimension, and you can think of them as simple lists of values. You can create arrays with more than one dimension. For example, to create a two-dimensional array, you specify an array that requires two integer indexes. The following code creates a two-dimensional array of 24 integers called items. If it helps, you can think of the array as a table with the first dimension specifying a number of rows, and the second specifying a number of columns.

int[,] items = new int[4, 6];

To access an element in the array, you provide two index values to specify the “cell” holding the element. (A cell is the intersection of a row and a column.) The following code shows some examples using the items array:

items[2, 3] = 99;           // set the element at cell(2,3) to 99
items[2, 4] = items [2,3];  // copy the element in cell(2, 3) to cell(2, 4)
items[2, 4]++;              // increment the integer value at cell(2, 4)

There is no limit on the number of dimensions that you can specify for an array. The next code example creates and uses an array called cube that contains three dimensions. Notice that you must specify three indexes to access each element in the array.

int[, ,] cube = new int[5, 5, 5];
cube[1, 2, 1] = 101;
cube[1, 2, 2] = cube[1, 2, 1] * 3;

At this point, it is worth giving a word of caution about creating arrays with more than three dimensions. Specifically, arrays can consume a lot of memory. The cube array contains 125 elements (5 * 5 * 5). A four-dimensional array for which each dimension has a size of 5 contains 625 elements. If you start to create arrays with three or more dimensions, you can soon run out of memory. Therefore, you should always be prepared to catch and handle OutOfMemoryException exceptions when you use multidimensional arrays.

Creating jagged arrays

In C#, ordinary multidimensional arrays are sometimes referred to as rectangular arrays. Each dimension has a regular shape. For example, in the following tabular two-dimensional items array, every row has a column containing 40 elements, and there are 160 elements in total:

int[,] items = new int[4, 40];

As mentioned in the previous section, multidimensional arrays can consume a lot of memory. If the application uses only some of the data in each column, allocating memory for unused elements is a waste. In this scenario, you can use a jagged array, for which each column has a different length, like this:

int[][] items = new int[4][];
int[] columnForRow0 = new int[3];
int[] columnForRow1 = new int[10];
int[] columnForRow2 = new int[40];
int[] columnForRow3 = new int[25];
items[0] = columnForRow0;
items[1] = columnForRow1;
items[2] = columnForRow2;
items[3] = columnForRow3;
...

In this example, the application requires only 3 elements in the first column, 10 elements in the second column, 40 elements in the third column, and 25 elements in the final column. This code illustrates an array of arrays—rather than items being a two-dimensional array, it has only a single dimension, but the elements in that dimension are themselves arrays. Furthermore, the total size of the items array is 78 elements rather than 160; no space is allocated for elements that the application is not going to use.

It is worth highlighting some of the syntax in this example. The following declaration specifies that items is an array of arrays of int.

int[][] items;

The following statement initializes items to hold four elements, each of which is an array of indeterminate length:

items = new int[4][];

The arrays columnForRow0 to columnForRow3 are all single-dimensional int arrays, initialized to hold the required amount of data for each column. Finally, each column array is assigned to the appropriate elements in the items array, like this:

items[0] = columnForRow0;

Recall that arrays are reference objects, so this statement simply adds a reference to columnForRow0 to the first element in the items array; it does not actually copy any data. You can populate data in this column either by assigning a value to an indexed element in columnForRow0 or by referencing it through the items array. The following statements are equivalent:

columnForRow0[1] = 99;
items[0][1] = 99;

You can extend this idea further if you want to create arrays of arrays of arrays rather than rectangular three-dimensional arrays, and so on.

In the following exercise, you will use arrays to implement an application that deals playing cards as part of a card game. The application displays a form with four hands of cards dealt at random from a regular (52-card) pack of playing cards. You will complete the code that deals the cards for each hand.

Use arrays 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 Press\Visual CSharp Step By Step\Chapter 10\Windows X\Cards folder in your Documents folder.

  3. On the Debug menu, click Start Debugging to build and run the application.

    A form appears with the caption Card Game, four text boxes (labeled North, South, East, and West), and a button with the caption Deal.

    If you are using Windows 7, the form looks like this:

    If you are using Windows 8.1, the Deal button is on the app bar rather than on the main form, and the application looks like this:

  4. Click Deal.

    Nothing happens. You have not yet implemented the code that deals the cards; this is what you will do in this exercise.

  5. Return to Visual Studio 2013. On the Debug menu, click Stop Debugging.

  6. In Solution Explorer, locate the Value.cs file. Open this file in the Code and Text Editor window.

    This file contains an enumeration called Value, which represents the different values that a card can have, in ascending order:

    enum Value { Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten, Jack, Queen, King,
    Ace }
  7. Open the Suit.cs file in the Code and Text Editor window.

    This file contains an enumeration called Suit, which represents the suits of cards in a regular pack:

    enum Suit { Clubs, Diamonds, Hearts, Spades }
  8. Display the PlayingCard.cs file in the Code and Text Editor window.

    This file contains the PlayingCard class. This class models a single playing card.

    class PlayingCard
    {
        private readonly Suit suit;
        private readonly Value value;
        public PlayingCard(Suit s, Value v)
        {
            this.suit = s;
            this.value = v;
        }
        public override string ToString()
        {
            string result = string.Format("{0} of {1}", this.value, this.suit);
            return result;
        }
        public Suit CardSuit()
        {
            return this.suit;
        }
        public Value CardValue()
        {
            return this.value;
        }
    }

    This class has two readonly fields that represent the value and suit of the card. The constructor initializes these fields.

    The class contains a pair of methods called CardValue and CardSuit that return this information, and it overrides the ToString method to return a string representation of the card.

  9. Open the Pack.cs file in the Code and Text Editor window.

    This file contains the Pack class, which models a pack of playing cards. At the top of the Pack class are two public const int fields called NumSuits and CardsPerSuit. These two fields specify the number of suits in a pack of cards and the number of cards in each suit. The private cardPack variable is a two-dimensional array of PlayingCard objects. You will use the first dimension to specify the suit and the second dimension to specify the value of the card in the suit. The randomCardSelector variable is a random number generated based on the Random class. You will use the randomCardSelector variable to help shuffle the cards before they are dealt to each hand.

    class Pack
    {
        public const int NumSuits = 4;
        public const int CardsPerSuit = 13;
        private PlayingCard[,] cardPack;
        private Random randomCardSelector = new Random();
        ...
    }
  10. Locate the default constructor for the Pack class. Currently, this constructor is empty apart from a // TODO: comment. Delete the comment, and add the following statement shown in bold to instantiate the cardPack array with the appropriate values for each dimension:

    public Pack()
    {
        this.cardPack = new PlayingCard[NumSuits, CardsPerSuit];
    }
  11. Add the following code shown in bold to the Pack constructor. These statements populate the cardPack array with a full, sorted deck of cards.

    public Pack()
    {
        this.cardPack = new PlayingCard[NumSuits, CardsPerSuit];
        for (Suit suit = Suit.Clubs; suit <= Suit.Spades; suit++)
        {
            for (Value value = Value.Two; value <= Value.Ace; value++)
            {
                this.cardPack[(int)suit, (int)value] = new PlayingCard(suit, value);
            }
        }
    }

    The outer for loop iterates through the list of values in the Suit enumeration, and the inner loop iterates through the values each card can have in each suit. The inner loop creates a new PlayingCard object of the specified suit and value, and adds it to the appropriate element in the cardPack array.

  12. Find the DealCardFromPack method in the Pack class. The purpose of this method is to pick a random card from the pack, remove the card from the pack to prevent it from being selected again, and then pass it back as the return value from the method.

    The first task in this method is to pick a suit at random. Delete the comment and the statement that throws the NotImplementedException exception from this method and replace them with the following statement shown in bold:

    public PlayingCard DealCardFromPack()
    {
        Suit suit = (Suit)randomCardSelector.Next(NumSuits);
    }

    This statement uses the Next method of the randomCardSelector random number generator object to return a random number corresponding to a suit. The parameter to the Next method specifies the exclusive upper bound of the range to use; the value selected is between 0 and this value minus 1. Note that the value returned is an int, so it has to be cast before you can assign it a Suit variable.

    There is always the possibility that there are no more cards left of the selected suit. You need to handle this situation and pick another suit if necessary.

  13. After the code that selects a suit at random, add the while loop that follows (shown in bold).

    This loop calls the IsSuitEmpty method to determine whether there are any cards of the specified suit left in the pack (you will implement the logic for this method shortly). If not, it picks another suit at random (it might actually pick the same suit again) and checks again. The loop repeats the process until it finds a suit with at least one card left.

    public PlayingCard DealCardFromPack()
    {
        Suit suit = (Suit)randomCardSelector.Next(NumSuits);
        while (this.IsSuitEmpty(suit))
        {
            suit = (Suit)randomCardSelector.Next(NumSuits);
        }
    }
  14. You have now selected a suit at random with at least one card left. The next task is to pick a card at random in this suit. You can use the random number generator to select a card value, but as before, there is no guarantee that the card with the chosen value has not already been dealt. However, you can use the same idiom as before: call the IsCardAlreadyDealt method (which you will examine and complete later) to determine whether the card has been dealt before, and if so, pick another card at random and try again, repeating the process until a card is found. To do this, add the following statements shown in bold to the DealCardFromPack method, after the existing code:

    public PlayingCard DealCardFromPack()
    {
        ...
        Value value = (Value)randomCardSelector.Next(CardsPerSuit);
        while (this.IsCardAlreadyDealt(suit, value))
        {
            value = (Value)randomCardSelector.Next(CardsPerSuit);
        }
    }
  15. You have now selected a random playing card that has not been dealt previously. Add the following code to the end of the DealCardFromPack method to return this card and set the corresponding element in the cardPack array to null:

    public PlayingCard DealCardFromPack()
    {
        ...
        PlayingCard card = this.cardPack[(int)suit, (int)value];
        this.cardPack[(int)suit, (int)value] = null;
        return card;
    }
  16. Locate the IsSuitEmpty method. Remember that the purpose of this method is to take a Suit parameter and return a Boolean value indicating whether there are any more cards of this suit left in the pack. Delete the comment and the statement that throws the NotImplementedException exception from this method, and add the following code shown in bold:

    private bool IsSuitEmpty(Suit suit)
    {
        bool result = true;
        for (Value value = Value.Two; value <= Value.Ace; value++)
        {
            if (!IsCardAlreadyDealt(suit, value))
            {
                result = false;
                break;
            }
        }
        return result;
    }

    This code iterates through the possible card values and determines whether there is a card left in the cardPack array that has the specified suit and value by using the IsCardAlreadyDealt method, which you will complete in the next step. If the loop finds a card, the value in the result variable is set to false and the break statement causes the loop to terminate. If the loop completes without finding a card, the result variable remains set to its initial value of true. The value of the result variable is passed back as the return value of the method.

  17. Find the IsCardAlreadyDealt method. The purpose of this method is to determine whether the card with the specified suit and value has already been dealt and removed from the pack. You will see later that when the DealFromPack method deals a card, it removes the card from the cardPack array and sets the corresponding element to null. Replace the comment and the statement that throws the NotImplementedException exception in this method with the code shown in bold:

    private bool IsCardAlreadyDealt(Suit suit, Value value)
    {
        return (this.cardPack[(int)suit, (int)value] == null);
    }

    This statement returns true if the element in the cardPack array corresponding to the suit and value is null, and it returns false otherwise.

  18. The next step is to add the selected playing card to a hand. Open the Hand.cs file, and display it in the Code and Text Editor window. This file contains the Hand class, which implements a hand of cards (that is, all cards dealt to one player).

    This file contains a public const int field called HandSize, which is set to the size of a hand of cards (13). It also contains an array of PlayingCard objects, which is initialized by using the HandSize constant. The playingCardCount field will be used by your code to keep track of how many cards the hand currently contains as it is being populated.

    class Hand
    {
        public const int HandSize = 13;
        private PlayingCard[] cards = new PlayingCard[HandSize];
        private int playingCardCount = 0;
        ...
    }

    The ToString method generates a string representation of the cards in the hand. It uses a foreach loop to iterate through the items in the cards array and calls the ToString method on each PlayingCard object it finds. These strings are concatenated together with a newline character in between (the \n character) for formatting purposes.

    public override string ToString()
    {
        string result = "";
        foreach (PlayingCard card in this.cards)
        {
            result += card.ToString() + "\n";
        }
        return result;
    }
  19. Locate the AddCardToHand method in the Hand class. The purpose of this method is to add the playing card specified as the parameter to the hand. Add the following statements shown in bold to this method:

    public void AddCardToHand(PlayingCard cardDealt)
    {
        if (this.playingCardCount >= HandSize)
        {
            throw new ArgumentException("Too many cards");
        }
        this.cards[this.playingCardCount] = cardDealt;
        this.playingCardCount++;
    }

    This code first checks to ensure that the hand is not already full. If the hand is full, it throws an ArgumentException exception (this should never occur, but it is good practice to be safe). Otherwise, the card is added to the cards array at the index specified by the playingCardCount variable, and this variable is then incremented.

  20. In Solution Explorer, expand the MainWindow.xaml node and then open the MainWindow.xaml.cs file in the Code and Text Editor window.

    This is the code for the Card Game window. Locate the dealClick method. This method runs when the user clicks the Deal button. Currently, it contains an empty try block and an exception handler that displays a message if an exception occurs.

  21. Add the following statement shown in bold to the try block:

    private void dealClick(object sender, RoutedEventArgs e)
    {
        try
        {
            pack = new Pack();
        }
        catch (Exception ex)
        {
            ...
        }
    }

    This statement simply creates a new pack of cards. You saw earlier that this class contains a two-dimensional array holding the cards in the pack, and the constructor populates this array with the details of each card. You now need to create four hands of cards from this pack.

  22. Add the following statements shown in bold to the try block:

    try
    {
        pack = new Pack();
        for (int handNum = 0; handNum < NumHands; handNum++)
        {
            hands[handNum] = new Hand();
        }
    }
    catch (Exception ex)
    {
        ...
    }

    This for loop creates four hands from the pack of cards and stores them in an array called hands. Each hand is initially empty, so you need to deal the cards from the pack to each hand.

  23. Add the following code shown in bold to the for loop:

    try
    {
        ...
        for (int handNum = 0; handNum < NumHands; handNum++)
        {
            hands[handNum] = new Hand();
            for (int numCards = 0; numCards < Hand.HandSize; numCards++)
            {
                PlayingCard cardDealt = pack.DealCardFromPack();
                hands[handNum].AddCardToHand(cardDealt);
            }
        }
    }
    catch (Exception ex)
    {
        ...
    }

    The inner for loop populates each hand by using the DealCardFromPack method to retrieve a card at random from the pack and the AddCardToHand method to add this card to a hand.

  24. Add the following code shown in bold after the outer for loop:

    try
    {
        ...
        for (int handNum = 0; handNum < NumHands; handNum++)
        {
            ...
        }
        north.Text = hands[0].ToString();
        south.Text = hands[1].ToString();
        east.Text = hands[2].ToString();
        west.Text = hands[3].ToString();
    }
    catch (Exception ex)
    {
        ...
    }

    When all the cards have been dealt, this code displays each hand in the text boxes on the form. These text boxes are called north, south, east, and west. The code uses the ToString method of each hand to format the output.

    If an exception occurs at any point, the catch handler displays a message box with the error message for the exception.

  25. On the Debug menu, click Start Debugging. When the Card Game window appears, click Deal.

    The cards in the pack should be dealt at random to each hand, and the cards in each hand should be displayed on the form, as shown in the following image:

  26. Click Deal again. Verify that a new set of hands is dealt and the cards in each hand change.

  27. Return to Visual Studio and stop debugging.