Swift Protocols: Magic of Dynamic & Static methods dispatches ✨

Tiny intro

This short story is about an enigmatic side of Protocols extensions after Swift 2 was released. You could use Protocol-Oriented Programming paradigm really actively but never have a chance to see this issue. After I had met with it I found a lot of questions on SO and articles on Medium where people wondered is it a bug or a feature 😄

In any case, it is still with us in Swift 4, so that’s why I would like to write about it too.

Firstly, let’s look at some basic stuff!

Dynamic dispatch vs. Static dispatch in Protocols

Dynamic dispatch (aka runtime dispatch aka table dispatch)

Widely known for everyone who understands Polymorphism. The system looks for specific method implementation in compiler created dispatch table (called witness table in Swift) in runtime, not in compilation time. In protocol’s world it is just protocol requirements:

protocol Movable {
func walk()
}

Static dispatch (aka compile time dispatch aka direct dispatch)

Very effectively optimized direct call. This kind of method dispatch used in protocol extensions:

extension Movable {
func crawl() {
print(“Default crawling”)
}
}

So where is an issue then?

Let’s create a primitive struct, which conforms to this protocol and see what it will print:

struct Animal: Movable {
func walk() {
print("Animal is walking proudly")
}
}
let panda = Animal()
panda.walk()
panda.crawl()
// Output
// Animal is walking proudly
// Default crawling

Nothing unusual, everything is clear. If you want to override a method in the extension it is straightforward too:

struct Animal: Movable {
func walk() {
print("Animal is walking proudly")
}
    func crawl() {
print("Animal is crawling silently")
}
}
let raccoon = Animal()
raccoon.walk()
raccoon.crawl()
// Output
// Animal is walking proudly
// Animal is crawling silently

As expected. But here is a situation where the magic appears:

let wolf: Movable = Animal()
wolf.walk()
wolf.crawl()
// Output
// Animal is walking proudly
// Default crawling

Ooops 🤔 What was happened? Nothing personal, just Static dispatch, mate.

We manually cast our wolf to Movable protocol and Swift compiler moved this variable to an existential container. When you create a variable without type, but which only conforms to the protocol instead, it is always created in the existential container which uses static dispatch. By the way, when you add some methods to your struct/enum/class in extension, they will also be statically dispatched.

How can we avoid this trouble? Very easy, just need to add a method from protocol extension to required protocol methods:

protocol Movable {
func walk()
func crawl()
}

This method now uses dynamic dispatch and our code will work as expected (at least I expected this behavior 😉).

Here is a gist if you want to check this by your own:


If you have any question or noticed some mistakes — don’t hesitate to write me!

Additional information