Skip to content

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:

swift
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:

  1. Open Player.swift, and change the declaration of Player from struct Player to class Player.

  2. Remove the mutating keyword from the methods in Player because classes don’t use this keyword.

  3. Open main.swift, and declare variable player right after you initialize the game:

    swift
    var game = Game()
    var player = game.player
  4. Replace all other mentions of game.player in main.swift with player.

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:

  1. An arrow represents a reference to an instance of a class.
  2. 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.
  3. 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:

swift
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:

swift
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:

swift
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:

swift
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:

swift
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:

swift
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:

swift
mutating func increment() {
  self = Counter(count: count + 1)
}

Mutable objects

Now consider what happens if you implement Counter as a class instead:

swift
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:

swift
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:

swift
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:

swift
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:

swift
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:

swift
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:

  1. Do the instances of this type have an identity?
  2. 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 either hit, stand, double, split, yes, or no.
  • A Rank is one of ace, two, three, and so on.
  • A Suit is either hearts, diamonds, spades, or clubs.

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:

swift
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:

swift
player.receive(deck.draw())

Here, the game draws a card from the deck and passes it to the player. Ideally, this code would be:

swift
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:

  1. In Deck.swift, change the declaration of Deck from struct Deck to class Deck and remove the mutating keyword where it’s used.

  2. In Game.swift, change deck from a variable to a constant. This property now stores a reference, which you’re not going to change.

  3. In Player.swift, replace receive with draw(from:):

    swift
    func draw(from deck: Deck) {
      let card = deck.draw()
      currentHand.receive(card)
    }
  4. In Game.swift, replace player.receive(deck.draw()) with the more readable call player.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:

  1. In Hand.swift, change the declaration of Hand from struct Hand to class Hand and remove the mutating keyword where it’s used.

  2. In Player.swift, remove the currentHand property and replace any mentions of currentHand with hands[currentHandIndex]. You’ll provide a different way of accessing the current hand in the next step.

  3. Add the following method to Player:

    swift
    func 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 returns nil if the player has no hands remaining.

  4. nextHand uses an isFirstHand property to avoid skipping directly to the second hand. Add this property:

    swift
    private var isFirstHand: Bool
  5. Set isFirstHand to true in the initializer and the resetHands method.

  6. Game will use nextHand to iterate over the player’s hands. This means you no longer need to expose currentHandIndex. Make this property private.

  7. In Game.swift, change the repeat-while loop of the play method to the following while loop:

    swift
    while let (index, hand) = player.nextHand() {
      // ...
    }

    This loop iterates over the player’s hands, stopping when nextHand returns nil.

  8. Inside the loop, replace any mentions of player.currentHand with hand. Also, remove any statements that increment player.currentHandIndex and replace the remaining mentions of this property with index.

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:

swift
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:

swift
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:

  1. In Game.swift, add a player parameter to the initializer:

    swift
    init(player: Player) {
      self.player = player
      deck = Deck(size: 4)
      dealer = Hand()
    }
  2. Make the player property a private constant. It now holds a reference, and you’ll never need to refer to a different player.

  3. In main.swift, create a player and pass it to the game:

    swift
    let player = Player(credits: Game.initialCredits)
    var game = Game(player: player)
  4. Move the initialCredits constant from Game to main.swift, as that’s now the only place where you use it:

    swift
    let initialCredits = 500
  5. Replace all mentions of Game.initialCredits with initialCredits.

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:

  1. In Game.swift, add player.resetHands() to the initializer. This makes sure the player is ready to start a new game.

  2. In initializeFirstHand, remove the code that resets the dealer’s hand and the player’s hands. This is no longer required.

  3. Rename initializeFirstHand to dealFirstHand to better convey its new purpose.

  4. Make dealer a constant because you no longer reassign it.

  5. In main.swift, remove the global variable game and initialize a new game inside the loop:

    swift
    let 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.