Search code examples
swiftprotocolshierarchy

Trying to Figure Out Strange Protocol Results in Swift


Here's the deal. I have run across this behavior in my implementation programming, and it has really puzzled me. I'm trying to figure out why it happens, so I can account for it in the future.

I'll paste a fairly involved (but minimal) playground in a minute, but I wanted to outline the issue, first.

The issue is that, if I define a class (not a struct) as conforming to a protocol with a default implementation, then override that by implementing the defined method, the protocol default implementation keeps getting executed, even when I'd expect the secondary implementation to be executed, and inheritance hierarchies get ignored.

Below, I have the playground (latest Xcode), and the resulting printout.

WHAT I EXPECT

Everything should follow the example set by the direct method call (1). This includes a couple of fallbacks to the protocol defaults.

WHAT ACTUALLY HAPPENS

The default implementation in the protocols (from which the classes derive/to which the classes conform) keeps getting executed.

The most puzzling ones are (6) - (8).

(2) - (4) are also a bit weird, as D and E (and Q and R) get printed correctly, but none of the other ones do.

Does anyone have any idea why I might be getting this behavior?

THE HIERARCHY MAP

Pseudo-UML Diagram of the Hierarchy

THE PLAYGROUND:

// MARK: - Test Functions

// Argument is Class A
func printValueFromClassA(_ inValueAsClassA: A) {
    inValueAsClassA.printValue()
}

// Argument is Class H
func printValueFromClassH(_ inValueAsClassH: H) {
    inValueAsClassH.printValue()
}

// Argument is Class J
func printValueFromClassJ(_ inValueAsClassJ: J) {
    inValueAsClassJ.printValue()
}

// Argument is Class D
func printValueFromClassD(_ inValueAsClassD: D) {
    inValueAsClassD.printValue()
}

// Argument is the Root protocol
func printValueFromProtocolA(_ inValueAsProtocolA: PA) {
    inValueAsProtocolA.printValue()
}

// Argument is the first derived protocol
func printValueFromProtocolB(_ inValueAsProtocolB: PB) {
    inValueAsProtocolB.printValue()
}

// Argument is the second derived protocol
func printValueFromProtocolC(_ inValueAsProtocolC: PC) {
    inValueAsProtocolC.printValue()
}

// MARK: - Protocols

// Root protocol: Defines the method
protocol PA {
    func printValue()
}

// Root protocol default implementation
extension PA {
    func printValue() { print("\t\tProtocol A") }
}

// Derived protocol: Depends on Root protocol default
protocol PB: PA { }

// Derived protocol: defines a new default implementation
protocol PC: PB { }

// Deived protocol default implementation
extension PC {
    func printValue() { print("\t\tProtocol C") }
}

// MARK: - Classes

// Direct conformance to PA; relying on the default implementation of the conforming method
class A: PA { }

// Derives from A, but implements the conforming method
class B: A {
    func printValue() { print("\t\tClass B") }
}

// Derives from B, and overrides the conforming method
class C: B {
    override func printValue() { print("\t\tClass C") }
}

// Direct conformance to PB (which is based on PA), and implements the conforming method
class D: PB {
    func printValue() { print("\t\tClass D") }
}

// Derives from D, and overrides the conforming method
class E: D {
    override func printValue() { print("\t\tClass E") }
}

// Direct conformance to PA, but defines the conforming method
class F: A {
    func printValue() { print("\t\tClass F") }
}

// Derives from C, and overrides the conforming method (second override)
class G: C {
    override func printValue() { print("\t\tClass G") }
}

// Direct conformance to PB, but relies on the default implementation of the conforming method
class H: PB { }

// Derives from H, but implements the conforming method.
class I: H {
    func printValue() { print("\t\tClass I") }
}

// Direct conformance to the second derived protocol, and relies on that protocol's default implementation
class J: PC { }

