Enumerations
In this chapter, you’ll continue to improve the implementation of Tic-Tac-Toe and discover a new category of types known as enumerations.
Enumerated types
Let’s revisit the type Game
from the previous chapter. This type has a currentPlayer
property that tracks whose turn it is:
var currentPlayer: String
This property stores the current player’s symbol ("O"
or "X"
). However, because it’s a String
, it can also store other values, not just "O"
and "X"
— this can be problematic.
For example, consider how you switch players after every turn:
currentPlayer = currentPlayer == "O" ? "X" : "O"
Now consider what happens if you make a typo and set currentPlayer
to "0"
instead of "O"
:
currentPlayer = currentPlayer == "O" ? "X" : "0"
Because your code assumes that currentPlayer
can only be "O"
or "X"
, this small typo stops your game from working correctly.
To protect against this problem, currentPlayer
needs a more appropriate type than String
. Its type should express that it can have only two possible values: O or X. Such a type is called an enumerated type, or enumeration.
Declaration
An enumeration is a type that has a predefined set of values. You declare an enumeration with the keyword enum
. Inside the declaration, you enumerate the possible values with the case
keyword:
enum Player {
case o
case x
}
This declares an enumerated type Player
. Every instance of Player
must be either o
or x
; no other values are possible.
Enumeration cases
The possible values for an enumeration are known as its cases. Enumeration cases follow the same naming rules as variables, so they start with a lowercase letter.
You refer to the cases of Player
as Player.o
and Player.x
:
var currentPlayer: Player
currentPlayer = Player.o
If the compiler can infer the type of the enumeration, you can use the following shorthand:
currentPlayer = .o
This is the same shorthand you used for static properties in Structures.
Type safety
Enumerations provide additional type safety. Now that currentPlayer
is an enumeration instead of a String
, you can rely on the compiler to check your code:
currentPlayer = .O
The compiler knows this code is invalid because O
is not one of the cases of Player
.
You can find other examples of enumerations in Blackjack. Here’s how the solution to that challenge stores a card’s rank and suit:
typealias Card = (rank: String, suit: String)
As it turns out, this isn’t very safe. rank
and suit
accept any string, so you need to remember whether you store hearts as "hearts"
, "Hearts"
, or "♥️"
, and then not make any mistakes.
The possible values for rank
and suit
are enumerable, so you can create the following types:
enum Rank {
case ace
case two
case three
case four
case five
case six
case seven
case eight
case nine
case ten
case jack
case queen
case king
}
enum Suit {
case hearts
case diamonds
case spades
case clubs
}
By using these enumerations instead of String
, you no longer need to remember which values you’re using; the compiler checks your code and reports any mistakes:
typealias Card = (rank: Rank, suit: Suit)
var card: Card
card = (.ace, .spades)
card = (.two, .pikes)
Control flow with cases
After you’ve declared a variable of an enumeration type, you can use a switch
statement or expression to find out which value it contains:
func symbol(for player: Player) -> String {
switch player {
case .o: "O"
case .x: "X"
}
}
This function returns the correct symbol to print for each player.
The switch
expression in this example is exhaustive. It doesn’t require a default case because the compiler knows that player
can only be o
or x
.
Of course, if you only want to check for a specific case, you can use the equality operator (==
):
if currentPlayer == .o {
currentPlayer = .x
} else {
currentPlayer = .o
}
Properties, methods, and initializers
Enumerations and structures have a lot in common. Both are types with properties, methods, and initializers. However, unlike structures, enumerations cannot have stored properties. Instead of properties, you use cases to store the state of an enumeration.
To understand how this works, consider the following types:
struct Point {
var x: Double
var y: Double
}
enum Result {
case success
case failure
}
Here, the state of a Point
consists of a value for x
and a value for y
, whereas the state of a Result
is either success
or failure
. In other words, the state of a structure is a combination of all of its properties, whereas the state of an enumeration is a selection of one of its cases.
Enumerations can have computed properties, as these don’t store state. To implement a computed property, you can compare the value of self
against the possible cases:
enum Player {
// ...
var symbol: String {
switch self {
case .o: "O"
case .x: "X"
}
}
}
The same is true for methods:
enum Player {
// ...
func next() -> Player {
self == .o ? .x : .o
}
}
You can create a mutating method by assigning a different case to self
:
enum Player {
// ...
mutating func toggle() {
if self == .o {
self = .x
} else {
self = .o
}
}
}
This is also how you implement initializers. For example, here’s how you can initialize a player from their symbol:
enum Player {
// ...
init?(symbol: String) {
switch symbol {
case "O":
self = .o
case "X":
self = .x
default:
return nil
}
}
}
This initializer is failable and returns nil
if you pass in a symbol other than "O"
or "X"
.
Finally, enumerations can also have static members. Here’s a static method that returns a random player:
enum Player {
// ...
static func random() -> Player {
Bool.random() ? .o : .x
}
}
Raw values
Enumerations often replace types such as String
or Int
for values that can be enumerated. Unfortunately, you can’t always fully replace these types. For example, the cases of type Player
replace the strings "O"
and "X"
, but you still need these strings to print the board.
In situations like this, it’s helpful to declare an enumeration with a raw type. Here’s how you declare that Player
has a raw type of String
:
enum Player: String {
case o
case x
}
When an enumeration has a raw type, every enumeration case has an equivalent raw value of this type.
In the previous example, the raw values for o
and x
are "o"
and "x"
— the name of each case. You can override these defaults by assigning explicit raw values:
enum Player: String {
case o = "O"
case x = "X"
}
An enumeration with a raw type receives a rawValue
property and an init?(rawValue:)
initializer that you can use to convert between an enumeration case and its raw value:
currentPlayer.rawValue
currentPlayer = Player(rawValue: "X")!
This combination of a property and a failable initializer is similar to the symbol
property and init?(symbol:)
initializer from earlier. You can remove these now that you have a raw type.
Other than String
, the types you can use for raw values are Character
, Int
, Double
, and other number types.
For type Int
, the default raw values start at 0 and increment from one case to the next. If you assign some of the cases an explicit raw value, the compiler will increment from these values instead.
For example:
enum HTTPStatus: Int {
case ok = 200
case created
case noContent = 204
case moved = 301
case found
case badRequest = 400
case unauthorized
case forbidden = 403
case notFound
}
Here, the compiler assigns the following raw values:
created
: 201found
: 302unauthorized
: 401notFound
: 404
Raw values create a one-to-one mapping between an enumeration case and its equivalent raw value. Therefore, you cannot use raw values to map a card’s rank to its value in a game of Blackjack:
enum Rank: Int {
case ace = 1
case two
case three
case four
case five
case six
case seven
case eight
case nine
case ten
case jack = 10
case queen = 10
case king = 10
}
This code is invalid because cases ten
, jack
, queen
, and king
all have the same raw value.
You can implement a rank’s value as a computed property instead. You’ll do this in an upcoming exercise.
Caseless enumerations
In the previous chapter, you declared a type Input
and gave it a private initializer to prevent it from being instantiated:
struct Input {
private init() { }
static func readPosition(on board: Board) -> Int {
// ...
}
static func readBool(question: String) -> Bool {
// ...
}
}
You can achieve the same result with an enumeration:
enum Input {
static func readPosition(on board: Board) -> Int {
// ...
}
static func readBool(question: String) -> Bool {
// ...
}
}
As you can see, you can declare an enumeration without any cases. You cannot instantiate this enumeration because every instance must equal one of its cases, of which there are none.
A caseless enumeration is the best way to declare a type that shouldn’t be instantiated.
Up next
This chapter introduced a new category of types called enumerations. In the upcoming exercises, you’ll use enumerations to improve your implementation of Blackjack.
To prepare for these exercises, first study the solution project for this chapter. This project uses the Player
and Input
enumerations introduced here to improve the implementation of Tic-Tac-Toe.
After you’ve completed the exercises, continue on to the next chapter, where you’ll learn about classes and how they differ from structures and enumerations.
You’ll also revisit enumerations in Wrapping Up to learn about their advanced features.