4

Please consider the following Swift 5 code:

protocol P: class {
    func call_foo()
    func foo()
    func call_bar()
    func bar()
}

extension P {
    func call_foo() { foo() }
    func foo() { print("P.foo") }
    func call_bar() { bar() }
    func bar() { print("P.bar") }
}

class C1: P {
    func foo() { print("C1.foo") }
}

class C2: C1 {
    func bar() { print("C2.bar") }
}

let c = C2()
c.call_foo()    //  C1.foo
c.foo()         //  C1.foo
c.call_bar()    //  P.bar
c.bar()         //  C2.bar

If the foo() call in P.call_foo() gets dynamically dispatched to C1.foo(), then why the bar() call in P.call_bar() does not get dynamically dispatched to C2.bar()?

The only difference is that foo() is overridden directly in the class that conforms to P, and bar() is only overridden in a subclass. Why does that make a difference?

Given that bar() is a protocol requirement, shouldn't all calls to it always get dynamically dispatched?

1 Answer 1

2

In the context of your extension:

extension P {
    func call_foo() { foo() }
    func foo() { print("P.foo") }
    func call_bar() { bar() }
    func bar() { print("P.bar") }
}

C2 does not exist, P is a protocol, and methods are dispatched statically, and although bar() is a requirements of P, it is not implemented by C1 which has the conformance to P so:

let c1: some P = C1()
c1.call_foo()    //  C1.foo
c1.foo()         //  C1.foo
c1.call_bar()    //  P.bar
c1.bar()         //  P.bar

and that is normal, and interestingly you have:

let someP: some P = C2()
someP.call_foo()    //  C1.foo
someP.foo()         //  C1.foo
someP.call_bar()    //  P.bar
someP.bar()         //  P.bar

Meaning that if you only have a reference to some P, the subclass C2 of C1 behaves exactly as it's superclass: call_bar() calls P.bar() because C1 does not implement bar()

now let's look at what happens if you implement bar() in C1:

class C1: P {
    func foo() { print("C1.foo") }
    func bar() { print("C1.bar") }
}

class C2: C1 {
    override func bar() { print("C2.bar") }
}

If we use a reference to C1 using some P:

let c1: some P = C1()
c1.call_foo()    //  C1.foo
c1.foo()         //  C1.foo
c1.call_bar()    //  C1.bar
c1.bar()         //  C1.bar

now in call_bar() the compiler knows it has to use C1.bar() so with a reference to C2 using some P:

let someP: some P = C2()
someP.call_foo()    //  C1.foo
someP.foo()         //  C1.foo
someP.call_bar()    //  C2.bar
someP.bar()         //  C2.bar

The subclass C2 still behaves the same way as it's superclass C1 and it's implementation of bar() get's called. (And I find it somewhat reassuring when sublasses behave as their parent).

now let's check the original snippet :

let c = C2()
c.call_foo()    //  C1.foo
c.foo()         //  C1.foo
c.call_bar()    //  C2.bar
c.bar()         //  C2.bar

it work's !

Sign up to request clarification or add additional context in comments.

3 Comments

Do I understand it correctly that there's only a single witness table for C1's conformance to P (as opposed to also having a separate one for each subclass of C1), shared between all instances of C1 or any of its subclasses?
yes, for more details see @Hamish answer here: stackoverflow.com/a/44706021/1425697
Ok, thanks. This explains the behavior I'm seeing, though I still don't understand the rationale behind it. Thanks for the link, I'll keep reading.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.