Reputation: 379
It seems that a function defined as a customization point in a protocol with a default implementation in protocol extension cannot be customized in a subclass that inherits the protocol indirectly via a base class if that base class did not customize the function at the first place.
Here is a simple protocol:
protocol MyProtocol
{
func myFunc() -> String
}
With a default implementation:
extension MyProtocol
{
func myFunc() -> String
{
return "hello from extension"
}
}
Let's create a base class and a sub-class like this:
class BaseClass: MyProtocol
{
}
class SubClass: BaseClass
{
func myFunc() -> String
{
return "hello from SubClass"
}
}
BaseClass().myFunc() // "hello from extension"
(BaseClass() as MyProtocol).myFunc() // "hello from extension"
SubClass().myFunc() // "hello from SubClass"
(SubClass() as BaseClass).myFunc() // "hello from extension"
(SubClass() as MyProtocol).myFunc() // "hello from extension"
Now with a customization in the base class:
class BaseClass: MyProtocol
{
func myFunc() -> String
{
return "hello from BaseClass"
}
}
class SubClass: BaseClass
{
override func myFunc() -> String
{
return "hello from SubClass"
}
}
BaseClass().myFunc() // "hello from BaseClass"
(BaseClass() as MyProtocol).myFunc() // "hello from BaseClass"
SubClass().myFunc() // "hello from SubClass"
(SubClass() as BaseClass).myFunc() // "hello from SubClass"
(SubClass() as MyProtocol).myFunc() // "hello from SubClass"
Is it an expected behavior?
Edit 14-nov:
Note about about this article: http://nomothetis.svbtle.com/the-ghost-of-swift-bugs-future from matt comment:
I think it is not strictly related to my question as the article does not cover the case where the subclass inherits the protocol indirectly (which seems to make a difference). In that later case, it is not obvious to me that a static dispatch does happen even if the function is a customization point (part of the protocol requirements). Depending on the inferred type at call time, behavior differs.
The article covers the other case where the function has the default implementation in the extension while not part of the protocol requirements and may shadow subclass customizations.
Edit 17 nov:
Possibly a duplicate of In Swift, why subclass method cannot override the one, provided by protocol extension in superclass
Upvotes: 1
Views: 196
Reputation: 535999
A protocol extension with a method implementation puts us into a situation where sometimes we are polymorphic (the object's internal type is what matters) and sometimes we are not (the way the object is externally typed or cast is what matters).
To explore this, I use a test grid covering the following parameters:
Does the protocol itself also require the method?
Is the adopter a struct or a class?
Does the adopter implement the method?
We start with the answer to the first question being NO. So, here are the type declarations:
protocol Flier {
}
extension Flier {
func fly() {
print("flap flap flap")
}
}
struct Bird : Flier {
}
struct Insect : Flier {
func fly() {
print("whirr")
}
}
class Rocket : Flier {
func fly() {
print("zoooom")
}
}
class AtlasRocket : Rocket {
override func fly() {
print("ZOOOOOM")
}
}
class Daedalus : Flier {
// nothing
}
class Icarus : Daedalus {
func fly() {
print("fall into the sea")
}
}
And here are the tests:
let b = Bird()
b.fly() // flap flap flap
(b as Flier).fly() // flap flap flap
let i = Insect()
i.fly() // whirr
(i as Flier).fly() // flap flap flap
let r = Rocket()
r.fly() // zoooom
(r as Flier).fly() // flap flap flap
let r2 = AtlasRocket()
r2.fly() // ZOOOOOM
(r2 as Rocket).fly() // ZOOOOOM
(r2 as Flier).fly() // flap flap flap
let d = Daedalus()
d.fly() // flap flap flap
(d as Flier).fly() // flap flap flap
let d2 = Icarus()
d2.fly() // fall into the sea
(d2 as Daedalus).fly() // flap flap flap
(d2 as Flier).fly() // flap flap flap
Result: How an object is typed is important. In effect, the compiler knows merely from the way the object is typed where to look for the fly
implementation that will be called; all the necessary information is present at compile time. There is, in general, no need for dynamic dispatch.
The exception is the AtlasRocket, a subclass whose superclass has its own implementation. When the AtlasRocket is typed as its superclass, Rocket, it is still (for purposes of flying) an AtlasRocket. But this is not surprising, because this is a subclass/superclass situation, where polymorphism and dynamic dispatch are in effect; obviously we're not going to turn off dynamic dispatch merely because there is also a protocol extension in the story.
Now the answer to the first question is YES. The type declarations are exactly the same as before, except that I've added a "2" to the names of all the types, and the protocol itself contains the method as a requirement:
protocol Flier2 {
func fly() // *
}
extension Flier2 {
func fly() {
print("flap flap flap")
}
}
struct Bird2 : Flier2 {
}
struct Insect2 : Flier2 {
func fly() {
print("whirr")
}
}
class Rocket2 : Flier2 {
func fly() {
print("zoooom")
}
}
class AtlasRocket2 : Rocket2 {
override func fly() {
print("ZOOOOOM")
}
}
class Daedalus2 : Flier2 {
// nothing
}
class Icarus2 : Daedalus2 {
func fly() {
print("fall into the sea")
}
}
And here are the tests; they are the same tests, with a "2" added to the names of all the types:
let b = Bird2()
b.fly() // flap flap flap
let i = Insect2()
i.fly() // whirr
(i as Flier2).fly() // whirr (!!!)
let r = Rocket2()
r.fly() // zoooom
(r as Flier2).fly() // zoooom (!!!)
let r2 = AtlasRocket2()
r2.fly() // ZOOOOOM
(r2 as Rocket2).fly() // ZOOOOOM
(r2 as Flier2).fly() // ZOOOOOM (!!!)
let d = Daedalus2()
d.fly() // flap flap flap
(d as Flier2).fly() // flap flap flap
let d2 = Icarus2()
d2.fly() // fall into the sea
(d2 as Daedalus2).fly() // flap flap flap
(d2 as Flier2).fly() // flap flap flap
Result: Polymorphism has sprung to life: what an object really is, matters. You can call an Insect2 a Flier2, but it still flies like an Insect2. You can call a Rocket2 a Flier2, but it still flies like a Rocket2. You can call an AtlasRocket2 a Flier2, but it still flies like an AtlasRocket2.
The exceptional case here is the one pointed out by your question — namely, when the adopter itself has no implementation of the method. Thus, we call an Icarus2 a Daedalus2, and lo and behold, it flies like a Daedalus2, exactly as in the previous example. There was no need to switch on polymorphism, and the compiler knew this from the outset, because Icarus2's implementation is not an override
.
Upvotes: 2