Protocol extensions and subclasses
Protocol extensions are one of the most powerful features in Swift, but they also come with a bit of danger. In this post I will explore how this feature interacts with class inheritance and how dangerous it can be.
Method dispatch is the system of the language that is used to decide what method implementation should be used in a polymorphic scenario. Swift has multiple dispatch systems that are used depending on the kind of type and the knowledge the compiler has about it.
Static dispatch is used when the compiler knows at compile time the concrete type being used and exactly which method implementation to use. This is fast at runtime and easy to understand.
Message dispatch is used when Swift needs to interact with the Objective-C runtime. Objective-C uses message passing to invoke all the methods so Swift needs to follow the same pattern when it needs to bridge to code that comes from that language.
Dynamic dispatch is used when he compiler can’t know at compile time the specific method implementation that needs to be used. This is commonly seen on class hierarchies and protocols were the concrete type is not known until runtime.
This is the scenario we are interested in today.
Protocols with default implementations
To examine the issue we first need to prepare some examples. We need a protocol with a method requirement that is implemented in an extension.
protocol Model {
func execute()
}
extension Model {
func execute() {
print("Default implementation")
}
}
Note that the method we implement on the extension is part of the protocol definition. We could add methods that are not part of the protocol but that behaves in a different way.
This is easily one of the most powerful features of the language. Thanks to the combination of protocols and extensions we can provide functionality for free, while still allowing those types to implement custom more specific code. This is a design that is very common on the standard library.
To illustrate the part where the compiler can’t know until runtime the specific types we’re using let’s make a class that calls the method on a property defined with the protocol type:
class Controller {
let model: Model
init(model: Model) {
self.model = model
}
func execute() {
self.model.execute()
}
}
As you can see when execute
is run, Controller
only has access to a property of type Model
. The compiler can’t know what specific type we’re using, it just knows is a type that conforms to that protocol.
Now let’s prepare two conforming types that illustrate Swift dispatching behaviour:
class ClassUsingDefault: Model {}
Controller(model: ClassUsingDefault()).execute() // prints Default implementation
This first type doesn’t implement the method itself, instead it relies on the default implementation provided by the extensions.
class ClassWithImplementation: Model {
func execute() {
print("ClassWithImplementation")
}
}
Controller(model: ClassWithImplementation()).execute() // ClassWithImplementation
This time we have a type that is implementing its own version of the protocol requirement. We want this implementation to take precedence over the default implementation and indeed that’s how Swift behaves.
This is the behaviour we want and expect from dynamic dispatch. Thanks to Swift’s runtime we’re able to use default or concrete implementations whenever we want.
The danger of inheritance
Now let’s take a look what happens when you include class inheritance into the mix.
class SubclassOfSuperclassThatHasImplementation: ClassWithImplementation {
override func execute() {
print("SubclassOfSuperclassThatHasImplementation")
}
}
Controller(model: SubclassOfSuperclassThatHasImplementation()).execute() // SubclassOfSuperclassThatHasImplementation
This time our type is subclassing a superclass that has a concrete method implementation. As expected the compiler dynamically finds the correct method to execute.
Pay attention to the method definition. The compiler is forcing us to write override
because we’re not only implementing a method required by a protocol, we’re also overriding a method implemented on the superclass.
This all makes sense and works as expected.
Now let’s implement a class that inherits from a superclass that relies on the default method implementation from the extension.
class SubclassOfSuperclassThatUsesDefault: ClassUsingDefault {
override func execute() { // ERROR: Method does not override any method from its superclass
print("SubclassOfSuperclassThatUsesDefault")
}
}
When writing this you can already see something weird. The compiler tells you that using override
is not allowed. This is correct because the superclass doesn’t implement that method.
But even if it makes sense it’s a little weird. You know the superclass conforms to a protocol, so the method must be on the superclass set of callable methods. That is correct but still Swift doesn’t consider that you are overriding the method because is not specifically defined on the superclass.
And this is were the problem resides.
Let’s trust the compiler error and remove the override
. We still want a custom implementation of the method but let’s agree with the compiler that we’re not overriding anything from the superclass.
class SubclassOfSuperclassThatUsesDefault: ClassUsingDefault {
func execute() {
print("SubclassOfSuperclassThatUsesDefault")
}
}
Now the compiler is happy. So let’s try to use this and see what dispatching behaviour we get.
Controller(model: SubclassOfSuperclassThatUsesDefault()).execute() // Default implementation !!!!
🚨 💣
We get the default implementation. Even if we’ve written a concrete method on our type, in the same way we’ve done on all previous cases, we still get the default implementation.
This is because Swift only checks the superclass when deciding if a type has concrete implementations of a protocol requirement. Since our superclass doesn’t have that, it relies on the default implementation, the compiler doesn’t even check the subclasses.
The worse part is that from a user’s perspective there is no way to fix this. You can’t stick a fancy keyword on your method and expect it to work. It’s also really subtle and if you’re not paying attention it’s very easy to introduce bugs with this. The good part is that you can’t break it by removing a superclass implementation since the compiler will warn you about the override
keyword.
This is considered a limitation of Swift (as of 4.2) and is tracked as a bug. There are also conversations on the forums that discuss a nice solution for this scenario.
Conclusion
Protocols are very nice in Swift, and together with default implementations on extensions they become an amazing feature of the language. Almost like a superpower. And you know what they say about that.