[Swift] Protocole: renvoyer self ou le type du protocole ?

JérémyJérémy Membre
août 2016 modifié dans Objective-C, Swift, C, C++ #1

Puisque nous sommes dans les bonnes pratiques, j'aurais une petite question.



protocol TotoProtocol {
var name: String { get }
func myFunc()-> TotoProtocol
}

struct Toto: TotoProtocol {
let name: String

init(toto: String) {
self.toto = toto
}

func myFunc()-> TotoProtocol {
return self
}
}

VS



protocol TotoProtocol {
var name: String { get }

func myFunc()-> Self
}

struct Toto: TotoProtocol {
let name: String

init(toto: String) {
self.toto = toto
}

func myFunc()-> Self {
return self
}
}

Ma question est simple. Vaut il mieux privilégié (tant que possible) un retour du type TotoProtocol ou de son implémentation Toto ?


[Edit] : correction du code


Réponses

  • Joanna CarterJoanna Carter Membre, Modérateur
    Je ne peux pas voir aucune différence entre les deux versions.
  • JérémyJérémy Membre
    août 2016 modifié #3

    Effectivement j'ai fait une petite boulette. Je viens d'apporter les modifications.  ::)


  • En général je mets le retour du Type mais ça dépend de l'architecture du code et de ce qu'on souhaite faire. Je pense pas qu'il y ait de bonnes pratiques ici. C'est surtout dépendant du besoin selon moi.


  • CéroceCéroce Membre, Modérateur
    août 2016 modifié #5
    J'ai réfléchi à  la question...
    Si tu mets TotoProtocol (ex. 1), alors il me semble que le compilateur voit que la fonction renvoie un objet générique conforme à  TotoProtocol. Ceci te permet de l'utiliser dans tout ce qui requiert un TotoProtocol.

    L'inconvénient, est que tu ne peux pas l'utiliser (sans caster) dans ce qui requiert un Toto. Donc, il me semble qu'il vaut mieux typer en Self (ex. 2): ça fonctionne ainsi dans les deux cas, que ça requiert un Toto ou un TotoProtocol.
  • CéroceCéroce Membre, Modérateur
    Du coup j'ai essayé, et ma réflexion est bonne, la dernière ligne ne passe pas.


    protocol TotoProtocol {
    var name: String { get }

    func myFunc()-> TotoProtocol
    }

    struct Lala: TotoProtocol {
    let name: String
    let nickname: String

    init(name: String) {
    self.name = name
    nickname = "Pepe"
    }

    func myFunc()-> TotoProtocol {
    return self
    }
    }

    func printNicknameOfToto(lala: Lala) {
    print("\(lala.nickname)")
    }


    let lala = Lala(name: "Coucou")
    let tutu = lala.myFunc()
    printNicknameOfToto(tutu)
  • C'est pour cette raison que je demande, en paramètre, un objet qui implémente le protocole.



    func printNicknameOfToto(lala: TotoProtocol) {
    print("\(lala.nickname)")
    }

    printNicknameOfToto(tutu)

  • CéroceCéroce Membre, Modérateur
    Dans mon exemple, le protocole n'a pas de propriété .nickname. Il faut donc bien une instance de type Lala.
  • CéroceCéroce Membre, Modérateur
    août 2016 modifié #9

    C'est pour cette raison que je demande, en paramètre, un objet qui implémente le protocole.

    func printNicknameOfToto(lala: TotoProtocol) {
    print("\(lala.nickname)")
    }

    printNicknameOfToto(tutu)


    ça compile ça? TotoProtocol n'a pas de propriété nickname; il me semble que la fonction devrait exiger nécessairement un paramètre de type Lala.
  • Arf... Je te présente mes excuses, je n'avais pas vu que printNicknameOfToto ne faisait pas parti du protocol TotoProtocol.


  • Joanna CarterJoanna Carter Membre, Modérateur
    août 2016 modifié #11

    Avec la fonction myFunc, la différence entre renvoyant TotoProtocol et Self :


     


    Avec TotoProtocol, on renvoie une instance de Toto mais elle est tenue dans une référence de type TotoProtocol ; du coup, tous les membres de Toto sont cachés, sauf ceux qui implement les membres de TotoProtocol.



    protocol TotoProtocol
    {
    var name: String { get }

    func myFunc() -> TotoProtocol
    }

    struct Toto: TotoProtocol
    {
    let name: String

    let age = 21

    init(name: String)
    {
    self.name = name
    }

    func myFunc() -> TotoProtocol
    {
    return self
    }
    }

    func test()
    {
    let tt = Toto(name: "fred")

    let t2 = tt.myFunc()

    let age = t2.age // erreur : Value of type 'TotoProtocol' has no member 'age'
    }

    Avec Self, on renvoie une instance de Toto, tenue dans une référence de Toto ; du coup, on peut accéder tous ses membres.



    protocol TotoProtocol
    {
    var name: String { get }

    func myFunc() -> Self
    }

    struct Toto: TotoProtocol
    {
    let name: String

    let age = 21

    init(name: String)
    {
    self.name = name
    }

    func myFunc() -> Toto
    {
    return self
    }
    }

    func test()
    {
    let tt = Toto(name: "fred")

    let t2 = tt.myFunc()

    let age = t2.age
    }

    Quand même, avec Self, on pourrait faire :



    func test()
    {
    let tt = Toto(name: "fred")

    let t2: TotoProtocol = tt.myFunc()

    let age = t2.age // erreur : Value of type 'TotoProtocol' has no member 'age'
    }

    Dans ce cas là , t2 est bien assigné une instance de Toto mais, comme avec la première version, les membres de Toto sont cachés.

  • Tu pourrais arriver à  accéder à  la variable mais tu es tout de même obligé de faire un cast (tu peux rencontrer la même problématique en Java quand tu travailles avec les Interfaces).



    struct Toto: TotoProtocol {
    let name: String
    let age = 21

    init(name: String) {
    self.name = name
    }

    func myFunc() -> TotoProtocol {
    return self
    }
    }

    func test() {
    let tt = Toto(name: "fred")
    let t2: toto = tt.myFunc() as! Toto
    let age = t2.age
    }


    Idem pour l'exemple de Céroce :



    protocol TotoProtocol {
    var name: String { get }
    func myFunc()-> TotoProtocol
    }

    struct Lala: TotoProtocol {
    let name: String
    let nickname: String

    init(name: String) {
    self.name = name
    nickname = "Pepe"
    }

    func myFunc()-> TotoProtocol {
    return self
    }
    }

    func printNicknameOfToto(lala: Lala) {
    print("\(lala.nickname)")
    }

    let lala = Lala(name: "Coucou")
    let tutu = lala.myFunc()
    printNicknameOfToto(tutu as! Lala)

    Je n'ai pas testé mais je pense que ça passe.  ::)


  • D'une manière générale avec un protocol toujours spécifier Self quand on renvoie une instance du type qui implémente le-dit protocol.


    ça évite de devoir caster et ça évite aussi d'avoir des surprises en cas d'associatedType dans le protocol.

  • FKDEVFKDEV Membre
    août 2016 modifié #14
    Une remarque :
    Si on implémente le protocol dans une struct, on doit retourner le type de la structure:
     
    struct Tata : TotoProtocol
    {
    func myFunc() -> Tata
    {
    return self
    }
    func myFunc() -> Self //ne compile pas

     
    Alors que si on l'implémente dans une classe, c'est l'inverse, on doit retourner 'Self':
    class Titi : TotoProtocol
    {
    func myFunc() -> Self //OK
    {
    return self
    }
    func myFunc() -> Titi //ne compile pas
    {
    return self
    }
    func myFunc() -> Self //ne compile pas
    {
    return Titi()
    }
    Le problème avec le dernier cas, c'est que si on dérive la classe Titi en Tata, le protocol aura déjà  été implémenté par Titi mais Titi aura renvoyé un objet de type Titi, et donc le protocol qui retourne Self ne sera pas correctement implémenté dans Tata, puisque de part sa classe mère, elle renvoie un objet de type Titi.


    Retourner Self dans un protocol est un véritable casse-tête si on utilise des classes (cf ici en anglais:http://stackoverflow.com/questions/25645090/protocol-func-returning-self)donc ce type de protocol est à  proscrire sauf si on est certain d'utiliser des structs.

    Mais dans ce cas c'est un peu contre l'esprit des protocols qui devraient pouvoir être implémentés par n'importe quel type d'objet (y compris une classe).
  • +1 Pyroh



    protocol TotoProtocol {
    var name: String { get }
    func myFunc()-> Self
    }

    struct Lala: TotoProtocol {
    let name: String
    let nickname: String

    init(name: String) {
    self.name = name
    nickname = "Pepe"
    }

    func myFunc()-> Lala {
    return self
    }
    }

    func printNicknameOfTotoA(lala: Lala) {
    print("\(lala.nickname)")
    }

    func printNicknameOfTotoB(lala: TotoProtocol) {
    print("\(lala.nickname)")
    }

    let lala = Lala(name: "Coucou")
    let tutu = lala.myFunc()
    printNicknameOfTotoA(tutu)
    printNicknameOfTotoB(tutu)

    Dans le code ci dessus, on peut voir que si nous retournont Self (dans notre cas Lala), l'objet retourné pourra très bien passé en paramètre dont les types Lala ou TotoProtocol seront demandés. 


  • FKDEVFKDEV Membre
    août 2016 modifié #16

    Pour ceux qui veulent faire des essais : (désolé pour l'indentation sauvage, j'ai fait ça sur le repl d'IBM).


     



    protocol TotoProtocol
    {
    var name: String { get }

    func myFunc() -> Self
    }

    class Toto: TotoProtocol
    {
    let name: String

    let ageToto = 21

    init(name: String)
    {
    self.name = name
    }

    //func myFunc() -> Toto //Ne compile pas
    func myFunc() -> Self
    {
    print("myFunc called on Toto \(ageToto)")
    //return Toto(name:"toto") // ne compile pas
    return self
    }
    }

    struct Tata : TotoProtocol
    {
    let name: String
    init(name: String)
    {
    self.name = name
    }
    //func myFunc() -> Self //Ne compile pas
    func myFunc() -> Tata
    {
    return Tata(name:"tata")
    }
    }

    class Titi : Toto
    {
    let ageTiti = 22
    /*
    override func myFunc() -> Self
    {
    print("myFunc called on Titi \(ageTiti)")
    return Titi(name:"titi") // ne compile pas

    } */
    }

    class Tyty : Titi
    {
    let ageTyty = 23

    }

    func test()
    {
    let toto = Toto(name: "toto")

    var result = toto.myFunc()
    print ("\(type(of:result)) \(toto.ageToto)")


    let titi = Titi(name:"titi")
    result = titi.myFunc()
    print ("\(type(of:result)) \(titi.ageTiti)")

    let tyty = Tyty(name:"tyty")
    result = tyty.myFunc()
    print ("\(type(of:result)) \(tyty.ageTyty)")
    }

    test()

  • Je ne peux pas tester actuellement (vivement que playground arrive sur iPad)...  :P 


    Tu es sur de ton coup FKDEV ?


     


    Quel intérêt y aurait il de retourner Self si les objets retournés sont forcément du type Tata ?  :/


     


    Par exemple si on exécute le code suivant :



    Let tyty: Tyty = Tyty()
    tyty.myFunc()

    :*


  • Joanna CarterJoanna Carter Membre, Modérateur

    La raison que l'on utilise Self ; c'est lorsqu'on se trouve dans une hiérarchie des classes où le type pourrait être changé selon la classe/sous-classe ; ou pour un protocol où le type qui l'implémente pourrait varier.


     


    Donc, Self - implémentation dynamique ; Type d'implémenteur - implémentation statique


  • D'une manière générale avec un protocol toujours spécifier Self quand on renvoie une instance du type qui implémente le-dit protocol.
    ça évite de devoir caster et ça évite aussi d'avoir des surprises en cas d'associatedType dans le protocol.


    De manière générale, non, surtout pas.
    Si on compte n'implémenter le protocole qu'avec des struct (ou des final class), oui.
    Mais ce n'est pas "général" puisque c'est limité aux struct.

    Attention, pour gagner un cast en sortie, vous vous interdisez l'utilisation d'une hiérarchie de classes derrière le protocol, ce qui est quand même assez restrictif.

    Pour que cela fonctionne dans les classes, il faudrait que le compilateur oblige à  implémenter la fonction à  chaque niveau de la hiérarchie.
    Pour l'instant le compilateur n'accepte la fonction que si on renvoie self. Dans ce cas je ne vois pas trop l'utilité de la fonction.


    Je ne maà®trise pas bien les associatedType, peux-tu détailler ?
  • Joanna CarterJoanna Carter Membre, Modérateur

    L'associatedType fonctionne comme les paramètres de type dans un type générique. Ils prennent la place d'un type pas encore connu dans les vars et funcs.



    struct TestStruct<ValueType>
    {
    var value: ValueType

    func touchValue(value: ValueType)
    {

    }
    }


    protocol TestProtocol
    {
    associatedtype ValueType

    var value: ValueType { get set }

    func touchValue(value: ValueType)
    }

    Le type de renvoie que l'on définie sur une méthode dans un protocol variera selon si on utilise un associatedType dans le protocol ou non.



    protocol TestProtocol
    {
    func creator() -> TestProtocol // permis
    }


    protocol TestProtocol
    {
    associatedtype ValueType

    var value: ValueType { get set }

    func touchValue(value: ValueType)

    // func creator() -> TestProtocol // pas permis : "Protocol 'TestProtocol' can only be used
    // as a generic constraint because it has Self or
    // associated type requirements"

    func creator() -> Self // permis
    }

    Pour l'implémentation avec une struct, on ne peut qu'utiliser le type de la struct pour type de renvoie



    public protocol TestProtocol
    {
    func creator() -> Self
    }


    public struct TestStruct : TestProtocol
    {
    public func creator() -> TestStruct
    {
    return TestStruct()
    }
    }

    Avec les classes, c'est plus compliqué :


     


    Avec une classe non-final, on est obligé d'utiliser Self pour le type de renvoie, et en plus...



    public class TestClass : TestProtocol
    {
    public func creator() -> Self
    {
    return self.dynamicType.init()
    }

    public required init() { }
    }

    ça prend en considération que le type puisse varier selon les sous-classes disponibles



    public class DerivedTestClass : TestClass
    {

    }


    func Test()
    {
    let tc = DerivedTestClass()

    let createdTC = tc.creator() // renvoie instance de DerivedTestClass
    }

    Mais, avec une classe final, on peut utiliser le type de la classe comme type de renvoie car il est bien connu et ne peut pas changer.



    public final class TestClass : TestProtocol
    {
    public func creator() -> TestClass
    {
    return TestClass()
    }
    }

    C'est également permis d'utiliser Self pour l'implémentation mais il faut utiliser le dynamicType pour l'instancier, mais sans besoin d'un required init() sur la classe :



    public final class TestClass : TestProtocol
    {
    public func creator() -> Self
    {
    return self.dynamicType.init()
    }
    }

  • Joanna CarterJoanna Carter Membre, Modérateur
    août 2016 modifié #21

    Si on insistait en voulant une méthode creator dans le protocol TestProtocol avec un associatedType qui renverrait TestProtocol, c'est beaucoup plus compliqué... mais c'est possible



    public protocol TestProtocol
    {
    associatedtype ValueType

    var value: ValueType { get set }

    func touchValue(value: ValueType)

    func creator<ProtocolType : TestProtocol>() -> ProtocolType
    }

    En revanche, l'implémentation dans une struct est un peu plus moche, comprenant la mort éventuelle d'un poney   ::)



    public struct TestStruct : TestProtocol
    {
    public var value: Int

    public func touchValue(value: Int)
    {

    }

    public func creator<ProtocolType : TestProtocol>() -> ProtocolType
    {
    return TestStruct(value: 0) as! ProtocolType
    }
    }

    Dans ce cas là , moi, je resterai avec Self 


  • Je pense que c'est un peu trop compliqué.


    En tous cas pour moi. 

  • Joanna CarterJoanna Carter Membre, Modérateur

    Je pense que c'est un peu trop compliqué.

    En tous cas pour moi.




    Même pour moi. Si tu as plus de questions, poses les et j'essayerai de répondre.
Connectez-vous ou Inscrivez-vous pour répondre.