Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

10. Protocols and Generics

A protocol is a list of things a type must provide. A generic is a function or type written once and used with many types. They are the same idea approached from two directions, and together they are how Swift avoids the inheritance hierarchies that other object-oriented languages use for code reuse.

10.1 Protocols

A protocol names requirements: methods, properties with get or get set, initializers, and associated types. A type conforms by providing them, and says so by listing the protocol after a colon.

// A protocol is a contract. An extension on it is a default implementation.
protocol Shape {
    var area: Double { get }
    var perimeter: Double { get }
    var name: String { get }
}

extension Shape {
    var name: String { "\(type(of: self))".lowercased() }

    var summary: String {
        "\(name): area \(rounded(area)), perimeter \(rounded(perimeter))"
    }

    private func rounded(_ x: Double) -> String {
        "\(Double(Int(x * 100)) / 100)"
    }
}

struct Circle: Shape {
    let radius: Double
    var area: Double { 3.14159265 * radius * radius }
    var perimeter: Double { 2 * 3.14159265 * radius }
}

struct Rectangle: Shape {
    let width: Double
    let height: Double
    var area: Double { width * height }
    var perimeter: Double { 2 * (width + height) }
}

struct Square: Shape {
    let side: Double
    var area: Double { side * side }
    var perimeter: Double { 4 * side }
    var name: String { "square (a rectangle really)" }
}

let shapes: [any Shape] = [
    Circle(radius: 1),
    Rectangle(width: 3, height: 4),
    Square(side: 2),
]

for shape in shapes {
    print(shape.summary)
}

print("total area", shapes.reduce(0) { $0 + $1.area })
circle: area 3.14, perimeter 6.28
rectangle: area 12.0, perimeter 14.0
square (a rectangle really): area 4.0, perimeter 8.0
total area 19.14159265

The extension on the protocol is where Swift differs from the interfaces you know. It can supply a default implementation for a requirement, which name uses, so conforming types get one without writing it and Square overrides it. And it can add methods that are not requirements at all, which summary is: every Shape has it, no Shape declares it, and a conforming type cannot replace it in a way that is seen through the protocol.

That distinction catches people. A requirement is dispatched dynamically, so Square’s name is the one you get whether you are holding a Square or a Shape. A method that exists only in the extension is dispatched statically, so a type that defines its own summary gets its version when the static type is Square and the extension’s when the static type is Shape. Put a requirement in the protocol whenever conformers are meant to be able to change it.

Protocols compose. protocol Drawable: Shape inherits requirements, and func draw(_ x: some Shape & Codable) demands both without either knowing about the other. A protocol may be constrained to classes with : AnyObject, which is what lets a conforming reference be weak.

10.2 Protocols the Standard Library Cares About

Conforming to these is how your type joins the language rather than sitting beside it.

Equatable gives ==, Hashable gives use as a dictionary key or set element, Comparable gives < and therefore sorted(), min() and max(). CustomStringConvertible gives a description used by print and by string interpolation. Codable gives conversion to and from JSON and other formats. Sequence gives for-in and a hundred methods with it.

For most of them the compiler writes the conformance:

// The compiler writes Equatable, Hashable and Codable for you.
import Foundation

struct Point: Equatable, Hashable, Codable {
    var x: Int
    var y: Int
}

struct Track: Codable {
    var name: String
    var points: [Point]
    var closed: Bool
}

let track = Track(
    name: "loop",
    points: [Point(x: 0, y: 0), Point(x: 3, y: 4), Point(x: 0, y: 0)],
    closed: true
)

print(Point(x: 1, y: 2) == Point(x: 1, y: 2))
print(Set(track.points).count)

let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys]
let data = try encoder.encode(track)
print(String(decoding: data, as: UTF8.self))

let restored = try JSONDecoder().decode(Track.self, from: data)
print(restored.name, restored.points.count, restored.closed)

