swiftprotocolsswift-protocolsdynamic-dispatch

Swift: dynamic dispatch with protocol and subclass


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?


Solution

  • 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 !