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:
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
:
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:
- Declare conformance to the protocol.
- 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:
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:
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:
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:
print(card)
This works in string interpolation too:
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:
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:
print("Dealer has \(dealer.cards[0]).")
Here, the compiler uses DefaultStringInterpolation
to concatenate the following pieces:
- The string literal
"Dealer has "
. - The
Card
instancedealer.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:
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:
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
:
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:
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
:
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:
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:
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:
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 (!=
):
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:
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:
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:
var grades: [String: Int] = [:]
Now that you have a Student
type, you should use that instead of String
:
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:
protocol Hashable: Equatable {
func hash(into hasher: inout Hasher)
}
This protocol has two requirements:
- A type that conforms to
Hashable
must also conform toEquatable
. The protocol specifies this requirement by declaring conformance toEquatable
. This feature is known as protocol inheritance. - 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 aHasher
, 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:)
:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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.