// Derived from J, but implements the conforming method
class K: J {
    func printValue() { print("\t\tClass K") }
}

// Derived from J, and overrides the method
class L: K {
    override func printValue() { print("\t\tClass L") }
}

// Derived from F, and overrides the method
class M: F {
    override func printValue() { print("\t\tClass M") }
}

// Derived from I, and overrides the method
class N: I {
    override func printValue() { print("\t\tClass N") }
}

// Derived from N, and overrides the method
class O: N {
    override func printValue() { print("\t\tClass O") }
}

// Derived from L, and overrides the method
class P: L {
    override func printValue() { print("\t\tClass P") }
}

// Direct conformance to the second derived protocol, but implements the conforming method
class Q: PC {
    func printValue() { print("\t\tClass Q") }
}

// Derived from Q, and overrides the method
class R: Q {
    override func printValue() { print("\t\tClass R") }
}

// MARK: - Testing

// MARK: instance definitions
let instanceOfA = A()
let instanceOfB = B()
let instanceOfC = C()
let instanceOfD = D()
let instanceOfE = E()
let instanceOfF = F()
let instanceOfG = G()
let instanceOfH = H()
let instanceOfI = I()
let instanceOfJ = J()
let instanceOfK = K()
let instanceOfL = L()
let instanceOfM = M()
let instanceOfN = N()
let instanceOfO = O()
let instanceOfP = P()
let instanceOfQ = Q()
let instanceOfR = R()


// MARK: Directly calling the instance methods
print("1) Direct Method Call:")
print("\tClass A:")
instanceOfA.printValue()                // "Protocol A"
print("\tClass B:")
instanceOfB.printValue()                // "Class B"
print("\tClass C:")
instanceOfC.printValue()                // "Class C"
print("\tClass D:")
instanceOfD.printValue()                // "Class D"
print("\tClass E:")
instanceOfE.printValue()                // "Class E"
print("\tClass F:")
instanceOfF.printValue()                // "Class F"
print("\tClass G:")
instanceOfG.printValue()                // "Class G"
print("\tClass H:")
instanceOfH.printValue()                // "Protocol A"
print("\tClass I:")
instanceOfI.printValue()                // "Class I"
print("\tClass J:")
instanceOfJ.printValue()                // "Protocol C"
print("\tClass K:")
instanceOfK.printValue()                // "Class K"
print("\tClass L:")
instanceOfL.printValue()                // "Class L"
print("\tClass M:")
instanceOfM.printValue()                // "Class M"
print("\tClass N:")
instanceOfN.printValue()                // "Class N"
print("\tClass O:")
instanceOfO.printValue()                // "Class O"
print("\tClass P:")
instanceOfP.printValue()                // "Class P"
print("\tClass Q:")
instanceOfQ.printValue()                // "Class Q"
print("\tClass R:")
instanceOfR.printValue()                // "Class R"

// MARK: Calling via a function that requires the argument be the Root protocol
print("\n2) printValueFromProtocolA(_: PA):")
print("\tClass A:")
printValueFromProtocolA(instanceOfA)    // "Protocol A"
print("\tClass B:")
printValueFromProtocolA(instanceOfB)    // "Protocol A"
print("\tClass C:")
printValueFromProtocolA(instanceOfC)    // "Protocol A"
print("\tClass D:")
printValueFromProtocolA(instanceOfD)    // "Class D"
print("\tClass E:")
printValueFromProtocolA(instanceOfE)    // "Class E"
print("\tClass F:")
printValueFromProtocolA(instanceOfF)    // "Protocol A"
print("\tClass G:")
printValueFromProtocolA(instanceOfG)    // "Protocol A"
print("\tClass H:")
printValueFromProtocolA(instanceOfH)    // "Protocol A"
print("\tClass I:")
printValueFromProtocolA(instanceOfI)    // "Protocol A"
print("\tClass J:")
printValueFromProtocolA(instanceOfJ)    // "Protocol C"
print("\tClass K:")
printValueFromProtocolA(instanceOfK)    // "Protocol C"
print("\tClass L:")
printValueFromProtocolA(instanceOfL)    // "Protocol C"
print("\tClass M:")
printValueFromProtocolA(instanceOfM)    // "Protocol A"
print("\tClass N:")
printValueFromProtocolA(instanceOfN)    // "Protocol A"
print("\tClass O:")
printValueFromProtocolA(instanceOfO)    // "Protocol A"
print("\tClass P:")
printValueFromProtocolA(instanceOfP)    // "Protocol A"
print("\tClass Q:")
printValueFromProtocolA(instanceOfQ)    // "Class Q"
print("\tClass R:")
printValueFromProtocolA(instanceOfR)    // "Class R"

