How to avoid unexpected behavior when subclassing types which conform to protocols. Swift encourages protocol oriented development and default implementations provide a powerful tool for composing types. Using these tools in combination with class inheritance has some surprising consequences which can result in unexpected behavior. Let’s explore an example and see what we can do […]
How to avoid unexpected behavior when subclassing types which conform to protocols.
Swift encourages protocol oriented development and default implementations provide a powerful tool for composing types. Using these tools in combination with class inheritance has some surprising consequences which can result in unexpected behavior. Let’s explore an example and see what we can do to avoid such problems. You can also find these examples as an Xcode Playground.
Alexandros Salazar’s “The Ghost of Swift Bugs Future” and Kevin Ballard’s “Method Dispatch in Protocol Extensions” posts cover the dispatch rules in use here in more detail and are great further reading.
I wanted serveral types to implement the same behavior so it’s time for a protocol. Let’s say I wanted all these types to be configurable based on some sort of stored configuration data which is loaded from a file. Most of the time I want to use some common default configuration. So I extend the protocol with a default implementation to specify that default configuration file:
When I create Configurable structs this works great. My types get the default implementation but can also provide their own version:
I can also create Configurable classes. At first this works fine:
When I subclass a Configurable class suddenly I have a problem:
What happened? Here my parent baseView class is using self to invoke the default implementation of a Configurable method. My child CustomView class is providing it’s own implementation of that Configurable method. However due to Swift’s method dispatch rules this child class’ implementation is never called and the default implementation is always used.
One workaround is to drop the default implementation. If we have a small number of classes adopting Configurable this might be fine. However when we have lots of Configurable types this becomes less satisfying.
The swift evolution mailing list suggests two additional workarounds. If classes always implement protocol methods and call any default implementations then they will always use dynamic dispatching and be able to invoke overrides from subclasses. Calling default implementations can be a little tricky as well.
One option is to avoid default implementations of methods declared in the protocol. Methods added to the protocol in an extension will always be statically dispatched so we can then call the default implementation from our class’ dynamically dispatched version of the method. Unfortunately this means that when we implement a class we need to remember to check the protocol extension and implement every method found there, even though they do not appear in the protocol declaration.
A second option is to define a wrapper type which can use a statically dispatched call to the default implementation. This allows us to include function declarations in our protocol definition but a large protocol with many required methods requires a large wrapper class.
We’ve seen that subclasses cannot reliably override protocol methods they inherit from their parent class when those methods have default implementations in a protocol extension. This causes confusing behavior where a subclass can implement protocol methods only to discover that they are never called from behavior inherited from a superclass. This can be a source of confusing bugs and identifying the root cause requires inspecting the behavior of all our parent classes. Something that can be especially difficult if we were to subclass a framework provided class.
To avoid creating types which are likely to introduce bugs in the future we should do one of the following:
In my current work at Good Eggs I chose option number 2. I needed class types but it was reasonable for us to implement our shared behavior on a base class and use class inheritance rather than protocol conformance to compose types. In other areas of the app where I expect to stick to value types I’ll probably use option number 1. If I were publishing a framework containing reference types then I would use options 3 and 4 to protect consumers of my types.
Subclass with caution and happy coding. If you liked this, please click the 💚 below so others will find it on Medium.
Good Eggs connects people who love food, directly with people who make it. We deliver the most incredible food, straight to Bay Area homes. If you are inspired by our mission is to grow and sustain local food systems worldwide, find out how you can help.
Overriding Swift Protocol Extension Default Implementations was originally published in Good Eggs Product Team on Medium, where people are continuing the conversation by highlighting and responding to this story.
Source: Good Eggs