// Retroactive conformance: teach a type you do not own a protocol it lacks.
extension Point: Comparable {
    static func < (a: Point, b: Point) -> Bool {
        (a.x * a.x + a.y * a.y) < (b.x * b.x + b.y * b.y)
    }
}
print(track.points.sorted().map(\.x))
true
2
{"closed":true,"name":"loop","points":[{"x":0,"y":0},{"x":3,"y":4},{"x":0,"y":0}]}
loop 3 true
[0, 0, 3]

You write : Equatable, Hashable, Codable and nothing else, provided every stored property already conforms. Track says Codable and gets JSON in both directions for a nested array of structs, with no field list repeated anywhere and therefore nothing to forget to update. Where the JSON field names differ from your property names you add a CodingKeys enum, and that is the only part you usually have to write.

The last lines are retroactive conformance: an extension in your code can make somebody else’s type conform to a protocol, including a protocol you defined. That is how you adapt a type you do not own without wrapping it, and it is the move that protocol-oriented design is built on. Conforming a type you do not own to a protocol you do not own is the one case to avoid, because two libraries doing it differently is a conflict nobody can resolve.

Exercise 10-1. Give Shape a scaled(by:) requirement and implement it for all three types. Then move it to the extension with a default that does nothing sensible, and see which mistakes each version lets you make.

Exercise 10-2. Make Track encode its closed field under the JSON name is_closed without renaming the property.

10.3 Generics

A generic function names one or more type parameters in angle brackets and constrains them to the protocols it needs.

// One function, any element type, with the constraints written down.
func firstDuplicate<T: Hashable>(in items: [T]) -> T? {
    var seen: Set<T> = []
    for item in items {
        if !seen.insert(item).inserted {
            return item
        }
    }
    return nil
}

func largest<C: Collection>(of items: C) -> C.Element? where C.Element: Comparable {
    guard var best = items.first else { return nil }
    for item in items {
        if item > best { best = item }
    }
    return best
}

print(firstDuplicate(in: [3, 1, 4, 1, 5]) ?? -1)
print(firstDuplicate(in: ["a", "b", "c"]) ?? "none")
print(largest(of: [3, 9, 2]) ?? 0)
print(largest(of: "hello") ?? " ")
print(largest(of: [Int]()) ?? -1)

// A generic type. Stack<T> is a real type for each T you use it with.
struct Stack<Element> {
    private var items: [Element] = []

    var isEmpty: Bool { items.isEmpty }
    var count: Int { items.count }

    mutating func push(_ item: Element) {
        items.append(item)
    }

    mutating func pop() -> Element? {
        items.popLast()
    }
}

extension Stack: CustomStringConvertible where Element: CustomStringConvertible {
    var description: String {
        "[" + items.map(\.description).joined(separator: " ") + "]"
    }
}

var s = Stack<Int>()
for n in [1, 2, 3] { s.push(n) }
print(s, s.pop() ?? 0, s)
1
none
9
o
-1
[1 2 3] 3 [1 2]

<T: Hashable> reads “for any type T that is Hashable”. The constraint is not decoration: without it, Set<T> would not compile, because a set needs to hash its elements. Swift checks the body of a generic function once, against the constraints, rather than once per use as C++ templates do, so an error in a generic function is reported where the function is written.

largest constrains a whole collection rather than an array, which is why it works on a String, whose elements are Character and are Comparable. The where clause carries constraints that do not fit in the angle brackets, usually because they are about an associated type of a parameter rather than the parameter itself.

A generic type works the same way, and the conditional extension at the end is worth noticing: Stack is printable when its elements are, and not otherwise. That is how the standard library says an array is Equatable when its elements are, and it lets a type carry a capability it cannot always offer.

Swift’s generics are not erased. Stack<Int> stores its integers directly with no boxing, and the generated code can be specialized per type, so a generic abstraction costs nothing at runtime.

10.4 some and any

Two keywords appear in front of protocol names and they mean opposite things.

// Conform to Sequence and every for-in and every collection method works.
struct Countdown: Sequence, IteratorProtocol {
    var n: Int

    mutating func next() -> Int? {
        guard n > 0 else { return nil }
        defer { n -= 1 }
        return n
    }
}