// MARK: Calling via a function that requires the argument be the first Derived protocol
print("\n3) printValueFromProtocolB(_: PB):")
print("\tClass D:")
printValueFromProtocolB(instanceOfD)    // "Class D"
print("\tClass E:")
printValueFromProtocolB(instanceOfE)    // "Class E"
print("\tClass H:")
printValueFromProtocolB(instanceOfH)    // "Protocol A"
print("\tClass I:")
printValueFromProtocolB(instanceOfI)    // "Protocol A"
print("\tClass J:")
printValueFromProtocolB(instanceOfJ)    // "Protocol C"
print("\tClass K:")
printValueFromProtocolB(instanceOfK)    // "Protocol C"
print("\tClass L:")
printValueFromProtocolB(instanceOfL)    // "Protocol C"
print("\tClass N:")
printValueFromProtocolB(instanceOfN)    // "Protocol A"
print("\tClass O:")
printValueFromProtocolB(instanceOfO)    // "Protocol A"
print("\tClass P:")
printValueFromProtocolB(instanceOfP)    // "Protocol A"
print("\tClass Q:")
printValueFromProtocolB(instanceOfQ)    // "Protocol A"
print("\tClass R:")
printValueFromProtocolB(instanceOfR)    // "Protocol A"

// MARK: Calling via a function that requires the argument be the second Derived protocol
print("\n4) printValueFromProtocolC(_: PC):")
print("\tClass J:")
printValueFromProtocolC(instanceOfJ)    // "Protocol C"
print("\tClass K:")
printValueFromProtocolC(instanceOfK)    // "Protocol C"
print("\tClass L:")
printValueFromProtocolC(instanceOfL)    // "Protocol C"
print("\tClass P:")
printValueFromProtocolC(instanceOfP)    // "Protocol C"
print("\tClass Q:")
printValueFromProtocolC(instanceOfQ)    // "Class Q"
print("\tClass R:")
printValueFromProtocolC(instanceOfR)    // "Class R"

// MARK: Calling via a function that requires the argument be an instance or subclass of Class D
print("\n5) printValueFromClassD(_: D):")
print("\tClass D:")
printValueFromClassD(instanceOfD)       // "Class D"
print("\tClass E:")
printValueFromClassD(instanceOfE)       // "Class E"

// MARK: Calling via a function that requires the argument be an instance or subclass of Class A
print("\n6) printValueFromClassA(_: A):")
print("\tClass A:")
printValueFromClassA(instanceOfA)       // "Protocol A"
print("\tClass B:")
printValueFromClassA(instanceOfB)       // "Protocol A"
print("\tClass C:")
printValueFromClassA(instanceOfC)       // "Protocol A"
print("\tClass G:")
printValueFromClassA(instanceOfG)       // "Protocol A"

// MARK: Calling via a function that requires the argument be an instance or subclass of Class H
print("\n7) printValueFromClassH(_: H):")
print("\tClass H:")
printValueFromClassH(instanceOfH)       // "Protocol A"
print("\tClass I:")
printValueFromClassH(instanceOfI)       // "Protocol A"
print("\tClass N:")
printValueFromClassH(instanceOfN)       // "Protocol A"
print("\tClass O:")
printValueFromClassH(instanceOfO)       // "Protocol A"

