Skip to content

Protocols and Extensions

In this chapter, you’ll learn how protocols define shared functionality. You’ll explore some of the protocols in the Standard Library and learn how your types can benefit from adopting them.

You’ll also learn how extensions add functionality to an existing type. Extensions can also add protocol conformance, making protocols and extensions a powerful combination.

CustomStringConvertible

A protocol is a set of requirements that types can choose to implement. By conforming to a protocol, a type gets access to all of the functionality that is built around this protocol.

As an example, let’s discuss the requirements of the CustomStringConvertible protocol, how you can adopt it, and how you benefit from doing so.

Requirements

The CustomStringConvertible protocol has a single requirement:

swift
protocol CustomStringConvertible {

  var description: String { get }
}

Types that adopt this protocol must provide a description property of type String. This property should return a string that describes the instance.

The get keyword in the requirement specifies that description need only be readable. This means you can implement description with any of the following:

  • A constant stored property.
  • A variable stored property.
  • A read-only computed property.
  • A read-write computed property.

This shows that protocol requirements are minimum requirements only. A conforming type can add additional functionality, such as making description writable.

Adoption

Next, let’s conform type Card to CustomStringConvertible:

swift
struct Card {

  let rank: Rank
  let suit: Suit

  var description: String {
    "\(rank.rawValue)\(suit.rawValue)"
  }
}

To adopt a protocol, a type must do two things:

  1. Declare conformance to the protocol.
  2. Implement the protocol’s requirements.

Card already has the required description property, so all that’s left is to declare conformance. You do this as follows:

swift
struct Card: CustomStringConvertible {
  // ...
}

This is the same syntax you use to declare an enumeration’s raw type. When an enumeration has a raw type and also adopts one or more protocols, you declare all of these in a comma-separated list, starting with the raw type:

swift
enum Suit: String, CustomStringConvertible {

  case hearts = "♥"
  case diamonds = "♦"
  case spades = "♠"
  case clubs = "♣"
  
  var description: String {
    rawValue
  }
}

Benefits

By conforming to CustomStringConvertible, Card has access to all of the functionality that is built around this protocol.

For example, type String has a String(describing:) initializer that can describe any instance of any type. If that type conforms to CustomStringConvertible, the initializer uses the description property to get an appropriate string:

swift
let card = Card(rank: .ace, suit: .spades)
String(describing: card)

The print function also checks for CustomStringConvertible conformance. If you print an instance that conforms to CustomStringConvertible, you’ll see that instance’s description printed:

swift
print(card)

This works in string interpolation too:

swift
print("Dealer has \(dealer.cards[0]).")

Custom string interpolation

The CustomStringConvertible protocol is too limiting for some types. For example, type Hand also has a description member, but this member is a method that uses a parameter to configure the description:

swift
func description(allowBlackjack: Bool = true) -> String {
  let listOfCards = cards.map { $0.description }
                         .joined(separator: ", ")
  if allowBlackjack && isBlackjack {
    return "blackjack: \(listOfCards)"
  }
  return "\(isSoft ? "soft " : "")\(value): \(listOfCards)"
}

This description doesn’t satisfy the requirement of CustomStringConvertible, which requires a property, not a method.

For types that need more flexibility than CustomStringConvertible, the Standard Library includes a DefaultStringInterpolation type. This type is responsible for building interpolated strings.

For example:

swift
print("Dealer has \(dealer.cards[0]).")

Here, the compiler uses DefaultStringInterpolation to concatenate the following pieces:

  • The string literal "Dealer has ".
  • The Card instance dealer.cards[0].
  • The string literal ".".

DefaultStringInterpolation checks if Card conforms to CustomStringConvertible and uses the description property to convert the card to a string.

You can extend DefaultStringInterpolation to teach it about types such as Hand that don’t conform to CustomStringConvertible. You do this by declaring an extension on the type. In this extension, you declare an appendInterpolation method that takes an instance to convert and any parameters you require:

swift
extension DefaultStringInterpolation {
    
  mutating func appendInterpolation(_ hand: Hand, allowBlackjack: Bool = true) {
    appendLiteral(hand.description(allowBlackjack: allowBlackjack))
  }
}

This method calls the hand’s description method and passes the result to appendLiteral, which is an existing method on DefaultStringInterpolation that builds up a string piece by piece.

