En Swift, ¿cómo puedo declarar una variable de un tipo específico que se ajuste a uno o más protocolos?

96

En Swift puedo establecer explícitamente el tipo de una variable declarándola de la siguiente manera:

var object: TYPE_NAME

Si queremos dar un paso más y declarar una variable que se ajuste a múltiples protocolos, podemos usar el protocoldeclarativo:

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

¿Qué sucede si me gustaría declarar un objeto que se ajusta a uno o más protocolos y también es de un tipo de clase base específico? El equivalente de Objective-C se vería así:

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

En Swift, esperaría que se vea así:

var object: TYPE_NAME,ProtocolOne//etc

Esto nos da la flexibilidad de poder lidiar con la implementación del tipo base, así como con la interfaz agregada definida en el protocolo.

¿Hay otra forma más obvia en la que podría estar perdiendo?

Ejemplo

Como ejemplo, digamos que tengo una UITableViewCellfábrica que se encarga de devolver las células de acuerdo con un protocolo. Podemos configurar fácilmente una función genérica que devuelva celdas que se ajusten a un protocolo:

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

más adelante quiero quitar estas celdas mientras aprovecho tanto el tipo como el protocolo

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

Esto devuelve un error porque una celda de vista de tabla no se ajusta al protocolo ...

Me gustaría poder especificar que la celda es una UITableViewCelly se ajusta a la MyProtocoldeclaración de la variable.

Justificación

Si está familiarizado con Factory Pattern, esto tendría sentido en el contexto de poder devolver objetos de una clase en particular que implementan una determinada interfaz.

Al igual que en mi ejemplo, a veces nos gusta definir interfaces que tienen sentido cuando se aplican a un objeto en particular. Mi ejemplo de la celda de vista de tabla es una de esas justificaciones.

Si bien el tipo suministrado no se ajusta exactamente a la interfaz mencionada, el objeto que devuelve la fábrica sí lo hace y, por lo tanto, me gustaría la flexibilidad para interactuar tanto con el tipo de clase base como con la interfaz de protocolo declarada

Daniel Galasko
fuente
Lo siento, pero de qué sirve esto en Swift. Los tipos ya saben qué protocolos cumplen. ¿Qué no usar el tipo?
Kirsteins
1
@Kirsteins No, a menos que el tipo se devuelva de una fábrica y, por lo tanto, sea un tipo genérico con una clase base común
Daniel Galasko
De ser posible, dé un ejemplo.
Kirsteins
NSSomething<ABCProtocolOne,ABCProtocolTwo> * object = ...;. Este objeto parece bastante inútil porque NSSomethingya sabe a qué se ajusta. Si no se ajusta a uno de los protocolos <>, obtendrá unrecognised selector ...bloqueos. Esto no proporciona ningún tipo de seguridad.
Kirsteins
@Kirsteins Por favor, vea mi ejemplo nuevamente, se usa cuando sabe que el objeto que vende su fábrica es de una clase base particular que se ajusta a un protocolo específico
Daniel Galasko

Respuestas:

72

En Swift 4 ahora es posible declarar una variable que es una subclase de un tipo e implementa uno o más protocolos al mismo tiempo.

var myVariable: MyClass & MyProtocol & MySecondProtocol

Para hacer una variable opcional:

var myVariable: (MyClass & MyProtocol & MySecondProtocol)?

o como parámetro de un método:

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

Apple anunció esto en la WWDC 2017 en la Sesión 402: Novedades de Swift

