Swift: When Unused Code Is a Bug

Spot test - what’s the output of this code?

protocol Greeter {
    func greet()
}

extension Greeter {
    func greet() {
        print("Hello, World!")
    }
}

class BaseGreeter: Greeter {}

class LazyGreeter: BaseGreeter {
    func greet() {
        print("sup")
    }
}

let greeter: Greeter = LazyGreeter()
greeter.greet()

Even seasoned Swift developers may get this wrong, so don’t feel bad if your answer was “sup”. The actual output is “Hello, World!”, yes really… go ahead and run it in a Playground.

So what’s going on? It’s all down to how Swift handles dispatch of protocol types. A Protocol Witness Table1 (PWT) is the mechanism Swift uses to keep track of types that implement a protocol. When you call a function, or access a property on a protocol type, the PWT is used to identify the concrete implementation to call. The gotcha is that PWTs are only associated with types that declare their conformance to a protocol. Note that I said declare, and not simply conform. In this example, only BaseGreeter explicitly declares conformance to Greeter, thus the PWT is only associated with BaseGreeter. PWTs do not descend to subclasses, as subclasses do not explicitly declare their conformance. Therefore, since the greeter local let is of type Greeter - not LazyGreeter - the call to greet() defaults to the implementation in the Greeter extension, as there is no PWT associated with LazyGreeter.

To clarify, LazyGreeter().greet() does output “sup” as you’d expect. The issue is specifically the use of the Greeter protocol type for the local let that introduces indirection and the need for the PWT. In order for this example to output the expected “sup”, we’d need to implement greet() in BaseGreeter so that LazyGreeter can override it. That’s not ideal, and brings into question your reason for using a protocol extension in the first place.

Would you notice this bug during code review? I can’t confidently say that I would. Even more worrying, is that if you were to write a unit test for LazyGreeter, you’d likely just use let greeter = LazyGreeter(), thus bypassing Greeter and the PWT altogether. Both these points make this a particularly nasty gotcha, so keep your eyes out - you could well save yourself or a teammate from a lot of head scratching!

Fortunately Periphery has your back, and it’ll correctly report that LazyGreeter.greet() is unused. Granted, you may be a little confused at first and wonder if Periphery has made a mistake. This is a great example of why my unofficial slogan for Periphery is: “So accurate, you’ll think it’s broken”.

1: WWDC 2016: Understanding Swift Performance - PWT explanation begins at 24:30.