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?

AnderCover
  • 2,488
  • 3
  • 23
  • 42
imre
  • 1,667
  • 1
  • 14
  • 28

1 Answers1

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 !

AnderCover
  • 2,488
  • 3
  • 23
  • 42
  • 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? – imre May 03 '21 at 12:18
  • yes, for more details see @Hamish answer here: https://stackoverflow.com/a/44706021/1425697 – AnderCover May 03 '21 at 12:24
  • 1
    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. – imre May 03 '21 at 12:27