En segundo lugar, quiero hablar sobre la composición de clases y protocolos. Entonces, aquí he presentado este protocolo movible para un elemento de la interfaz de usuario que puede dar un pequeño efecto de sacudida para llamar la atención sobre sí mismo. Y seguí adelante y extendí algunas de las clases de UIKit para proporcionar esta funcionalidad de agitación. Y ahora quiero escribir algo que parezca simple. Solo quiero escribir una función que tome un montón de controles que se pueden sacudir y agite los que están habilitados para llamar la atención sobre ellos. ¿Qué tipo puedo escribir aquí en esta matriz? En realidad, es frustrante y complicado. Entonces, podría intentar usar un control de interfaz de usuario. Pero no todos los controles de la interfaz de usuario se pueden mover en este juego. Podría probar sacudidas, pero no todas las sacudidas son controles de interfaz de usuario. Y en realidad no hay una buena forma de representar esto en Swift 3.Swift 4 introduce la noción de componer una clase con cualquier número de protocolos.

Philipp Otto
fuente
3
Solo agrego un enlace a la propuesta de evolución rápida github.com/apple/swift-evolution/blob/master/proposals/…
Daniel Galasko
¡Gracias Philipp!
Omar Albeik
¿y si necesita una variable opcional de este tipo?
Vyachaslav Gerchicov
2
@VyachaslavGerchicov: Puede ponerlo entre paréntesis y luego el signo de interrogación como este: var myVariable: (MyClass & MyProtocol & MySecondProtocol)?
Philipp Otto
30

No puedes declarar variables como

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

ni declarar el tipo de retorno de la función como

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

Puede declarar como un parámetro de función como este, pero es básicamente una conversión.

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)

A partir de ahora, todo lo que puede hacer es como:

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()
}

Con esto, técnicamente celles idéntico a asProtocol.

Pero, en cuanto al compilador, celltiene una interfaz de UITableViewCellsolo, mientras que asProtocolsolo tiene una interfaz de protocolos. Entonces, cuando quiera llamar a UITableViewCelllos métodos, debe usar cellvariable. Cuando desee llamar al método de protocolos, use asProtocolvariable.

Si está seguro de que la celda se ajusta a los protocolos, no es necesario que utilice if let ... as? ... {}. me gusta:

let cell = CellFactory.createCellForItem(special)
let asProtocol = cell as protocol<MyProtocol1,MyProtocol2>
rintaro
fuente
Dado que la fábrica especifica los tipos de devolución, técnicamente no necesito realizar el molde opcional. ¿Podría simplemente confiar en la escritura implícita rápida para realizar la escritura donde declaro explícitamente los protocolos?
Daniel Galasko
No entiendo lo que quieres decir, lo siento por mis malas habilidades en inglés. Si está diciendo sobre -> UITableViewCell<MyProtocol>, esto no es válido, porque UITableViewCellno es un tipo genérico. Creo que esto ni siquiera se compila.
rintaro
No me refiero a su implementación genérica, sino a su ejemplo de implementación. donde dices dejar asProtocol = ...
Daniel Galasko
o, simplemente podría hacer: var cell: protocol <ProtocolOne, ProtocolTwo> = someObject como UITableViewCell y obtener el beneficio de ambos en una variable
Daniel Galasko
2
No lo creo. Incluso si pudiera hacer eso, cellsolo tiene métodos de protocolos (para el compilador).
rintaro
2

Desafortunadamente, Swift no admite la conformidad del protocolo a nivel de objeto. Sin embargo, hay una solución un tanto incómoda que puede servir para sus propósitos.

