web-dev-qa-db-fra.com

Swift Protocol Extensions écrasant

J'essaie des extensions de protocole Swift et j'ai trouvé ce comportement assez déroutant. Pourriez-vous m'aider à obtenir le résultat souhaité?

Voir les commentaires sur les 4 dernières lignes du code. (Vous pouvez copier le coller sur le terrain de jeu XCode7 si vous voulez). Je vous remercie!!

//: Playground - noun: a place where people can play

import UIKit

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"
43
VojtaStavik

La réponse courte est que les extensions de protocole ne font pas le polymorphisme de classe. Cela a un certain sens, car un protocole peut être adopté par une structure ou une énumération, et parce que nous ne voudrions pas que la simple adoption d’un protocole introduise une transmission dynamique là où ce n’est pas nécessaire.

Ainsi, dans getColor(), la variable d'instance color (qui peut être plus précisément écrite en tant que self.color) ne signifie pas ce que vous pensez, car vous envisagez une classe polymorphe et le protocole ne l'est pas. Donc ça marche:

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

... parce que vous demandez à class de résoudre color, mais cela ne répond pas à vos attentes:

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

... parce que la méthode getColor est entièrement définie dans une extension de protocole. Vous pouvez résoudre le problème en redéfinissant getColor dans B:

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

La classe getColor est maintenant appelée et elle a une idée polymorphe de ce que self est.

42
matt

J'ai réussi à le faire fonctionner en définissant color sur Color et en changeant la liste d'implémentation pour B. Pas très bon si B doit être un A cependant.

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"
4
Ian Warburton

Il y a deux problèmes très différents en jeu ici: le comportement dynamique des protocoles et la résolution des implémentations de protocoles "par défaut".

  1. Sur le plan dynamique, nous pouvons illustrer le problème avec un exemple simple:

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

    Comme vous le signalez dans votre réponse , vous pouvez obtenir le comportement dynamique si vous définissez color dans le cadre du protocole Color original (c’est-à-dire en ordonnant au compilateur de s’attendre raisonnablement à ce que les classes conformes implémentent cette méthode et utilisent uniquement le protocole. mise en œuvre si aucune n'est trouvée):

    protocol Color {
        var color: String { get }
    }
    
    ...
    
    let colorfulThing: Color = BlueBerry()
    print("\(colorfulThing.color)")         // now prints "Blue color", as expected
    
  2. Maintenant, dans votre réponse , vous vous demandez pourquoi cela s’efface un peu lorsque B est une sous-classe de A

    Je pense qu'il est utile de se rappeler que les implémentations de méthodes dans les extensions de protocole sont des implémentations "par défaut", c'est-à-dire des implémentations à utiliser si la classe conforme ne l'implémente pas elle-même. La source de confusion dans votre cas provient du fait que B est conforme à RedColor qui a une implémentation par défaut pour color, mais B est également une sous-classe de A qui est conforme à Color, qui a une implémentation par défaut différente de color.

    Nous pourrions donc nous interroger sur le traitement de cette situation par Swift (personnellement, je préférerais voir un avertissement concernant cette situation intrinsèquement ambigu), mais la source du problème, à mon sens, réside dans le fait qu'il existe deux hiérarchies différentes (la OOP hiérarchie d'objets des sous-classes et hiérarchie de protocole POP d'héritage de protocole), ce qui entraîne deux implémentations "par défaut" concurrentes.

Je sais que c'est une vieille question et que vous êtes probablement passé depuis longtemps à autre chose, ce qui est bien. Mais si vous avez encore du mal à trouver le bon moyen de refactoriser ce code, partagez un peu plus sur ce que cette hiérarchie de classes et ce que cet héritage de protocole représente réellement et nous pourrons peut-être offrir des conseils plus concrets. C’est l’un de ces cas où des exemples abstraits ne font que compliquer davantage la question. Voyons ce que sont vraiment les types/protocoles. (Si vous avez un code de travail, http://codereview.stackexchange.com est peut-être le meilleur endroit.)

4
Rob

Je suis tombé sur ce problème en essayant d'implémenter une méthode "optionnelle" via un protocole. Il peut être fait pour fonctionner, dans les structures, dans les classes qui n'héritent pas, ainsi que dans les classes qui héritent d'une base qui implémente une méthode non protocole par défaut qui peut être remplacée. Le seul cas qui ne fonctionne pas est une classe qui hérite d'une base qui déclare la conformité mais ne fournit pas sa propre implémentation "non par défaut" - dans ce cas, l'extension de protocole par défaut est "intégrée" à la classe de base , et ne peuvent pas être remplacés ou redéfinis. 

Exemple simple:

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
0
Grimxn

Remarque: La solution proposée "Définir color dans le cadre du protocole Color original" ne résout pas le problème lorsque l'héritage est impliqué, par exemple RedBerry hérite de BlueBerry qui est conforme au protocole 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
0
Grand M