Classes
In this chapter, you’ll learn about a category of types known as classes. On the surface, classes are similar to structures, but there are plenty of differences under the hood.
To learn about classes, you’ll revisit the implementation of Blackjack from the previous exercises, and you’ll introduce classes into the code.
To get started, open the solution project for Exercises 8.
Reference types
Consider the code in main.swift. This code uses the game.player
property in several places. You may have tried to create a shorthand for this property:
var player = game.player
However, this won’t work. As explained in Structures, this assignment copies the instance in game.player
to player
. This means you now have two Player
instances, and changes to one don’t affect the other.
For this shorthand to work, you need a way to express that player
should be the same instance as game.player
, and not a copy. Classes make this possible.
To demonstrate how classes work, make the following changes:
Open Player.swift, and change the declaration of
Player
fromstruct Player
toclass Player
.Remove the
mutating
keyword from the methods inPlayer
because classes don’t use this keyword.Open main.swift, and declare variable
player
right after you initialize the game:swiftvar game = Game() var player = game.player
Replace all other mentions of
game.player
in main.swift withplayer
.
By changing Player
to a class, the variable player
now refers to the same instance as game.player
. This is because classes are reference types.
When you create an instance of a reference type and assign it to a variable, that variable stores only a reference to the instance. The instance itself is stored elsewhere in memory:
This diagram introduces several new notations:
- An arrow represents a reference to an instance of a class.
- This instance is drawn as a box labeled with the name of its class. The contents of the box show the properties of the instance.
- An ellipsis (
...
) indicates that the contents of a box have been omitted to simplify the diagram.
Because game.player
stores a reference and not an instance, the following assignment copies this reference to variable player
:
var player = game.player
Both variables now refer to the same instance:
This behavior is different from how structures and enumerations work. This is because structures and enumerations are value types.
When you create an instance of a value type, such as Game
, and assign it to a variable, that variable stores the instance:
When Player
was a structure, its instance was also stored in game
:
Assigning game.player
to player
copied this instance to a different variable:
As you can see, there is a fundamental difference in how value types and reference types behave. The next section explains why this difference exists.
Objects and identity
An instance of a class is known as an object. What separates objects from other instances is that they have an identity. No two objects are the same — even if they have the same values for all of their properties.
For example:
class Person {
var name: String
init(name: String) {
self.name = name
}
}
var person1 = Person(name: "Alice")
var person2 = Person(name: "Alice")
Even though person1
and person2
have the same name, they aren’t identical because they refer to different objects:
You can use the identity operators (===
and !==
) to check if two variables refer to the same object:
person1 === person2
person1 !== person2
Identity is not the same as equality:
- The identity operator (
===
) inspects an object’s identity. This is based on the object’s location in memory, so no two objects are identical. - The equality operator (
==
) inspects an object’s value. Objects with the same value aren’t identical, but they can be interchangeable.
For example, consider what happens when you assign each person a unique ID:
class Person {
let id: Int
var name: String
init(id: Int, name: String) {
self.id = id
self.name = name
}
}
var person1 = Person(id: 1, name: "Alice")
var person2 = Person(id: 1, name: "Alice")
Based on their values, person1
and person2
clearly represent the same person. Therefore, you can say that these objects are equal, even though they aren’t identical.
It’s up to you, the programmer, to decide when objects of your type are equal. You do this by implementing the equality operator for your type. You’ll learn how to do this in the next chapter.
Instances of a value type lack an identity; they only have a value. This explains why reference types and value types behave differently:
- Objects are unique. Therefore, classes must use references so that you can use a single, unique object in multiple parts of your code. Objects cannot be copied on assignment as this would create a new object with a different identity.
- Values aren’t unique. Therefore, structures and enumerations don’t need references. Values can be copied on assignment because it doesn’t matter which instance you use, as long as it has the correct value.
Mutability
Another important difference between objects and values is their mutability:
- Values are immutable. Mutating a value effectively replaces it with a different value.
- Objects are mutable. You can change an object’s value without changing its identity.
Immutable values
To better understand how values are immutable, consider the following example:
struct Counter {
private(set) var count = 0
mutating func increment() {
count += 1
}
}
var counter = Counter()
Here, Counter
is a structure, so counter
stores a value:
When you call counter.increment()
, you change this value:
Because values lack an identity, there’s no way to express that these are the same instance. All you can say is that counter
now holds a different value.
This means counter.increment()
is equivalent to:
counter = Counter(count: counter.count + 1)
Both statements replace the contents of counter
with an incremented value.
This equivalence between mutating a value and replacing it is why you can implement a mutating method by assigning a new value to self
:
mutating func increment() {
self = Counter(count: count + 1)
}
Mutable objects
Now consider what happens if you implement Counter
as a class instead:
class Counter {
private(set) var count = 0
func increment() {
count += 1
}
}
let counter = Counter()
Counter
is now a reference type, so counter
stores a reference to an object:
Calling counter.increment()
doesn’t affect this reference, so you can mutate the object without changing its identity:
This also means counter
can be a constant. A constant of a reference type only holds a reference, so you can mutate the object, provided that you don’t mutate the reference:
counter = Counter()
This is also why classes don’t use the mutating
keyword. They don’t have to distinguish between mutating and non-mutating methods because objects are always mutable.
Shared mutable state
Together, references and mutability enable shared mutable state. By implementing Counter
as a class, you can share a counter between multiple parts of your code. Any changes you make to the counter are visible by all code that holds a reference to it.
Shared mutable state is a powerful feature. Unfortunately, it also increases the complexity of your code. Through shared mutable state, issues in your code can affect other code that shares the same state. These issues can be hard to resolve because it’s not always clear which code owns the state and which code mutates it. This makes proper encapsulation hard to achieve.
For these reasons, you should only use shared mutable state when you really need it.
Initialization
Unlike structures, classes don’t receive an automatic memberwise initializer. In the following example, structure Color
relies on its memberwise initializer, whereas class Image
must declare its own:
struct Color {
let red: Int
let green: Int
let blue: Int
static let black = Color(red: 0, green: 0, blue: 0)
}
class Image {
var width: Int
var height: Int
private(set) var pixels: [Color]
init(width: Int, height: Int) {
self.width = width
self.height = height
pixels = Array(repeating: .black, count: width * height)
}
}
let image = Image(width: 200, height: 200)
Classes do receive an automatic default initializer if they assign a default value to every stored property:
class Image {
var width = 0
var height = 0
private(set) var pixels: [Color] = []
}
let image = Image()
As with structures, a class loses this automatic initializer when it declares its own initializers:
class Image {
var width = 0
var height = 0
private(set) var pixels: [Color] = []
init(width: Int, height: Int) {
self.width = width
self.height = height
pixels = Array(repeating: .black, count: width * height)
}
}
let image = Image()
Designated and convenience initializers
Classes must distinguish between regular initializers (designated initializers) and initializers that delegate to another initializer (convenience initializers). This distinction is important because it supports some of the advanced features of classes that you’ll learn about in a future course.
Initializers are designated by default. Convenience initializers have the convenience
keyword in their declaration:
class Image {
var width: Int
var height: Int
private(set) var pixels: [Color]
init(width: Int, height: Int) {
self.width = width
self.height = height
pixels = Array(repeating: .black, count: width * height)
}
convenience init(
copiedFrom other: Image,
at origin: (x: Int, y: Int),
width: Int,
height: Int
) {
self.init(width: width, height: height)
for row in 0..<height {
let source = (origin.y + row) * other.width + origin.x
let sourceRange = source..<(source + width)
let target = row * width
let targetRange = target..<(target + width)
pixels[targetRange] = other.pixels[sourceRange]
}
}
}
This convenience initializer creates a new image by copying part of an existing image. It first delegates to the designated initializer to create an empty image, then finds and copies the correct pixels from the source image.
Deciding between value types and reference types
When you declare a type, you have to decide whether you want a value type or a reference type. Unfortunately, this isn’t always an easy decision. Some types work equally well as a value type and as a reference type.
Use the following questions to decide whether a type should be a value type or a reference type:
- Do the instances of this type have an identity?
- Do you need shared mutable state?
If you answer yes to both questions, you need a class. Otherwise, use either a structure or an enumeration.
The remainder of this chapter reviews the types in Blackjack and decides whether each type should remain a value type or become a class.
Action
, Rank
, and Suit
These types have an enumerable set of values:
- An
Action
is eitherhit
,stand
,double
,split
,yes
, orno
. - A
Rank
is one oface
,two
,three
, and so on. - A
Suit
is eitherhearts
,diamonds
,spades
, orclubs
.
These values lack an identity. For example, you never need to distinguish between two instances of Suit
that have the value hearts
; only this value matters. Therefore, you don’t need a class, and an enumeration remains the appropriate choice for all three types.
Card
In this program, you only care about the rank and suit of a card, and these values are immutable. Therefore, a value type is appropriate.
Currently, Card
is a structure. However, it can also be an enumeration because it has only 52 possible values:
enum Card {
case aceOfHearts
case twoOfHearts
// ...
case kingOfClubs
}
This implementation of Card
removes the need for the Rank
and Suit
enumerations. However, it also makes it much harder to find the rank and suit of a card. The current implementation is much more practical, so Card
should remain a structure.
Deck
A Deck
is essentially just an array of cards with some added functionality. Therefore, you can argue that Deck
should behave similarly to Array
, which is a structure. However, you can also argue that decks have an identity and that they should be considered distinct, even if they contain the same cards. Both arguments are valid, so the question becomes whether or not you need shared mutable state.
The current implementation avoids shared mutable state by only mutating the deck in Game
. The deck isn’t shared with other types, such as Player
, which results in the following workaround:
player.receive(deck.draw())
Here, the game draws a card from the deck and passes it to the player. Ideally, this code would be:
player.draw(from: deck)
However, that’s not possible with value types because the player would draw from a copy of the deck, not the deck that’s stored in Game
.
To implement draw(from:)
, you need to make Deck
a class and use shared mutable state. Here’s how you do that:
In Deck.swift, change the declaration of
Deck
fromstruct Deck
toclass Deck
and remove themutating
keyword where it’s used.In Game.swift, change
deck
from a variable to a constant. This property now stores a reference, which you’re not going to change.In Player.swift, replace
receive
withdraw(from:)
:swiftfunc draw(from deck: Deck) { let card = deck.draw() currentHand.receive(card) }
In Game.swift, replace
player.receive(deck.draw())
with the more readable callplayer.draw(from: deck)
.
As you can see, implementing Deck
as a class results in clearer code. However, this doesn’t mean Deck
should definitely be a class. Both implementations have their pros and cons:
- Using a structure means
Deck
behaves like an array of cards. However, your code requires some workarounds because you can’t share state. - Using a class removes these workarounds. However, shared mutable state always adds complexity. The more you share state, the harder it becomes to figure out where you mutate it.
Hand
The same discussion applies to type Hand
. It’s essentially an array of cards, but it can also benefit from shared mutable state. For example, both Game
and Player
mutate the player’s hand. The current implementation requires careful programming to avoid mutating a copy of this hand.
To explore what’s possible with shared mutable state, convert Hand
to a class:
In Hand.swift, change the declaration of
Hand
fromstruct Hand
toclass Hand
and remove themutating
keyword where it’s used.In Player.swift, remove the
currentHand
property and replace any mentions ofcurrentHand
withhands[currentHandIndex]
. You’ll provide a different way of accessing the current hand in the next step.Add the following method to
Player
:swiftfunc nextHand() -> (index: Int, hand: Hand)? { if isFirstHand { isFirstHand = false } else if currentHandIndex == hands.count - 1 || hands[currentHandIndex + 1].isEmpty { return nil } else { currentHandIndex += 1 } return (currentHandIndex, hands[currentHandIndex]) }
You’ll use this method to iterate over the player’s hands. Every time you call
nextHand
, it returns a reference to the player’s next hand and that hand’s index, starting with the first hand. The method returnsnil
if the player has no hands remaining.nextHand
uses anisFirstHand
property to avoid skipping directly to the second hand. Add this property:swiftprivate var isFirstHand: Bool
Set
isFirstHand
totrue
in the initializer and theresetHands
method.Game
will usenextHand
to iterate over the player’s hands. This means you no longer need to exposecurrentHandIndex
. Make this property private.In Game.swift, change the
repeat-while
loop of theplay
method to the followingwhile
loop:swiftwhile let (index, hand) = player.nextHand() { // ... }
This loop iterates over the player’s hands, stopping when
nextHand
returnsnil
.Inside the loop, replace any mentions of
player.currentHand
withhand
. Also, remove any statements that incrementplayer.currentHandIndex
and replace the remaining mentions of this property withindex
.
As you can see, implementing Hand
as a class gives you more freedom in how you program your game. You no longer need to worry about mutating a local copy of a hand in Game
. Game
and Player
have references to the same hand, and all changes to this hand are visible through both references. Additionally, you can remove the doubleDown
method from Player
because Game
can now mutate the current hand directly.
However, this new implementation also has some drawbacks. For example, consider how you encapsulate hands
:
private(set) var hands: [Hand]
You declared this property as private(set)
to require other types to use methods when they want to mutate a hand. However, now that Hand
is a reference type, hands
becomes an array of references:
Declaring hands
as private(set)
only protects these references, not the objects they refer to. This means you can do the following in Game
:
let aceUpSleeve = Card(rank: .ace, suit: .spades)
player.hands[0].receive(aceUpSleeve)
This code doesn’t violate the encapsulation of hands
because it doesn’t add, remove, or mutate any references. However, it does violate the rules of the game. To fully encapsulate hands
in this implementation, you need to make it private and provide methods for the operations you wish to expose.
As was the case with Deck
, Hand
can be either a structure or a class. Both implementations have their benefits and drawbacks. In this chapter, we’ve explored converting these types to classes. However, most Swift developers will always favor structures, to avoid shared mutable state.
Player
Suppose you want to extend your game to support multiple players. It’s possible for players to have the same hand and the same number of credits. Does this make them the same player? Definitely not. This means players have an identity, and a class is appropriate for them.
You already converted Player
to a class in the introduction. Next, you’ll create a player in main.swift and use references to share it with Game
. Make the following changes:
In Game.swift, add a
player
parameter to the initializer:swiftinit(player: Player) { self.player = player deck = Deck(size: 4) dealer = Hand() }
Make the
player
property a private constant. It now holds a reference, and you’ll never need to refer to a different player.In main.swift, create a player and pass it to the game:
swiftlet player = Player(credits: Game.initialCredits) var game = Game(player: player)
Move the
initialCredits
constant fromGame
to main.swift, as that’s now the only place where you use it:swiftlet initialCredits = 500
Replace all mentions of
Game.initialCredits
withinitialCredits
.
Game
Suppose you want to develop your game into an online service. This service would have to serve multiple games simultaneously, perhaps even multiple games for a single player. This means it’s possible for two or more games to be in the same state. That definitely doesn’t make them the same game, so games have an identity, and a class is appropriate for them.
Change the declaration of Game
from struct Game
to class Game
and remove the mutating
keyword where it’s used. Additionally, you can simplify your code a bit by making Game
instances single-use:
In Game.swift, add
player.resetHands()
to the initializer. This makes sure the player is ready to start a new game.In
initializeFirstHand
, remove the code that resets the dealer’s hand and the player’s hands. This is no longer required.Rename
initializeFirstHand
todealFirstHand
to better convey its new purpose.Make
dealer
a constant because you no longer reassign it.In main.swift, remove the global variable
game
and initialize a new game inside the loop:swiftlet game = Game(player: player) game.play()
Now that Game
is a class, its instances have an identity, so it makes sense to create a new object for every game. This way, you don’t have to worry about properly resetting the game before every play.
Input
Type Input
is a special case. It doesn’t need any instances, so you don’t have to decide between a value type or a reference type. A caseless enumeration is the correct choice here.
Up next
In this chapter, you learned about classes and how they, as reference types, differ from structures and enumerations, which are value types.
Throughout the chapter, you made several changes to your implementation of Blackjack. Use the solution project for this chapter to verify your changes. Study this project carefully; you’ll need it for the upcoming exercises.
Complete the exercises first, then continue on the next chapter, where you’ll learn about protocols and extensions.