struct VCWithSomeProtocol {
    let protocol: SomeProtocol
    let viewController: UIViewController

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

Luego, en cualquier lugar donde necesite hacer cualquier cosa que tenga UIViewController, accedería al aspecto .viewController de la estructura y cualquier cosa que necesite el aspecto del protocolo, haría referencia al .protocol.

Por ejemplo:

class SomeClass {
   let mySpecialViewController: VCWithSomeProtocol

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

Ahora, cada vez que necesite mySpecialViewController para hacer algo relacionado con UIViewController, simplemente haga referencia a mySpecialViewController.viewController y siempre que lo necesite para realizar alguna función de protocolo, haga referencia a mySpecialViewController.protocol.

Con suerte, Swift 4 nos permitirá declarar un objeto con protocolos adjuntos en el futuro. Pero por ahora esto funciona.

¡Espero que esto ayude!

Michael Curtis
fuente
1

EDITAR: Estaba equivocado , pero si alguien más leyó este malentendido como yo, dejo esta respuesta ahí. El OP preguntó sobre la verificación de la conformidad del protocolo del objeto de una subclase determinada, y esa es otra historia, como muestra la respuesta aceptada. Esta respuesta habla sobre la conformidad del protocolo para la clase base.

Tal vez me equivoque, pero ¿no estás hablando de agregar conformidad con el protocolo a la UITableCellViewclase? En ese caso, el protocolo se extiende a la clase base y no al objeto. Consulte la documentación de Apple sobre cómo declarar la adopción de protocolos con una extensión, que en su caso sería algo como:

extension UITableCellView : ProtocolOne {}

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

Además de la documentación de Swift ya mencionada, consulte el artículo de Nate Cooks Funciones genéricas para tipos incompatibles con más ejemplos.

Esto nos da la flexibilidad de poder lidiar con la implementación del tipo base, así como con la interfaz agregada definida en el protocolo.

¿Hay otra forma más obvia en la que podría estar perdiendo?

La adopción de protocolo hará precisamente esto, hacer que un objeto se adhiera al protocolo dado. Sin embargo, tenga en cuenta el lado adverso, que una variable de un tipo de protocolo determinado no sabe nada fuera del protocolo. Pero esto puede evitarse definiendo un protocolo que tenga todos los métodos / variables / ...

Si bien el tipo suministrado no se ajusta exactamente a la interfaz mencionada, el objeto que devuelve la fábrica sí lo hace y, por lo tanto, me gustaría la flexibilidad para interactuar tanto con el tipo de clase base como con la interfaz de protocolo declarada

Si desea que un método genérico, una variable se ajuste tanto a un protocolo como a los tipos de clase base, es posible que no tenga suerte. Pero parece que necesita definir el protocolo lo suficientemente amplio para tener los métodos de conformidad necesarios y, al mismo tiempo, lo suficientemente estrecho como para tener la opción de adoptarlo en las clases base sin demasiado trabajo (es decir, simplemente declarando que una clase se ajusta a la protocolo).

holroy
fuente
1
Eso no es de lo que estoy hablando, pero gracias :) Quería poder interactuar con un objeto a través de su clase y un protocolo específico. Al igual que en obj-c puedo hacer NSObject <MyProtocol> obj = ... No hace falta decir que esto no se puede hacer rápidamente, tienes que convertir el objeto en su protocolo
Daniel Galasko
0

Una vez tuve una situación similar al intentar vincular mis conexiones interactor genéricas en Storyboards (IB no le permitirá conectar puntos de venta a protocolos, solo instancias de objetos), que solucioné simplemente enmascarando el ivar público de la clase base con un ivar privado calculado propiedad. Si bien esto no evita que alguien realice asignaciones ilegales per se, proporciona una forma conveniente de evitar de forma segura cualquier interacción no deseada con una instancia no conforme en tiempo de ejecución. (es decir, evitar llamar a métodos delegados a objetos que no se ajustan al protocolo).

Ejemplo:

@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)
    }
}

El "outputReceiver" se declara opcional, al igual que el "protocolOutputReceiver" privado. Al acceder siempre al outputReceiver (también conocido como delegado) a través de este último (la propiedad calculada), filtro de manera efectiva cualquier objeto que no se ajuste al protocolo. Ahora puedo simplemente usar el encadenamiento opcional para llamar de manera segura al objeto delegado ya sea que implemente el protocolo o incluso que exista.

Para aplicar esto a su situación, puede hacer que el ivar público sea del tipo "YourBaseClass?" (a diferencia de AnyObject) y utilice la propiedad computada privada para hacer cumplir la conformidad del protocolo. FWIW.

quickthyme
fuente