Answer
The core idea
Method dispatch is how Swift decides which implementation runs when code calls a method. Swift uses three mechanisms, and which one applies depends on the type, whether the method can be overridden, and whether the Objective-C runtime is involved.
The short interview answer is: static dispatch when the callee is fixed at compile time, table dispatch (vtable or protocol witness table) when the callee is chosen at runtime through one pointer lookup, and message dispatch when the Objective-C runtime resolves the selector.
Knowing which one applies matters for both performance (inlining and optimization) and correctness features (subclass overrides, method swizzling, KVO).
1. Static dispatch
With static dispatch, the compiler knows the exact function at compile time. The call can be a direct jump, and the optimizer is free to inline it. That is the default for:
- methods on
structandenum - global functions
finalclasses andfinalmethods- methods the compiler can prove are never overridden (for example under whole-module optimization)
struct ValueType {
func doWork() { } // ✅ static dispatch — compiler may inline this
}
func run(_ value: ValueType) {
value.doWork()
}This is one reason value types are attractive on hot paths: there is no runtime lookup tax, and inlining can collapse small methods into surrounding code.
2. Table dispatch for classes (vtable)
Class methods that can be overridden use vtable dispatch. Each class has a virtual method table. At the call site the runtime loads the object's class metadata, reads the function pointer from the table, then calls it. That is one level of indirection — slower than a direct call, and it blocks inlining unless the compiler can de-virtualize the call.
class RegularClass {
func doWork() { } // table dispatch — vtable lookup
final func doFinal() { } // ✅ static dispatch — final removes the override slot
}
func run(_ object: RegularClass) {
object.doWork() // must go through the vtable; a subclass might override it
object.doFinal() // compiler can call RegularClass.doFinal directly
}Marking a method (or the whole class) final is not only an API decision. It tells the compiler the implementation cannot change, so the call can use static dispatch again.
3. Table dispatch for protocols (witness tables)
Protocol requirements use a related idea: a protocol witness table. For an existential such as any Worker, the value carries (or is paired with) a table that maps each protocol requirement to the conforming type's implementation. The call looks up the witness and jumps — again one level of indirection.
protocol Worker {
func doWork()
}
struct FastWorker: Worker {
func doWork() { } // static when the concrete type is known
}
func runExistential(_ worker: any Worker) {
worker.doWork() // ❌ witness-table dispatch — the concrete type is erased
}
func runGeneric<T: Worker>(_ worker: T) {
worker.doWork() // often specialized to a direct call for each T
}A useful interview distinction: generics keep the concrete type and can specialize to static dispatch, while any Protocol existentials pay for witness-table dispatch because the type is erased at the call site.
4. Message dispatch (@objc dynamic)
Message dispatch goes through the Objective-C runtime (objc_msgSend). The runtime resolves the selector, typically via a class's method cache and dispatch tables. It is the most flexible mechanism — and the slowest — because the implementation can change at runtime.
Swift uses message dispatch for Objective-C methods and for Swift members marked @objc dynamic (needed for features like KVO and method swizzling).
class ObjCClass: NSObject {
@objc dynamic func doWork() { } // message dispatch — swizzleable / KVO-visible
}
func run(_ object: ObjCClass) {
object.doWork() // resolved by the Objective-C runtime each call (after caching)
}Use this path when the feature set requires it. Do not mark hot-path Swift methods @objc dynamic only out of habit — that permanently opts them into the slowest dispatch.
5. Putting the three side by side
| Mechanism | Typical cases | Runtime cost | Enables |
|---|---|---|---|
| Static | structs, enums, final, globals | Direct call; can inline | Max optimization |
| Table | overridable class methods; protocol existentials | One pointer lookup (vtable / witness table) | Subclassing and protocol polymorphism |
| Message | @objc dynamic, ObjC APIs | Selector lookup via ObjC runtime | Swizzling, KVO, ObjC interop |
struct ValueType {
func doWork() { } // static
}
class RegularClass {
func doWork() { } // vtable
final func doFinal() { } // static
}
class ObjCClass: NSObject {
@objc dynamic func doWork() { } // message
}
protocol Worker {
func doWork() // witness table for `any Worker`
}6. Why it matters
- On a hot path, static dispatch plus inlining can remove call overhead entirely; vtable and message dispatch cannot be inlined as freely.
- Preferring structs (or
finalclasses) on performance-sensitive code is not superstition — it is choosing a cheaper dispatch model. - Marking something
@objc dynamicfor convenience can keep a method off the optimizer's fast path for the life of the app. - Interviewers use this topic to check whether a candidate connects language keywords (
final,@objc,dynamic,any) to real machine behavior, not just to API style.
7. How to reason about it in practice
There is no Instruments template labeled "dispatch kind," so candidates usually argue from the declaration:
- Ask whether the callee can change at runtime (subclass override, existential, ObjC swizzle).
- If not, expect static dispatch — especially for value types and
final. - If it is an overridable class member, expect a vtable; if it is a protocol existential, expect a witness table.
- If it is
@objc dynamicor an ObjC selector, expect message send. - When optimizing for real, profile first, then remove unnecessary dynamism (
final, concrete generics instead ofany, avoid@objc dynamicon hot methods). The compiler docs on Optimization Tips spell out howfinalrecovers direct calls.
Interview angle
Lead with the three mechanisms and their cost order: static, table, message. Show one short example for each: a struct method (static), an overridable class method versus a final method (vtable versus static), an any Protocol call (witness table), and an @objc dynamic method (message send). Emphasize that final is both an inheritance restriction and a performance hint that unlocks static dispatch and inlining — one reason Swift structs and final types win on hot paths. Close by naming the tradeoff: table and message dispatch buy polymorphism and ObjC runtime features; static dispatch buys speed. Point to the Swift compiler's Optimization Tips and Apple's How Messaging Works as the references behind the answer.