With this extension in place, you can use instances of Hand in string interpolation and provide a value for the allowBlackjack parameter:

swift
print("Dealer has \(dealer).")
print("Dealer has \(dealer, allowBlackjack: false).")

The parentheses around the interpolation act just like the parentheses of a call to appendInterpolation. Any external parameter names you declare in appendInterpolation show up as argument labels in the interpolation.

Using extensions for protocol conformance

Extensions add functionality — such as computed properties, methods, and initializers — to a type. They can even add protocol conformance.

The following example declares a class Student and an extension that conforms it to CustomStringConvertible:

swift
class Student {
    
  let id: Int
  var name: String
  
  init(id: Int, name: String) {
    self.id = id
    self.name = name
  }
}

extension Student: CustomStringConvertible {

  var description: String { name }
}

This extension declares conformance and provides the required description property. Using extensions in this way lets you split a type declaration into coherent pieces, making it easier to understand.

Equatable

In the previous chapter, you learned the difference between identity and equality:

  • Identity is a property of objects. Objects are uniquely identified by their location in memory. No two objects are identical, but you can have multiple references to the same object.
  • Equality pertains to the value of an instance. Equal instances are interchangeable.

It’s up to you, the programmer, to decide when instances of your type are equal. You do this by implementing the equality operator (==), which is a requirement of the Equatable protocol:

swift
protocol Equatable {

  static func == (lhs: Self, rhs: Self) -> Bool
}

Here, lhs and rhs are the left-hand side and right-hand side operands. Their type, Self, is the type that conforms to the protocol. In other words, an Equatable type must have a static == method that compares two instances of itself.

Here’s how you can conform class Student to Equatable:

swift
extension Student: Equatable {

  static func == (lhs: Student, rhs: Student) -> Bool {
    lhs.id == rhs.id
  }
}

This implementation assumes that each student has a unique ID. If lhs and rhs have the same id, then they must represent the same student, so the objects are interchangeable.

You can now use the equality operator to check if two objects represent the same student:

swift
let s1 = Student(id: 1, name: "Alice")
let s2 = Student(id: 1, name: "Alice")
s1 == s2

For value types, an implementation of == should compare all of the type’s stored properties. Instances of a value type don’t have an identity, so they must be equal when they have the same value.

For example, two cards are equal when they have the same rank and the same suit:

swift
extension Card: Equatable {
    
  static func == (lhs: Card, rhs: Card) -> Bool {
    lhs.rank == rhs.rank &&
    lhs.suit == rhs.suit
  }
}

Because this is such a predictable implementation, the compiler can synthesize it for you. All you need to do is declare conformance:

swift
extension Card: Equatable { }

This feature requires that all of the type’s stored properties are themselves Equatable. Also, you must declare conformance in the original type declaration or using an extension in the same file. This is because the compiler may need to compare private properties, which are inaccessible outside of the original source file.

The equality operator isn’t the only requirement specified in the Equatable protocol. This protocol also includes the inequality operator (!=):

swift
static func != (lhs: Self, rhs: Self) -> Bool

However, the Standard Library provides a default implementation for this operator that inverts the result of the equality operator:

swift
static func != (lhs: Self, rhs: Self) -> Bool {
  !(lhs == rhs)
}

Therefore, implementing the equality operator is sufficient to conform to Equatable.

Conforming to Equatable also opens up new functionality in the Standard Library. For example, arrays have contains and firstIndex(of:) methods that rely on the equality operator to search for an element in the array:

swift
let aceOfSpades = Card(rank: .ace, suit: .spades)
dealer.cards.contains(aceOfSpades)
dealer.cards.firstIndex(of: aceOfSpades)

These methods are only available when the type of the elements conforms to Equatable.

Hashable

In Dictionaries, you learned that dictionaries store pairs of keys and values. One of the examples in that chapter was a dictionary of grades for students:

swift
var grades: [String: Int] = [:]

Now that you have a Student type, you should use that instead of String:

swift
var grades: [Student: Int] = [:]  

However, this won’t work. The compiler tells you that Student should conform to the Hashable protocol. Why is that?

Dictionaries rely on hash codes to efficiently store and find keys. When you store a key in a dictionary, the dictionary computes the hash code for that key and uses it as an index to store the key. Likewise, when you look up a key, the dictionary computes the hash code for that key to find its index.

Your types must support hash codes if you want to use them as the key type in a dictionary. You do this by conforming to the Hashable protocol:

