Dans Swift, comment puis-je déclarer une variable d'un type spécifique conforme à un ou plusieurs protocoles?

96

Dans Swift, je peux définir explicitement le type d'une variable en la déclarant comme suit:

var object: TYPE_NAME

Si nous voulons aller plus loin et déclarer une variable conforme à plusieurs protocoles, nous pouvons utiliser le protocoldéclaratif:

var object: protocol<ProtocolOne,ProtocolTwo>//etc

Que faire si je souhaite déclarer un objet conforme à un ou plusieurs protocoles et qui est également d'un type de classe de base spécifique? L'équivalent Objective-C ressemblerait à ceci:

NSSomething<ABCProtocolOne,ABCProtocolTwo> * object = ...;

Dans Swift, je m'attendrais à ce que cela ressemble à ceci:

var object: TYPE_NAME,ProtocolOne//etc

Cela nous donne la flexibilité de pouvoir gérer l'implémentation du type de base ainsi que l'interface ajoutée définie dans le protocole.

Y a-t-il un autre moyen plus évident que je pourrais manquer?

Exemple

À titre d'exemple, disons que j'ai une UITableViewCellusine qui est chargée de renvoyer des cellules conformes à un protocole. Nous pouvons facilement configurer une fonction générique qui retourne des cellules conformes à un protocole:

class CellFactory {
    class func createCellForItem<T: UITableViewCell where T:MyProtocol >(item: SpecialItem,tableView: UITableView) -> T {
        //etc
    }
}

plus tard, je veux retirer ces cellules de la file d'attente tout en exploitant à la fois le type et le protocole

var cell: MyProtocol = CellFactory.createCellForItem(somethingAtIndexPath) as UITableViewCell

Cela renvoie une erreur car une cellule de vue tableau n'est pas conforme au protocole ...

Je voudrais pouvoir spécifier que la cellule est a UITableViewCellet est conforme à la MyProtocoldans la déclaration de variable?

Justification

Si vous êtes familier avec le modèle d'usine, cela aurait du sens dans le contexte de pouvoir renvoyer des objets d'une classe particulière qui implémentent une certaine interface.

Tout comme dans mon exemple, nous aimons parfois définir des interfaces qui ont du sens lorsqu'elles sont appliquées à un objet particulier. Mon exemple de cellule de vue tableau est une de ces justifications.

Bien que le type fourni ne soit pas exactement conforme à l'interface mentionnée, l'objet renvoyé par l'usine le fait et j'aimerais donc avoir la flexibilité d'interagir avec le type de classe de base et l'interface de protocole déclarée

Daniel Galasko
la source
Désolé, mais à quoi ça sert en bref. Les types savent déjà à quels protocoles ils se conforment. Qu'est-ce pas seulement utiliser le type?
Kirsteins
1
@Kirsteins Pas à moins que le type ne soit renvoyé par une usine et soit donc un type générique avec une classe de base commune
Daniel Galasko
Veuillez donner un exemple si possible.
Kirsteins
NSSomething<ABCProtocolOne,ABCProtocolTwo> * object = ...;. Cet objet semble tout à fait inutile car il NSSomethingsait déjà à quoi il se conforme. S'il n'est pas conforme à l'un des protocoles de, <>vous obtiendrez des unrecognised selector ...plantages. Cela n'offre aucune sécurité de type.
Kirsteins
@Kirsteins Veuillez revoir mon exemple, il est utilisé lorsque vous savez que l'objet que votre usine vend est d'une classe de base particulière conforme à un protocole spécifié
Daniel Galasko

Réponses:

72

Dans Swift 4, il est maintenant possible de déclarer une variable qui est une sous-classe d'un type et implémente un ou plusieurs protocoles en même temps.

var myVariable: MyClass & MyProtocol & MySecondProtocol

Pour faire une variable facultative:

var myVariable: (MyClass & MyProtocol & MySecondProtocol)?

ou comme paramètre d'une méthode:

func shakeEm(controls: [UIControl & Shakeable]) {}

Apple l'a annoncé à la WWDC 2017 lors de la session 402: Quoi de neuf dans Swift

Deuxièmement, je veux parler de la composition de classes et de protocoles. Donc, ici, j'ai présenté ce protocole modifiable pour un élément d'interface utilisateur qui peut donner un petit effet de secousse pour attirer l'attention sur lui-même. Et je suis allé de l'avant et j'ai étendu certaines des classes UIKit pour fournir réellement cette fonctionnalité de shake. Et maintenant, je veux écrire quelque chose qui semble simple. Je veux juste écrire une fonction qui prend un tas de contrôles qui peuvent être secoués et secoue ceux qui sont activés pour attirer l'attention sur eux. Quel type puis-je écrire ici dans ce tableau? C'est en fait frustrant et délicat. Donc, je pourrais essayer d'utiliser un contrôle d'interface utilisateur. Mais tous les contrôles de l'interface utilisateur ne sont pas modifiables dans ce jeu. Je pourrais essayer shakable, mais tous les shakables ne sont pas des contrôles d'interface utilisateur. Et il n'y a en fait aucun bon moyen de représenter cela dans Swift 3.Swift 4 introduit la notion de composition d'une classe avec un nombre quelconque de protocoles.