for i in Countdown(n: 3) {
    print(i, terminator: " ")
}
print()
print(Array(Countdown(n: 6).filter { $0 % 2 == 1 }))
print(Countdown(n: 4).reduce(0, +))

// some names one concrete type without saying which; any boxes several.
protocol Animal {
    func speak() -> String
}

struct Dog: Animal { func speak() -> String { "woof" } }
struct Cat: Animal { func speak() -> String { "meow" } }

func pet() -> some Animal {
    Dog()
}

func pet(number n: Int) -> any Animal {
    n.isMultiple(of: 2) ? Dog() : Cat()
}

print(pet().speak())
print((0..<4).map { pet(number: $0).speak() })

// some works on a parameter too, and there means "any one type, decided by the caller".
func twice(_ animal: some Animal) -> String {
    animal.speak() + " " + animal.speak()
}
print(twice(Cat()))
3 2 1 
[5, 3, 1]
10
woof
["woof", "meow", "woof", "meow"]
meow meow

some Animal is one specific type, fixed at compile time, whose identity the caller is not told. pet() always returns a Dog, and a caller can rely on two calls returning the same type without knowing which, which is what makes it usable in a Set or compared with ==. It costs nothing at runtime.

any Animal is a box that can hold a Dog on one call and a Cat on the next. That flexibility needs indirection and a table of methods, so it costs an allocation and a dynamic call. pet(number:) needs it, because the type really does vary.

The rule: use some unless the type varies at runtime, then use any. An array of mixed conformers, as in [any Shape] earlier, is the ordinary case for any.

Countdown at the top shows how little a protocol conformance can be. Sequence needs one method, next(), returning the next element or nil, and in exchange the type gets for-in, map, filter, reduce, prefix, contains and the rest, all written once in extensions on Sequence. That is protocol extensions doing the work the standard library would otherwise have to repeat for every collection.

10.5 Asking What Is in the Box

A value held as any Shape has a concrete type underneath, and sometimes you need it. is asks, as? converts and hands back nil if the answer is no, and as! converts and crashes if the answer is no.

// Asking what a boxed value actually is.
protocol Shape {}
struct Circle: Shape { let radius: Double }
struct Square: Shape { let side: Double }

let shapes: [any Shape] = [Circle(radius: 1), Square(side: 2), Circle(radius: 3)]

for shape in shapes {
    if let circle = shape as? Circle {
        print("circle of radius \(circle.radius)")
    } else if shape is Square {
        print("some square")
    }
}

// switch does the same thing more neatly when there are several cases.
for shape in shapes {
    switch shape {
    case let c as Circle: print("r=\(c.radius)")
    case let s as Square: print("s=\(s.side)")
    default: print("unknown shape")
    }
}

print(shapes.compactMap { $0 as? Circle }.map(\.radius))
circle of radius 1.0
some square
circle of radius 3.0
r=1.0
s=2.0
r=3.0
[1.0, 3.0]

as? is the one to use, and it pairs with if let exactly as an optional does, because that is what it produces. as! earns the same warning try! and ! get in Chapters 5 and 9: right when the surrounding code has already established the answer, and a crash you chose to write everywhere else.

A switch with as patterns is the readable form when there are more than two possibilities, and it is also a signal. A long chain of casts usually means the protocol is missing a requirement: if every caller has to ask which shape it is holding, the thing they are asking for belongs on Shape itself, and Chapter 8’s closing section is the argument for when to reach for an enum instead.

There is also as with no punctuation, which is not a test at all but a compile-time conversion that is guaranteed to succeed, as in 5 as Double or value as Any.

Exercise 10-3. Write a Fibonacci sequence conforming to Sequence and take its first ten values. Decide what should happen when the numbers overflow.

Exercise 10-4. Change pet() to return any Animal and find out which of the calls in the program stop compiling, and why the some version allowed them.

Exercise 10-5. Add a peek to Stack returning the top without removing it, and a conditional conformance to Equatable.

Exercise 10-6. largest(of:) duplicates the standard library’s max(). Read its declaration and work out what each part of it is doing.