How does protocol dispatch differ from dynamic dispatch?
Protocol dispatch (via witness tables) is a form of dynamic dispatch, alongside vtable dispatch and message dispatch.
The three (really four) dispatch mechanisms in Swift
1. Static dispatch (direct call) — compiler knows exactly which function to call; applies to structs, final classes/methods, free functions, specialized generics.
2. Table dispatch — vtables (class inheritance) — non-final methods resolve at runtime via a per-class-hierarchy vtable.
class Shape { func area() -> Double { 0 } }
class Circle: Shape { override func area() -> Double { ... } }
let s: Shape = Circle()
s.area() // vtable dispatch
3. Table dispatch — witness tables (protocols) — each protocol conformance gets its own witness table.
protocol Shape { func area() -> Double }
struct Circle: Shape { func area() -> Double { ... } }
func render<T: Shape>(_ shape: T) { shape.area() } // witness table dispatch (unless specialized)
4. Message dispatch (objc_msgSend — @objc dynamic) — the most expensive, but the only one supporting runtime method swizzling, KVO.
The key structural difference
Vtables model inheritance (a linear hierarchy); witness tables model conformance (a type can satisfy arbitrarily many unrelated protocols, each with its own independent table).
| Vtable (class) dispatch | Witness table (protocol) dispatch | |
|---|---|---|
| Table scope | Per class, inherited/overridden | Per protocol conformance |
| Resolved via | Object’s vtable pointer | Witness table passed as hidden param or bundled in existential |
| Supports multiple unrelated conformances? | No | Yes |
Extension methods — a common gotcha
Methods added via extension that aren’t part of the protocol’s requirement list are statically dispatched, even called on a protocol-typed value:
protocol Greetable { func name() -> String }
extension Greetable {
func greet() -> String { "Hello, \(name())" } // in witness table
func loudGreet() -> String { greet().uppercased() } // NOT a requirement — static dispatch
}
struct Person: Greetable {
func name() -> String { "Alice" }
func loudGreet() -> String { "OVERRIDE" } // does NOT override the extension version
}
let p: any Greetable = Person()
p.loudGreet() // calls the EXTENSION's loudGreet(), not Person's
One-liner if pressed: “Protocol dispatch via witness tables is itself a form of dynamic dispatch, structurally similar in cost to class vtable dispatch, but witness tables model per-conformance dispatch rather than an inheritance hierarchy. The real fork that matters for performance is static vs. dynamic: a protocol method call is statically dispatched and fully inlinable when the compiler can specialize, and falls back to witness-table indirection only when the type is genuinely unknown at compile time.”