Swift path · Abstraction and memory · Topic 5 of 13

Protocols and protocol extensions

Not started

A **protocol** describes what a type can do: a list of required methods, properties, initialisers and associated types. Structs, enums, classes and actors can all conform, which is why Swift code reaches for protocols where other languages use base classes. **Protocol extensions** then add implementations that every conforming type gets, so a protocol can carry shared behaviour without inheritance.

Why it matters: Protocols are how Swift shares behaviour between structs, enums and classes. Extensions add free implementations, but only requirements are dispatched dynamically, which surprises almost everyone once.

  1. 1A protocol is a contract

    It lists required methods, properties and initialisers; any struct, enum, class or actor can conform.

  2. 2Extensions add behaviour

    A protocol extension can implement requirements (defaults) and add new methods for every conforming type.

  3. 3Only requirements are dynamic

    A method declared only in the extension is chosen by the static type, so a conforming type's own version may be skipped.

  4. 4Conformance can be added later

    extension Int: MyProtocol {} retroactively makes an existing type conform, even one you don't own.

1

How it works

The explanation, with examples you can run.

Requirements and defaults

protocol Shape {    var area: Double { get }                 // requirement    func describe() -> String                // requirement, with a default below} extension Shape {    func describe() -> String { "a shape of area \(area)" }   // default implementation    func isLarger(than other: some Shape) -> Bool { area > other.area }  // extra method, not a requirement} struct Square: Shape {    var side: Double    var area: Double { side * side }    func describe() -> String { "a square with side \(side)" }   // overrides the default} struct Circle: Shape {    var radius: Double    var area: Double { 3.14159 * radius * radius }               // uses the default describe()} let shapes: [any Shape] = [Square(side: 2), Circle(radius: 1)]for s in shapes { print(s.describe()) }print(Square(side: 2).isLarger(than: Circle(radius: 1)))        // true

Conformance can also be added in an extension, even to types you don't own: extension Int: Shape { ... }. Grouping each conformance in its own extension is common Swift style.

The dispatch trap

Calls to a requirement go through the type's witness table, so the conforming type's own implementation always runs. A method that exists only in the protocol extension is not in that table: it is chosen by the static type at the call site.

protocol Greeter {}extension Greeter { func hi() -> String { "generic hi" } }   // not a requirement struct English: Greeter { func hi() -> String { "hello" } } let direct = English()let boxed: any Greeter = English()print(direct.hi())   // helloprint(boxed.hi())    // generic hi: resolved statically through the protocol

The fix is to declare func hi() -> String inside protocol Greeter. The same thing happens in generic code (func greet<T: Greeter>(_ g: T) calls the extension version) and is the Swift twin of Kotlin's statically resolved extension functions.

Constrained extensions

where clauses add members only for some conforming types:

extension Collection where Element: Numeric {    func total() -> Element { reduce(0, +) }}print([1, 2, 3].total())        // 6print([0.5, 0.25].total())      // 0.75

This is how the standard library gives Array a sorted() only when its elements are Comparable, and == only when they are Equatable.

Synthesised conformances

For Equatable, Hashable, Comparable (enums) and Codable, the compiler writes the implementation when all stored properties (or associated values) conform. You only declare the conformance. Two protocols that matter beyond this: Sendable (safe to share across concurrency domains, covered later) and Identifiable, which SwiftUI uses to track items in lists.

Protocols with associated types and the some and any keywords are the next topic.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.protocol Greeter {} with extension Greeter { func hi() -> String { "generic" } }, and struct En: Greeter { func hi() -> String { "hello" } }. What does let g: any Greeter = En(); g.hi() return?
  2. 2.How does a protocol extension provide a default implementation?
  3. 3.What does a where clause on a protocol extension, like extension Collection where Element: Numeric, do?

0 of 3 right so far. Get all three to finish this topic.