Swift Protocol Extensions overriding
Asked Answered
J

5

68

I'm experimenting with Swift protocol extensions and I found this quite confusing behaviour. Could you help me how to get the result I want?

See the comments on the last 4 lines of the code. (You can copy paste it to Xcode7 playground if you want). Thank you!!

protocol Color { }
extension Color {  var color : String { return "Default color" } }

protocol RedColor: Color { }
extension RedColor { var color : String { return "Red color" } }


protocol PrintColor {
    
     func getColor() -> String
}

extension PrintColor where Self: Color {
    
    func getColor() -> String {
        
        return color
    }
}


class A: Color, PrintColor { }
class B: A, RedColor { }


let colorA = A().color // is "Default color" - OK
let colorB = B().color // is "Red color" - OK


let a = A().getColor() // is "Default color" - OK
let b = B().getColor() // is "Default color" BUT I want it to be "Red color"
Jig answered 15/7, 2015 at 13:28 Comment(0)
K
69

The short answer is that protocol extensions don't do class polymorphism. This makes a certain sense, because a protocol can be adopted by a struct or enum, and because we wouldn't want the mere adoption of a protocol to introduce dynamic dispatch where it isn't necessary.

Thus, in getColor(), the color instance variable (which may be more accurately written as self.color) doesn't mean what you think it does, because you are thinking class-polymorphically and the protocol is not. So this works:

let colorB = B().color // is "Red color" - OK

...because you are asking a class to resolve color, but this doesn't do what you expect:

let b = B().getColor() // is "Default color" BUT I want it to be "Red color"

...because the getColor method is defined entirely in a protocol extension. You can fix the problem by redefining getColor in B:

class B: A, RedColor {
    func getColor() -> String {
        return self.color
    }
}

Now the class's getColor is called, and it has a polymorphic idea of what self is.

Katar answered 15/7, 2015 at 14:2 Comment(4)
Good discussion here: nomothetis.svbtle.com/the-ghost-of-swift-bugs-futureKatar
Thank you for your answer. Let me redefine my question: Is there a way how to modify some instance variable simply by adding a protocol to the class? Let's say we have class A and A.text is "Hello" . But class A:Protocol would cause A.text to be "Ciao"?Jig
No, that would make no sense. Again, think about what a protocol is for. It would be terrible if mere adoption of a protocol had more power than the class itself to say what something should be or do. — However, read the article I pointed you to, because it shows that if an object reference is typed as the protocol, the protocol's implementation can be preferred over the adopter's.Katar
@Jig Constrain the extension to more than one protocol extension Lang where Self is Lang & Italian{print("Ciao")}Fusspot
H
15

There are two very different issues at play here: The dynamic behavior of protocols and the resolution of protocol "default" implementations.

  1. On the dynamic front, we can illustrate the problem with a simple example:

    protocol Color { }
    
    extension Color {
        var color: String { return "Default color" }
    }
    
    class BlueBerry: Color {
        var color: String { return "Blue color" }
    }
    
    let berry = BlueBerry()
    print("\(berry.color)")                 // prints "Blue color", as expected
    
    let colorfulThing: Color = BlueBerry()
    print("\(colorfulThing.color)")         // prints "Default color"!
    

    As you point out in your answer, you can get the dynamic behavior if you define color as part of the original Color protocol (i.e. thereby instructing the compiler to reasonably expect the conforming classes to implement this method and only use the protocol's implementation if none is found):

    protocol Color {
        var color: String { get }
    }
    
    ...
    
    let colorfulThing: Color = BlueBerry()
    print("\(colorfulThing.color)")         // now prints "Blue color", as expected
    
  2. Now, in your answer, you question why this falls apart a bit when B is a subclass of A.

    I think it helps to remember that the method implementations in protocol extensions are "default" implementations, i.e. implementations to be used if the conforming class doesn't implement it, itself. The source of the confusion in your case comes from the fact that B conforms to RedColor which has a default implementation for color, but B is also a subclass of A which conforms to Color, which has a different default implementation of color.

    So, we might quibble about Swift's handling of this situation (personally I'd rather see a warning about this inherently ambiguous situation), but the root of the problem, in my mind, is that there are two different hierarchies (the OOP object hierarchy of subclasses and the POP protocol hierarchy of protocol inheritance) and this results in two competing "default" implementations.

I know this is an old question, so you've probably long since moved on to other things, which is fine. But if you're still struggling regarding the right way to refactor this code, share a little about what this class hierarchy and what this protocol inheritance actually represent and we might be able to offer more concrete counsel. This is one of those cases where abstract examples just further confuse the issue. Let's see what the types/protocols really are. (If you've got working code, http://codereview.stackexchange.com might be the better venue.)