// MARK: Calling via a function that requires the argument be an instance or subclass of Class J
print("\n8) printValueFromClassJ(_: J):")
print("\tClass J:")
printValueFromClassJ(instanceOfJ)       // "Protocol C"
print("\tClass K:")
printValueFromClassJ(instanceOfK)       // "Protocol C"
print("\tClass L:")
printValueFromClassJ(instanceOfL)       // "Protocol C"
print("\tClass P:")
printValueFromClassJ(instanceOfP)       // "Protocol C"

THE PRINTOUT:

1) Direct Method Call:
    Class A:
        Protocol A
    Class B:
        Class B
    Class C:
        Class C
    Class D:
        Class D
    Class E:
        Class E
    Class F:
        Class F
    Class G:
        Class G
    Class H:
        Protocol A
    Class I:
        Class I
    Class J:
        Protocol C
    Class K:
        Class K
    Class L:
        Class L
    Class M:
        Class M
    Class N:
        Class N
    Class O:
        Class O
    Class P:
        Class P
    Class Q:
        Class Q
    Class R:
        Class R

2) printValueFromProtocolA(_: PA):
    Class A:
        Protocol A
    Class B:
        Protocol A
    Class C:
        Protocol A
    Class D:
        Class D
    Class E:
        Class E
    Class F:
        Protocol A
    Class G:
        Protocol A
    Class H:
        Protocol A
    Class I:
        Protocol A
    Class J:
        Protocol C
    Class K:
        Protocol C
    Class L:
        Protocol C
    Class M:
        Protocol A
    Class N:
        Protocol A
    Class O:
        Protocol A
    Class P:
        Protocol C
    Class Q:
        Class Q
    Class R:
        Class R

3) printValueFromProtocolB(_: PB):
    Class D:
        Class D
    Class E:
        Class E
    Class H:
        Protocol A
    Class I:
        Protocol A
    Class J:
        Protocol C
    Class K:
        Protocol C
    Class L:
        Protocol C
    Class N:
        Protocol A
    Class O:
        Protocol A
    Class P:
        Protocol C
    Class Q:
        Class Q
    Class R:
        Class R

4) printValueFromProtocolC(_: PC):
    Class J:
        Protocol C
    Class K:
        Protocol C
    Class L:
        Protocol C
    Class P:
        Protocol C
    Class Q:
        Class Q
    Class R:
        Class R

5) printValueFromClassD(_: D):
    Class D:
        Class D
    Class E:
        Class E

6) printValueFromClassA(_: A):
    Class A:
        Protocol A
    Class B:
        Protocol A
    Class C:
        Protocol A
    Class G:
        Protocol A

7) printValueFromClassH(_: H):
    Class H:
        Protocol A
    Class I:
        Protocol A
    Class N:
        Protocol A
    Class O:
        Protocol A

8) printValueFromClassJ(_: J):
    Class J:
        Protocol C
    Class K:
        Protocol C
    Class L:
        Protocol C
    Class P:
        Protocol C

Solution

  • To override a method that method needs to be implemented, so if you try to use the override keyword in B you get a compilation error since while A conforms to PA it doesn't implement the function in PA but uses the default one instead.

    So this is most likely the reason for the confusion, since A hasn't implemented printValue it can't be overridden even though objects of type A can be called with it, a.printValue(), since it relies on the default implementation of the protocol.

    Given this I think most of the test cases can be understood why they behave the way they do although I have only focused on case 6.

    Implement printValue() in A and see what happens. You get compilation errors first that needs to be fixed but after that the output will be what you have expected.

    Regarding the print out for case 6, since nothing is overriden from A all objects sent to printValueFromClassA will be treated as an instance of A and will be using the protocol default implementation.