Protocols in Swift
Part 4 of a series on Swift enums, pattern matching, and generics. Previous post.
Parts of this blog post are adapted from a talk I gave at the Swift Language Users Group.
Having covered Swift’s pattern-matching switch statement to some level of detail, we now turn our attention to another one of Swift’s major features: generics. In order to understand how generics work in Swift, though, it is first necessary to understand Swift’s protocols.
Protocol basics
Those who have worked with Objective-C or other object-oriented languages might already be familiar with protocols. For example, protocols in Java are known as interfaces. For those who aren’t as familiar with the concept, the following brief overview will explain what protocols are and how they work.
A protocol can be thought of as a promise or contract that a type (specifically, a class, struct, or enum) makes with the compiler. Protocols may be empty, or they may have one or more method or property signatures. The following example declares a simple protocol:
protocol MyProtocol {
var someProperty : Int { get set }
func someMethod(x: Int, y: Int) -> String
}
Let’s break this protocol declaration down:
- The protocol is declared with the
protocol
keyword, and its name isMyProtocol
- The first item the protocol defines is an
Int
-typed property namedsomeProperty
; this property must be readable and writable - The second item the protocol defines is a method that takes two
Int
s as arguments and returns aString
- The protocol declares interfaces for its property and method, but it doesn’t define how they are implemented
Now that we have a protocol, what can we do with it? It turns out that protocols exist so that other types can conform to them. A type can declare that it conforms to one or more protocols. When it does so, it is promising the compiler that it will provide implementations for all the methods and properties declared in those protocols.
Let us take the following class:
class MyClass {
// nothing here...
}
…and make it conform to MyProtocol
. To declare that a type conforms to one or more protocols, a colon is inserted after the type name, and the protocols are listed after the colon. (If the type is a class that has a superclass, the protocols are listed after the superclass.)
class MyClass : MyProtocol {
// still nothing here...
}
Are we done yet? We are not: the previous snippet won’t compile. This is because we’ve made MyClass
promise that it will provide implementations for the methods and properties in MyProtocol
. But MyClass
doesn’t actually have any code to implement either the method or property. The compiler sees that MyClass
isn’t fulfilling the promise it made, and so it will complain.
How can we rectify this? We can add code to implement the property and method in MyProtocol
:
class MyClass : MyProtocol {
var myProperty : Int = 0
func someMethod(x: Int, y: Int) -> String {
return "your numbers are \(x) and \(y)"
}
}
Now the compiler will consider the promise satisfied and compile our code. It’s important to note that, although the protocol defined what properties and methods that conforming types had to implement, it was up to MyClass
to decide how it was going to implement those properties and methods for itself.
Using protocols
How can we leverage the fact that a given type conforms to some given protocol?
Properties, variables, and constants in Swift are typed. We are accustomed to giving them concrete types:
let townName : String = "West Meoley"
var population : Int = 3000
func printMyNumber(myNumber: Int) {
println("my number is \(myNumber)")
}
However, we can also declare a property, variable, or constant with a protocol as its type:
// 'Printable' is a protocol that defines a 'description' property, which is a
// description of the object
var itemToPrint : Printable
itemToPrint = 10.0
println(itemToPrint.description)
itemToPrint = false
println(itemToPrint.description)
func printMyItem(myItem : Printable) {
println("My item is " + myItem.description)
}
If a property, variable, or constant is declared with a protocol as its type, we have access to all the methods and properties that protocol defines - and nothing else.
We can also declare a property, variable, or constant whose type must satisfy multiple protocols, using protocol<ProtocolA, ProtocolB, ProtocolC, ...>
:
// 'DebugPrintable' is a protocol that defines a 'debugDescription' property
func printMyItem(myItem : protocol<Printable, DebugPrintable>) {
let desc = myItem.description
let debugDesc = myItem.debugDescription
println("My item is described as \"\(desc)\", and its debug description is \"\(debugDesc)\"")
}
struct Point : Printable, DebugPrintable {
let x : Int
let y : Int
var description : String { return "(\(x), \(y))" }
var debugDescription : String { return "x value: \(x), y value: \(y)" }
}
printMyItem(Point(x: 10, y: 20))
// Prints to console:
// My item is described as "(10, 20)", and its debug description is "x value: 10, y value: 20"
If you try playing around with Swift’s built-in protocols, you might run into an error that reads something like Protocol 'Foo' can only be used as a generic constraint because it has Self or associated type requirements
. The next article in this series will discuss generics in detail, but suffice it to say that you cannot use the techniques described above to work with certain protocols. Instead, you must constrain your types using the generics system. A simple example follows:
// 'Equatable' is a protocol that allows types that implement it to be compared using ==
// This does NOT work
func equateThreeValues(x: Equatable, y: Equatable, z: Equatable) -> Bool {
return (x == y) && (y == z)
}
// Instead, you must do this
func equateThreeValues<T : Equatable>(x: T, y: T, z: T) -> Bool {
return (x == y) && (y == z)
}
A future article will discuss why this restriction actually makes sense (and why the first, invalid example is actually semantically incorrect) in more detail.
Motivation
Hopefully, it is now clear that:
- Protocols may define methods and properties (or be empty)
- Types can choose to conform to protocols
- A type that conforms to a protocol must provide implementations for that protocol’s methods and properties
- If given a variable (constant, property) of protocol type, that protocol’s methods and properties can be invoked upon the variable
But the question remains: why are protocols useful?
Just as protocols forced you to fulfill a promise by implementing some methods or properties, a protocol also serves as a promise to you, the developer, that some object or value is capable of doing something. For example, if you have an object that conforms to the Printable
protocol, you don’t need to care whether it’s an Int
, a Double
, a Range
, an Array
, or something else; you can be assured that it will have a description
property you can call to get a string describing the object.
Parent-child relationships
One practical use of protocols is to define flexible parent-child relationships.
For example, the UITableView
class in Cocoa implements a list-style UI widget (a table view) consisting of some number of rows, called cells. The table view needs some way to figure out how many rows it should display, exactly what cell it should display for a given row, what to do when a given row is tapped, and so forth. For now, let’s just worry about how the table view knows how many rows to display.
If we were inventing UITableView
, one way we might solve this problem is by defining a UITableViewDataSource
class with the method numberOfRows() -> Int
. The table view would then have a dataSource
property of type UITableViewDataSource
:
class UITableViewDataSource {
func numberOfRows() -> Int {
fatalError("You must subclass this class and override this method!")
}
}
class UITableView {
var dataSource : UITableViewDataSource?
func renderTableView() {
let rowCount = dataSource.numberOfRows()
for rowIdx in 0..<rowCount {
// render the cell at index 'rowIdx'...
}
}
}
// MyDataSource is a *subclass* of UITableViewDataSource
class MyDataSource : UITableViewDataSource {
override func numberOfRows() -> Int {
return 10
}
}
let myTableView = UITableView()
myTableView.dataSource = MyDataSource()
Developers would subclass the UITableViewDataSource
class and override the numberOfRows()
method to return however many rows they wanted, and then assign to their table view’s dataSource
property an instance of this custom subclass. Later, the table view would query the subclass by calling the numberOfRows()
method and getting the number of rows it should display.
This solution works fine, but it forces our developers to make their data source objects subclasses of our UITableViewDataSource
class. This might be too restrictive, especially since the table view only really cares that it has a numberOfRows()
method to call on its dataSource
.
A better option would be to ditch our class and instead create a UITableViewDataSource
protocol instead. This protocol would declare the numberOfRows()
method. Then, our developers would be able to create any type of data source object, be it a class or struct, and declare that their data source conforms to our UITableViewDataSource
protocol. Finally, they’d implement the numberOfRows()
method in their data source object. Everything else would work the same way:
protocol UITableViewDataSource {
func numberOfRows() -> Int
}
class UITableView {
var dataSource : UITableViewDataSource?
func renderTableView() {
let rowCount = dataSource.numberOfRows()
for rowIdx in 0..<rowCount {
// render the cell at index 'rowIdx'...
}
}
}
// MyDataSource is a struct that *implements* UITableViewDataSource
struct MyDataSource : UITableViewDataSource {
func numberOfRows() -> Int {
return 10
}
}
let myTableView = UITableView()
myTableView.dataSource = MyDataSource()
In fact, the actual UITableView
class works very much like the hypothetical table view we’ve just described.
Aggregating functionality
Protocols can also be used to aggregate functionality for a given type.
For example, let’s say we are building a computer game, and we’re writing a struct to represent the player character. Player characters are named objects in the game, because the player has a name, and they can be healed (like non-player characters and monsters, but not items). Player characters can also be moved, since the player is a mighty hero/heroine of prophecy (and not a tree). A sketch of how we might implement such a model follows:
protocol Nameable {
var name : String { get }
}
protocol Healable {
func heal(hitPointsToHeal: Int)
}
protocol Moveable {
func move(direction: Direction, distance: Double)
}
// A PlayerCharacter has a name, therefore it conforms to Nameable
// A PlayerCharacter can be healed, therefore it conforms to Healable
// A PlayerCharacter can be moved, therefore it conforms to Moveable
class PlayerCharacter : Nameable, Healable, Moveable {
var name : String = // ...
func heal(hitPointsToHeal: Int) { /* ... */ }
func move(direction: Direction, distance: Double) { /* ... */ }
}
Many of the types included in the Swift standard library are defined in such a way. For example, the CFunctionPointer
struct in Swift, which represents a C function pointer, has the following signature:
struct CFunctionPointer<T> : Equatable, Hashable, NilLiteralConvertible {
// ...
}
This type conforms to three different protocols:
Equatable
, which means that you can compare twoCFunctionPointer
s using the==
operatorHashable
, which means that you can get aCFunctionPointer
’s hash value by checking itshashValue
property (this also means you can use it as a key in a dictionary)NilLiteralConvertible
, which means that the type has provided an initializer that allows Swift to convert anil
into aCFunctionPointer
representing a null pointer in C
A historical note: when used in an object-oriented context, Swift’s system of single inheritance and protocols is sometimes termed ‘single inheritance of implementation, multiple inheritance of interface’. What this means is that a class in Swift can inherit an interface for a method or property from multiple places (protocols, its superclass), but it can only inherit at most one implementation (from its superclass).
Heterogeneous collections
A third possible use for protocols is giving multiple types something in common, so they can be mixed and matched within the same array or dictionary. (More generally, this can be viewed as allowing various types to all be described by the same generic type parameter, a topic which will be covered in more detail in the forthcoming article on generics.)
Those who went through the previous posts may remember an example in which JSON data was represented using enums and associated types. What if we want a less cumbersome way to represent JSON, closer in spirit to Objective-C’s untyped collections?
// An empty protocol; it exists to mark different types as JSON types
protocol JSONType { }
// We can use extensions to make existing classes conform to additional protocols
extension String : JSONType { }
extension Double : JSONType { }
extension Bool : JSONType { }
extension Array : JSONType { }
extension Dictionary : JSONType { }
let b : [JSONType] = [10.1, "hello", "goodbye"]
let a : [JSONType] = [10.2, "foo", true, false, b]
This looks beautiful! We can create an array mixing and matching JSON types as easily as we could in Objective-C. However, there is a serious problem: all arrays and dictionaries are now considered valid JSON types, not just those containing JSON-typed objects! Unfortunately, Swift’s type system is not powerful enough to represent what we really want, which is something like:
// This is NOT valid Swift code
// Only Arrays which contain objects of JSONType are also JSONType conforming
extension Array<T where T : JSONType> : JSONType { }
Note that even though this JSON example isn’t practical, the technique itself is still valuable (as long as we remain cognizant of its limitations). For example, let’s say we wanted to collect the results of various computations in an array, and then print them all out once all the computations had completed:
// A custom type, representing a two-dimensional point
struct Point : Printable {
let x : Int
let y : Int
var description : String { return "(\(x), \(y))" }
}
// For some odd reason String isn't publicly declared as Printable
extension String : Printable {
public var description : String { return self }
}
let buffer : [Printable] = [1.0, "hello", true, Point(x: 10, y: 20)]
// Note that running this in a Playground won't print out Point's description correctly
println(buffer.description)
// Or...
for item in buffer {
println(item.description)
}
In this case, we are able to store our original results in buffer
, and then leverage the fact that they are all Printable
to print out their descriptions for the benefit of the end user.
More about protocols
Now that we have established both the fundamental operation and motivation behind protocols, a few notes on several important details about protocols follow.
Properties in protocols
Protocols can declare properties, as noted earlier, but only using var
. Properties can be declared as either get
(read-only) or get set
(read-write). They cannot be declared as write-only. A conforming type can choose to implement a property either as a stored property or a computed property.
Even if a protocol declares a property as get
, a conforming type can choose to implement that property as a read-write property (for example, by implementing a setter for a computed property, or by using a stored property).
Protocol inheritance
Protocols can inherit from other protocols. Unlike with classes, a protocol can inherit from multiple parent protocols. In the following example, P3
inherits from both P1
and P2
:
protocol P1 {
func foo()
}
protocol P2 {
func bar()
}
// Any type that implements P3 must implement not only baz(), but also foo()
// and bar()
protocol P3 : P1, P2 {
func baz()
}
If a type implements a protocol, it automatically conforms to that protocol’s parent protocols as well:
struct MyStruct : P3 {
func foo() { println("foo") }
func bar() { println("bar") }
func baz() { println("baz") }
}
func doSomething(x: P1) {
x.foo()
}
// MyStruct conforms to P3, but can be used as a P1 or P2
doSomething(MyStruct())
Extensions and protocols
Swift’s extensions are a way to add functionality to an existing type. Extensions can be used to add protocol conformance to an existing type, as demonstrated by this (admittedly contrived) example:
protocol NotEmptyProtocol {
var notEmpty : Bool { get }
}
extension Optional : NotEmptyProtocol {
var notEmpty : Bool {
switch self {
case .None: return false
case .Some: return true
}
}
}
extension Array : NotEmptyProtocol {
var notEmpty : Bool {
return !self.isEmpty
}
}
Another common use of extensions is to break the definition of a single type into several chunks, each logically organized around one or more protocols.
Initializers, subscripts, and operators
In addition to methods and properties, protocols can also define initializers, subscripts, and operators.
A protocol declares an initializer just like it would a method:
protocol InitWithIntegerProtocol {
init(integerValue: Int)
}
Protocols definining initializers are most useful in the context of generics, and will be further discussed in a forthcoming article.
A protocol declares a subscript using a combination of function and property notation, similar to how subscripts are declared in concrete types. Like with properties, subscripts can either be declared as get
or get set
.
protocol IntStringSubscriptable {
subscript(idx: Int) -> String { get set }
}
A protocol declares an operator just like it would a method. However, a conforming class does not implement the overloaded operator as a method; instead, it must be implemented as a global function with the proper types. For example, Swift’s Equatable
protocol is defined as follows:
protocol Equatable {
func ==(lhs: Self, rhs: Self) -> Bool
}
(Self
is a generic parameter that must be substituted with the conforming type’s own type.)
If a type wished to implement the Equatable
protocol, it would do so as follows:
struct Point : Equatable {
let x : Int
let y : Int
}
func ==(lhs: Point, rhs: Point) -> Bool {
return lhs.x == rhs.x && lhs.y == rhs.y
}
Notice how the ==
operator overload is defined outside the Point
struct, and how both lhs
and rhs
are of type Point
(in accordance with the protocol definition).
Class-only protocols
A protocol can be limited to only classes by listing class
before any parent protocols:
protocol ClassOnlyProtocol : class, ParentProtocol1, ParentProtocol2 {
//...
}
Trying to define a struct or enum that conforms to a class-only protocol will cause a compile-time error.
@objc
protocols
A protocol can be declared with the modifier @objc
:
@objc protocol LegacyProtocol {
//...
}
@objc
protocols have certain characteristics:
- They are exposed to Objective-C code as part of the interop system
- Only classes can conform to them; enums and structs cannot conform to a
@objc
protocol is
,as
, andas?
can be used to check or downcast a class to an@objc
protocol type- Methods and properties can be marked
optional
(which removes the requirement for conforming classes to implement them), and optional chaining can be used to try invoking an optional method or property
The Apple interop guide has more information on using @objc
protocols.
Thanks for reading this rather long post about Swift’s protocols! The next blog post in this series will discuss generics in Swift.