Hibernaculum answered 23/1, 2017 at 19:34 Comment(0)
C
5

I managed to get it working by defining color on Color and switching the implementation list for B. Not much good if B must be an A though.

protocol Color {
    var color : String { get }
}

protocol RedColor: Color {

}

extension Color {
    var color : String {
        get {return "Default color"}
    }
}

extension RedColor {
    var color : String {
        get {return "Red color"}
    }
}

protocol PrintColor {
    func getColor() -> String
}

extension PrintColor where Self: Color {
    func getColor() -> String {
        return color
    }
}

class A : Color, PrintColor {

}

class B : RedColor, PrintColor {

}

let a = A().getColor() // "Default color"
let b = B().getColor() // "Red color"
Chivalrous answered 12/7, 2016 at 18:51 Comment(0)
C
4

I came across this problem whilst trying to implement an "optional" method through a protocol. It can be made to work, in structs, in classes that do not inherit, and also in classes that inherit from a base that implements a non-protocol-default method which can be overridden. The only case that doesn't work is a class that inherits from a base that declares conformity but doesn't provide its own "non-default" implementation - in that case the protocol extension's default is "baked-in" to the base class, and cannot be overridden or re-defined.

Simple example:

typealias MyFunction = () -> ()
protocol OptionalMethod {
    func optionalMethod() -> MyFunction?
    func executeOptionalMethod()
}
extension OptionalMethod {
    func optionalMethod() -> MyFunction? { return nil }
    func executeOptionalMethod() {
        if let myFunc = self.optionalMethod() {
            myFunc()
        } else {
            print("Type \(self) has not implemented `optionalMethod`")
        }
    }
}

class A: OptionalMethod {
}
class B: A {
    func optionalMethod() -> MyFunction? {
        return { print("Hello optional method") }
    }
}
struct C: OptionalMethod {
    func optionalMethod() -> MyFunction? {
        return { print("Hello optionalMethod") }
    }
}
class D: OptionalMethod {
    func optionalMethod() -> MyFunction? {
        return { print("Hello optionalMethod") }
    }
}
class E: D {
    override func optionalMethod() -> MyFunction? {
        return { print("Hello DIFFERENT optionalMethod") }
    }
}
/* Attempt to get B to declare its own conformance gives:
// error: redundant conformance of 'B2' to protocol 'OptionalMethod'
class B2: A, OptionalMethod {
    func optionalMethod() -> MyFunction? {
        return { print("Hello optional method") }
    }
}
*/
class A2: OptionalMethod {
    func optionalMethod() -> MyFunction? {
        return nil
    }
}
class B2: A2 {
    override func optionalMethod() -> MyFunction? {
        return { print("Hello optionalMethod") }
    }
}

let a = A() // Class A doesn't implement & therefore defaults to protocol extension implementation
a.executeOptionalMethod() // Type __lldb_expr_201.A has not implemented `optionalMethod`
let b = B() // Class B implements its own, but "inherits" implementation from superclass A
b.executeOptionalMethod() // Type __lldb_expr_205.B has not implemented `optionalMethod`
let c = C() // Struct C implements its own, and works
c.executeOptionalMethod() // Hello optionalMethod
let d = D() // Class D implements its own, inherits from nothing, and works
d.executeOptionalMethod() // Hello optionalMethod
let e = E() // Class E inherits from D, but overrides, and works
e.executeOptionalMethod() // Hello DIFFERENT optionalMethod
let a2 = A2() // Class A2 implements the method, but returns nil, (equivalent to A)
a2.executeOptionalMethod() // Type __lldb_expr_334.A2 has not implemented `optionalMethod`
let b2 = B2() // Class B2 overrides A2's "nil" implementation, and so works
b2.executeOptionalMethod() // Hello optionalMethod
Crymotherapy answered 4/10, 2017 at 17:43 Comment(0)
B
4

Note: The proposed solution "Defining color as part of the original Color protocol" is not solving the problem when you have inheritance involved e.g. RedBerry inherits from BlueBerry which conforms to protocol Color.

protocol Color {
    var color: String { get }
}

extension Color {
    var color: String { return "Default color" }
}

class BlueBerry: Color {
    //    var color: String { return "Blue color" }
}

class RedBerry: BlueBerry {
    var color: String { return "Red color" }
}

let berry = RedBerry()
print(berry.color)             // Red color

let colorfulThing: Color = RedBerry()
print(colorfulThing.color)     // Actual: Default color, Expected: Red color
Baking answered 11/4, 2018 at 14:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.