swift
protocol Hashable: Equatable {

  func hash(into hasher: inout Hasher)
}

This protocol has two requirements:

  1. A type that conforms to Hashable must also conform to Equatable. The protocol specifies this requirement by declaring conformance to Equatable. This feature is known as protocol inheritance.
  2. A hash(into:) method. The purpose of this method is to simplify the calculation of a hash code. Instead of computing a code yourself, you feed the properties you want to include in the calculation to a Hasher, which then computes a code on your behalf.

Note

The Hasher is an inout parameter. You’ll learn what this means in the next chapter.

Type Student already conforms to Equatable, so all that’s left is to implement hash(into:):

swift
extension Student: Hashable {
    
  func hash(into hasher: inout Hasher) {
    hasher.combine(id)
  }
}

As you can see, you use the combine method to tell the hasher which properties you want to include in the calculation. These must be the same properties you use to implement ==. Equal instances must have the same hash code, so you must use id, and only id, to compute this hash code.

Now that Student conforms to Hashable, you can use it as the key type in a dictionary:

swift
var grades: [Student: Int] = [:]

For value types, an implementation of hash(into:) should combine all of the type’s stored properties. Fortunately, the compiler can synthesize this implementation for you, so all you have to do is declare conformance:

swift
extension Card: Hashable { }

As with Equatable, this feature requires that all of the type’s stored properties also conform to Hashable and that the conformance is added to the original type declaration or to an extension in the same file.

Comparable

The Comparable protocol inherits from Equatable and adds the remaining comparison operators as requirements:

swift
protocol Comparable: Equatable {

  static func < (lhs: Self, rhs: Self) -> Bool
  static func <= (lhs: Self, rhs: Self) -> Bool
  static func > (lhs: Self, rhs: Self) -> Bool
  static func >= (lhs: Self, rhs: Self) -> Bool
}

The Standard Library provides default implementations for <=, >, and >=, so the only operators you have to implement are the lesser-than operator (<) and the equality operator (==) from Equatable.

Here’s an implementation of the lesser-than operator that compares students by name, in ascending alphabetical order:

swift
extension Student: Comparable {
    
  static func < (lhs: Student, rhs: Student) -> Bool {
    lhs.name < rhs.name
  }
}

Together with the earlier conformance to Equatable, this enables all of the comparison operators:

swift
let alice = Student(id: 1, name: "Alice")
let bob = Student(id: 2, name: "Bob")
alice == bob
alice != bob
alice < bob
alice <= bob
alice > bob
alice >= bob

Conforming to Comparable also opens up new functionality in the Standard Library. For example, arrays can sort themselves using the lesser-than operator to compare elements:

swift
var students = [bob, alice]
students.sort()

The Comparable protocol expresses a natural ordering of a type’s instances. When no such ordering exists — or when there’s more than one — you may not want to adopt Comparable. In these cases, it’s better to make the ordering explicit by using the sort and sorted methods that take a closure to compare elements:

swift
students.sort { $0.id < $1.id }

CaseIterable

When using an enumeration, you often need to list all of its cases. For example, to create a deck of cards, you need to list all the possible suits and ranks:

swift
var cards: [Card] = []
for suit in [Suit.hearts, .diamonds, .spades, .clubs] {
  for rank in [Rank.ace, .two, .three, .four, .five,
               .six, .seven, .eight, .nine,
               .ten, .jack, .queen, .king] {
    cards.append(Card(rank: rank, suit: suit))
  }
}

The CaseIterable protocol makes this much easier. If an enumeration declares conformance to CaseIterable, the compiler synthesizes a static allCases property for it:

swift
enum Suit: CaseIterable {

  case hearts
  case diamonds
  case spades
  case clubs
}

Suit.allCases

enum Rank: CaseIterable {

  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
}

Rank.allCases

You can use this property to iterate over all the cases in the enumeration:

swift
var cards: [Card] = []
for suit in Suit.allCases {
  for rank in Rank.allCases {
    cards.append(Card(rank: rank, suit: suit))
  }
}

Up next

This chapter introduced you to protocols by exploring some of the protocols in the Standard Library. You’ll revisit protocols in a future course, where you’ll learn how to create and use your own protocols.

In the next chapter, you’ll wrap up some loose ends and complete the topics for this course. Keep going. You’re almost ready for your final challenge.