So far, this chapter’s examples have used arrays of value-type elements. This section uses random-number generation and an array of reference-type elements—namely, objects representing playing cards—to develop a class that simulates card shuffling and dealing. This class can then be used to implement apps that play card games. The exercises at the end of the chapter use the techniques developed here to build a poker app.
We first develop class Card
(Fig. 8.10), which represents a playing card that has a face (e.g., "Ace"
, "Deuce"
, "Three"
, …, "Jack"
, "Queen"
, "King"
) and a suit (e.g., "Hearts"
, "Diamonds"
, "Clubs"
, "Spades"
). Next, we develop class DeckOfCards
(Fig. 8.11), which creates a deck of 52 playing cards in which each element is a Card
object. Then we build an app (Fig. 8.12) that uses class DeckOfCards
’s card shuffling and dealing capabilities.
Card
and Getter-Only Auto-Implemented Properties Class Card
(Fig. 8.10) contains two auto-implemented string
properties—Face
and Suit
—that store references to the face value and suit name for a specific Card
. Prior to C# 6, auto-implemented properties required both a get
and a set
accessor. Face
and Suit
are declared as C# 6 getter-only auto-implemented properties, which client code can use only to get each property’s value. Such properties are read only. Getter-only auto-implemented properties can be initialized only either in their declarations or in all of the type’s constructors. Initializing an auto-implemented property in its declaration is another C# 6 feature known as auto-property initializers. To do so, follow the property declaration with an = and the initial value, as in
Type PropertyName { get; set; } = initializer;
You can also initialize instance variables in their declarations.
The constructor (lines 9–13) receives two string
s that it uses to initialize the class’s properties. Method ToString
(line 16)—implemented here as an expression-bodied method—creates a string
consisting of the Face
of the card, the string "of "
and the Suit
of the card (e.g., "Ace of Spades"
). An object’s ToString
method is called implicitly in many cases when the object is used where a string
is expected, such as
when Write
or WriteLine
outputs the object,
when the object is concatenated to a string
using the +
operator, or
when the object is inserted into a string
-interpolation expression.
ToString
also can be invoked explicitly to obtain a string
representation of an object.
ToString
must be declared with the header exactly as shown in line 16 of Fig. 8.10. We’ll explain the purpose of the override
keyword in Section 11.4.1.
DeckOfCards
Class DeckOfCards
(Fig. 8.11) creates and manages an array of Card
references. The named constant NumberOfCards
(line 10) specifies the number of Card
s in a deck (52). Line 11 declares an instance-variable named deck
that refers to an array of Card
objects with Number-OfCards
(52) elements—the elements of the deck
array are null
by default. Like simple-type array-variable declarations, the declaration of a variable for an array of objects (e.g., Card[] deck
) includes the type of the array’s elements, followed by square brackets and the name of the array variable. Class DeckOfCards
also declares int
instance variable current-Card
(line 12), representing the next Card
to be dealt from the deck
array. Note that we cannot use var
for type inference with NumberOfCards
, deck
and currentCard
, because they are not local variables of a method or property.
DeckOfCards
: ConstructorThe constructor uses a for
statement (lines 22–25) to fill the deck
array with Card
s. The for
statement initializes count
to 0
and loops while count
is less than deck.Length
, causing count
to take on each integer value from 0
to 51
(the indices of the deck
array). Each Card
is instantiated and initialized with two string
s—one from the faces
array at lines 17–18 (which contains the string
s "Ace"
through "King"
) and one from the suits
array at line 19 (which contains the string
s "Hearts"
, "Diamonds"
, "Clubs"
and "Spades"
). The calculation count % 13
(line 24) always results in a value from 0
to 12
—the 13 indices of array faces
. Similarly, the calculation count / 13
always results in a value from 0
to 3
—the four indices of array suits
. When the deck
array is initialized, it contains the Card
s with faces "Ace"
through "King"
in order for each suit. Note that we cannot use a foreach
loop in lines 22–25, because we need to modify each element of deck
.
DeckOfCards
: Shuffle
MethodMethod Shuffle
(lines 29–45) shuffles the Card
s in the deck. The method loops through all 52 Card
s and performs the following tasks:
For each Card
, a random number between 0
and 51
is picked to select another Card
.
Next, the current Card
object and the randomly selected Card
object are swapped in the array. This exchange is performed by the three assignments in lines 41–43. The extra variable temp
temporarily stores one of the two Card
objects being swapped.
The swap cannot be performed with only the two statements
deck[first] = deck[second];
deck[second] = deck[first];
If deck[first]
is the "Ace"
of "Spades"
and deck[second]
is the "Queen"
of "Hearts"
, then after the first assignment, both array elements contain the "Queen"
of "Hearts"
, and the "Ace"
of "Spades"
is lost—hence, the extra variable temp
is needed. After the for
loop terminates, the Card
objects are randomly ordered. Only 52 swaps are made in a single pass of the entire array, and the array of Card
objects is shuffled.
It’s recommended that you use a so-called unbiased shuffling algorithm for real card games. Such an algorithm ensures that all possible shuffled card sequences are equally likely to occur. A popular unbiased shuffling algorithm is the Fisher-Yates algorithm
http://en.wikipedia.org/wiki/Fisher–Yates_shuffle
This page also uses pseudocode to shows how to implement the algorithm.
DeckOfCards
: DealCard
MethodMethod DealCard
(lines 48–59) deals one Card
in the array. Recall that currentCard
indicates the index of the next Card
to be dealt (i.e., the Card
at the top of the deck). Thus, line 51 compares currentCard
to the length of the deck
array. If the deck
is not empty (i.e., currentCard
is less than 52
), line 53 returns the top Card
and increments current-Card
to prepare for the next call to DealCard
—otherwise, line 57 returns null
to indicate that the end of deck
has been reached.
Figure 8.12 demonstrates the card shuffling and dealing capabilities of class DeckOfCards
(Fig. 8.11). Line 10 of Fig. 8.12 creates a DeckOfCards
object named myDeckOfCards
and uses type inference to determine the variable’s type. Recall that the DeckOfCards
constructor creates the deck with the 52 Card
objects in order by suit and face. Line 11 invokes myDeckOfCards
’s Shuffle
method to rearrange the Card
objects. The for
statement in lines 14–22 deals all 52 Card
s in the deck and displays them in four columns of 13 Card
s each. Line 16 deals and displays a Card
object by invoking myDeckOfCards
’s DealCard
method. When a Card
object is placed in a string
-interpolation expression, the Card
’s To-String
method is invoked implicitly. Because the field width is negative, the result is displayed left-aligned in a field of width 19.