Philipp Otto
la source
3
Il suffit d'ajouter un lien vers la proposition d'évolution rapide github.com/apple/swift-evolution/blob/master/proposals/…
Daniel Galasko
Merci Philipp!
Omar Albeik
et si besoin d'une variable optionnelle de ce type?
Vyachaslav Gerchicov
2
@VyachaslavGerchicov: Vous pouvez mettre des parenthèses autour, puis le point d'interrogation comme ceci: var myVariable: (MyClass & MyProtocol & MySecondProtocol)?
Philipp Otto
30

Vous ne pouvez pas déclarer de variable comme

var object:Base,protocol<ProtocolOne,ProtocolTwo> = ...

ni déclarer le type de retour de fonction comme

func someFunc() -> Base,protocol<MyProtocol,Protocol2> { ... }

Vous pouvez déclarer comme paramètre de fonction comme celui-ci, mais il s'agit essentiellement d'une conversion ascendante.

func someFunc<T:Base where T:protocol<MyProtocol1,MyProtocol2>>(val:T) {
    // here, `val` is guaranteed to be `Base` and conforms `MyProtocol` and `MyProtocol2`
}

class SubClass:BaseClass, MyProtocol1, MyProtocol2 {
   //...
}

let val = SubClass()
someFunc(val)

À partir de maintenant, tout ce que vous pouvez faire est de:

class CellFactory {
    class func createCellForItem(item: SpecialItem) -> UITableViewCell {
        return ... // any UITableViewCell subclass
    }
}

let cell = CellFactory.createCellForItem(special)
if let asProtocol = cell as? protocol<MyProtocol1,MyProtocol2> {
    asProtocol.protocolMethod()
    cell.cellMethod()
}

Avec cela, techniquement cellest identique à asProtocol.

Mais, comme pour le compilateur, celln'a UITableViewCellqu'une interface , tandis que asProtocoln'a qu'une interface de protocoles. Ainsi, lorsque vous souhaitez appeler UITableViewCellles méthodes de, vous devez utiliser cellvariable. Lorsque vous souhaitez appeler la méthode des protocoles, utilisez asProtocolvariable.

Si vous êtes sûr que la cellule est conforme aux protocoles, vous n'avez pas à utiliser if let ... as? ... {}. comme:

let cell = CellFactory.createCellForItem(special)
let asProtocol = cell as protocol<MyProtocol1,MyProtocol2>
Rintaro
la source
Étant donné que l'usine spécifie les types de retour, je n'ai pas techniquement besoin d'effectuer le cast optionnel? Je pourrais simplement me fier au typage implicite Swift pour effectuer le typage là où je déclare explicitement les protocoles?
Daniel Galasko
Je ne comprends pas ce que tu veux dire, désolé pour mes mauvaises compétences en anglais. Si vous parlez de -> UITableViewCell<MyProtocol>, ce n'est pas valide, car ce UITableViewCelln'est pas un type générique. Je pense que cela ne compile même pas.
rintaro
Je ne fais pas référence à votre implémentation générique mais plutôt à votre exemple de mise en œuvre. où vous dites let asProtocol = ...
Daniel Galasko
ou, je pourrais simplement faire: var cell: protocol <ProtocolOne, ProtocolTwo> = someObject as UITableViewCell et profiter des deux dans une variable
Daniel Galasko
2
Je ne pense pas. Même si vous pouviez faire comme ça, cellil n'y a que des méthodes de protocoles (pour le compilateur).
rintaro
2

Malheureusement, Swift ne prend pas en charge la conformité du protocole au niveau objet. Cependant, il existe une solution de contournement quelque peu délicate qui peut servir vos objectifs.

struct VCWithSomeProtocol {
    let protocol: SomeProtocol
    let viewController: UIViewController

    init<T: UIViewController>(vc: T) where T: SomeProtocol {
        self.protocol = vc
        self.viewController = vc
    }
}

Ensuite, partout où vous avez besoin de faire quoi que ce soit que possède UIViewController, vous accéderez à l'aspect .viewController de la structure et tout ce dont vous avez besoin de l'aspect protocole, vous référeriez au .protocol.

Par exemple:

class SomeClass {
   let mySpecialViewController: VCWithSomeProtocol

   init<T: UIViewController>(injectedViewController: T) where T: SomeProtocol {
       self.mySpecialViewController = VCWithSomeProtocol(vc: injectedViewController)
   }
}

Désormais, chaque fois que vous avez besoin de mySpecialViewController pour faire quoi que ce soit lié à UIViewController, vous faites simplement référence à mySpecialViewController.viewController et chaque fois que vous en avez besoin pour effectuer une fonction de protocole, vous référencez mySpecialViewController.protocol.

