Swift protocol extension method is called instead of method implemented in subclass
Asked Answered
P

4

32

I've encountered a problem that is explained in the code below (Swift 3.1):

protocol MyProtocol {
    func methodA()
    func methodB()
}

extension MyProtocol {
    func methodA() {
        print("Default methodA")
    }

    func methodB() {
        methodA()
    }
}

// Test 1
class BaseClass: MyProtocol {

}

class SubClass: BaseClass {
    func methodA() {
        print("SubClass methodA")
    }
}


let object1 = SubClass()
object1.methodB()
//

// Test 2
class JustClass: MyProtocol {
    func methodA() {
        print("JustClass methodA")
    }
}

let object2 = JustClass()
object2.methodB()
//
// Output
// Default methodA
// JustClass methodA

So I would expect that "SubClass methodA" text should be printed after object1.methodB() call. But for some reason default implementation of methodA() from protocol extension is called. However object2.methodB()call works as expected.

Is it another Swift bug in protocol method dispatching or am I missing something and the code works correctly?

Playhouse answered 22/6, 2017 at 15:8 Comment(0)
F
52

This is just how protocols currently dispatch methods.

A protocol witness table (see this WWDC talk for more info) is used in order to dynamically dispatch to implementations of protocol requirements upon being called on a protocol-typed instance. All it is, is really just a listing of the function implementations to call for each requirement of the protocol for a given conforming type.

Each type that states its conformance to a protocol gets its own protocol witness table. You'll note that I said "states its conformance", and not just "conforms to". BaseClass gets its own protocol witness table for conformance to MyProtocol. However SubClass does not get its own table for conformance to MyProtocol – instead, it simply relies on BaseClass's. If you moved the
: MyProtocol down to the definition of SubClass, it would get to have its own PWT.

So all we have to think about here is what the PWT for BaseClass looks like. Well, it doesn't provide an implementation for either of the protocol requirements methodA() or methodB() – so it relies on the implementations in the protocol extension. What this means is that the PWT for BaseClass conforming to MyProtocol just contains mappings to the extension methods.

So, when the extension methodB() method is called, and makes the call out to methodA(), it dynamically dispatches that call through the PWT (as it's being called on a protocol-typed instance; namely self). So when this happens with a SubClass instance, we're going through BaseClass's PWT. So we end up calling the extension implementation of methodA(), regardless of the fact that SubClass provides an implementation of it.

Now let's consider the PWT of JustClass. It provides an implementation of methodA(), therefore its PWT for conformance to MyProtocol has that implementation as the mapping for methodA(), as well as the extension implementation for methodB(). So when methodA() is dynamically dispatched via its PWT, we end up in its implementation.

As I say in this Q&A, this behaviour of subclasses not getting their own PWTs for protocols that their superclass(es) conform to is indeed somewhat surprising, and has been filed as a bug. The reasoning behind it, as Swift team member Jordan Rose says in the comments of the bug report, is

[...] The subclass does not get to provide new members to satisfy the conformance. This is important because a protocol can be added to a base class in one module and a subclass created in another module.

Therefore if this was the behaviour, already-compiled subclasses would lack any PWTs from superclass conformances that were added after the fact in another module, which would be problematic.


As others have already said, one solution in this case is to have BaseClass provide its own implementation of methodA(). This method will now be in BaseClass's PWT, rather than the extension method.

Although of course, because we're dealing with classes here, it won't just be BaseClass's implementation of the method that's listed – instead it will be a thunk that then dynamically dispatches through the class' vtable (the mechanism by which classes achieve polymorphism). Therefore for a SubClass instance, we'll wind up calling its override of methodA().

Furie answered 22/6, 2017 at 17:36 Comment(11)
long story short, is it correct to say, the PWT gets updated when you either directly conform or extend a protocol requirement? Which in this case is: extension MyProtocol { func methodA() { print("Default methodA"); } func methodB() { methodA(); } } class BaseClass: MyProtocol { }. Having that said, the PWT won't re-map once you subclass, rather per each rewrite(class SubClass : BaseClass{ func methodA() { print("subClass methodA") } }Zanthoxylum
I said rewrite because it seems it's neither the actual conformance nor an override of a method requirement, it will update that only. I'm curious to know what's the correct jargon for itZanthoxylum
@Honey The PWT gets evaluated at the site at which protocol conformance is stated, and it's done at compile-time, so I'm not really sure either "update" or "rewrite" is quite correct. Subclassing doesn't have any effect on the base class' PWT, but changes in dispatch may be noticed if say BaseClass provides its own implementation of methodA, and then SubClass comes along and overrides that implementation (because now with a protocol-typed SubClass instance, we're dispatching first to the PWT, and then to the class vtable). The PWT remains the same tho, only SubClass's vtable changes.Furie
1) so you're saying we have a PWT and class vTable, which are somewhat similar and every method gets dispatched based on the combined result of the two? 2) specifically about methodA from the first test of the OP, the PWT is changed, but eventually the class vtable is changed and so it points to where the subclass's vtable points to. However for methodB, its subclass hasn't made any changes and it just points to where the PWT says it should point to? 3. In general: even though subclassing can affect dispatch, it doesn't PWT, it can only affect class vtable; only conformance affects PWT. Right?Zanthoxylum
@Honey 1) Yes, if you're dealing with a protocol-typed instance of a class; the dispatch of the protocol requirements will be determined by the PWT + vtable. 2) I'm not quite sure what you mean by "PWT is changed"; changed from what to what? The subclass doesn't have any influence on the PWT, no; that's purely determined by BaseClass's conformance to the protocol (whether it provides its own implementations of the requirements). And yes, I think that then answers #3 :)Furie
"no; that's purely determined by BaseClass's conformance to the protocol (whether it provides its own implementations of the requirem". You mean the default implementation? It all makes sense now.Zanthoxylum
@Honey Yes, if BaseClass doesn't provide its own implementation of the protocol requirements, the extension implementations will get listed in its PWT; and thus will be the ones dynamically dispatched to (when called on a protocol-typed instance). Glad it makes sense :)Furie
It looks like a possible workaround at least in some cases, could be replacing "class hierarchy" with "protocol hierarchy" (with implementations provided in protocol extensions). See gist.github.com/grigorye/fa4fce6f0ca63cfb97b3c48448a98239 for the original sample switched to protocols.Rah
In such a case (see the my comment above), in case of subclass we defer "instantiation" of PWT till the class definition, as it states the conformance itself, while "inheriting" "base" implementations from the base protocol not from the base class.Rah
Another workaround would be replacing default implementations in protocol with a dummy "Defaults" class that would provide them. It quite limited solution, but might be worth considering. Imho, it makes whole thing more clear/understandable because it enforces "override" for both base-class and sub-class overrides of the default implementation. See gist.github.com/grigorye/27e0f6e4f50a7650768ccd1761f6587aRah
@GrigoryEntin Both useful workarounds – thanks for mentioning them! Such workarounds do depend on the exact use case though, for example you might not be able to control the base class' conformance to a given protocol or the base class may inherit from another type.Furie
Z
2