Espérons que Swift 4 nous permettra de déclarer un objet avec des protocoles qui lui sont attachés à l'avenir. Mais pour l'instant, cela fonctionne.

J'espère que cela t'aides!

Michael Curtis
la source
1

EDIT: Je me suis trompé , mais si quelqu'un d'autre lit ce malentendu comme moi, je laisse cette réponse là-bas. L'OP a demandé comment vérifier la conformité du protocole de l'objet d'une sous-classe donnée, et c'est une autre histoire comme le montre la réponse acceptée. Cette réponse parle de la conformité du protocole pour la classe de base.

Peut-être que je me trompe, mais ne parlez-vous pas d'ajouter la conformité de protocole à la UITableCellViewclasse? Le protocole est dans ce cas étendu à la classe de base, et non à l'objet. Consultez la documentation d'Apple sur déclaration d'adoption de protocole avec une extension qui, dans votre cas, serait quelque chose comme:

extension UITableCellView : ProtocolOne {}

// Or alternatively if you need to add a method, protocolMethod()
extension UITableCellView : ProcotolTwo {
   func protocolTwoMethod() -> String {
     return "Compliant method"
   }
}

En plus de la documentation Swift déjà référencée, consultez également l'article de Nate Cooks Fonctions génériques pour les types incompatibles avec d'autres exemples.

Cela nous donne la flexibilité de pouvoir gérer l'implémentation du type de base ainsi que l'interface ajoutée définie dans le protocole.

Y a-t-il un autre moyen plus évident que je pourrais manquer?

L'adoption du protocole fera exactement cela, faire adhérer un objet au protocole donné. Soyez cependant conscient du côté négatif, qu'une variable d'un type de protocole donné ne savoir quoi que ce soit en dehors du protocole. Mais cela peut être contourné en définissant un protocole contenant toutes les méthodes / variables / ...

Bien que le type fourni ne soit pas exactement conforme à l'interface mentionnée, l'objet renvoyé par l'usine le fait et j'aimerais donc avoir la flexibilité d'interagir avec le type de classe de base et l'interface de protocole déclarée

Si vous souhaitez qu'une méthode générique, une variable se conforme à la fois à un protocole et à des types de classe de base, vous pourriez être malchanceux. Mais il semble que vous deviez définir le protocole suffisamment large pour avoir les méthodes de conformité nécessaires, et en même temps assez étroit pour avoir la possibilité de l'adopter aux classes de base sans trop de travail (c'est-à-dire simplement déclarer qu'une classe est conforme au protocole).

Holroy
la source
1
Ce n'est pas du tout ce dont je parle mais merci :) Je voulais pouvoir interfacer avec un objet à la fois par sa classe et par un protocole spécifique. Tout comme comment dans obj-c je peux faire NSObject <MyProtocol> obj = ... Inutile de dire que cela ne peut pas être fait rapidement, vous devez
convertir
0

Une fois, j'ai eu une situation similaire en essayant de lier mes connexions d'interacteurs génériques dans Storyboards (IB ne vous permettra pas de connecter des prises à des protocoles, uniquement des instances d'objet), que j'ai contournée en masquant simplement la classe de base public ivar avec un calcul privé propriété. Bien que cela n'empêche pas quelqu'un d'effectuer des affectations illégales en soi, cela constitue un moyen pratique d'empêcher en toute sécurité toute interaction indésirable avec une instance non conforme au moment de l'exécution. (c'est-à-dire éviter d'appeler des méthodes déléguées à des objets qui ne sont pas conformes au protocole.)

Exemple:

@objc protocol SomeInteractorInputProtocol {
    func getSomeString()
}

@objc protocol SomeInteractorOutputProtocol {
    optional func receiveSomeString(value:String)
}

@objc class SomeInteractor: NSObject, SomeInteractorInputProtocol {

    @IBOutlet var outputReceiver : AnyObject? = nil

    private var protocolOutputReceiver : SomeInteractorOutputProtocol? {
        get { return self.outputReceiver as? SomeInteractorOutputProtocol }
    }

    func getSomeString() {
        let aString = "This is some string."
        self.protocolOutputReceiver?.receiveSomeString?(aString)
    }
}

Le "outputReceiver" est déclaré facultatif, tout comme le "protocolOutputReceiver" privé. En accédant toujours au outputReceiver (aka délégué) via ce dernier (la propriété calculée), je filtre efficacement tous les objets qui ne sont pas conformes au protocole. Maintenant, je peux simplement utiliser le chaînage facultatif pour appeler en toute sécurité l'objet délégué, qu'il implémente ou non le protocole ou même qu'il existe.

Pour appliquer cela à votre situation, vous pouvez faire en sorte que l'ivar public soit de type "YourBaseClass?" (par opposition à AnyObject) et utilisez la propriété privée calculée pour appliquer la conformité du protocole. FWIW.

thym vif
la source