A very short answer that a friend shared with me was:

Only the class that declares the conformance gets a protocol witness table

Meaning a subclass having that function has no effect on how the protocol witness table is setup.

The protocol witness is a contract only between the protocol, it's extensions, and the concrete class that implements it.

Zanthoxylum answered 9/11, 2021 at 21:34 Comment(0)
L
0

Well I suppose the subclass method A is not polymorphic because you can't put the override keyword on it, since the class doesn't know the method is implemented in an extension of the protocol and thus doesn't let you override it. The extension method is probably stepping on your implementation in runtime, much like 2 exact category methods trump each other with undefined behavior in objective C. You can fix this behavior by adding another layer in your model and implementing the methods in a class rather than the protocol extension, thus getting polymorphic behavior out of them. The downside is that you cannot leave methods unimplemented in this layer, as there is no native support for abstract classes (which is really what you're trying to do with protocol extensions)

protocol MyProtocol {
    func methodA()
    func methodB()
}

class MyProtocolClass: MyProtocol {
    func methodA() {
        print("Default methodA")
    }

    func methodB() {
        methodA()
    }
}

// Test 1
class BaseClass: MyProtocolClass {

}

class SubClass: BaseClass {
    override func methodA() {
        print("SubClass methodA")
    }
}


let object1 = SubClass()
object1.methodB()
//

// Test 2
class JustClass: MyProtocolClass {
    override func methodA() {
        print("JustClass methodA")
    }
}

let object2 = JustClass()
object2.methodB()
//
// Output
// SubClass methodA
// JustClass methodA

Also relevante answer here: Swift Protocol Extensions overriding

Legislate answered 22/6, 2017 at 15:17 Comment(0)
A
0

In your code,

let object1 = SubClass()
object1.methodB()

You invoked methodB from an instance of SubClass, but SubClass does not have any method named methodB. However its super class, BaseClass conform to MyProtocol, which has a methodB methodB.

So, it will invoke the methodB from MyProtocal. Therefore it will execute the methodA in extesion MyProtocol.

To reach what you expect, you need implement methodA in BaseClass and override it in SubClass, like the following code

class BaseClass: MyProtocol {
    func methodA() {
        print("BaseClass methodA")
    }
}

class SubClass: BaseClass {
    override func methodA() {
        print("SubClass methodA")
    }
}

Now, output would become

//Output
//SubClass methodA
//JustClass methodA

Although the method can reach what you expect, but I'm not sure this kind of code struct is recommended.

Adorno answered 22/6, 2017 at 15:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.