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


En Swift puedo establecer explícitamente el tipo de una variable al declararla 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 protocol declarativo:

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

¿Qué ocurre si deseo 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 tratar con la implementación del tipo de base así como la interfaz agregada definida en el protocolo.

¿Hay alguna otra forma más obvia que pueda estar perdiendo?

Ejemplo

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

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

más tarde deseo dequeue estas células mientras aprovecho el tipo y 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 UITableViewCell y se ajusta a la MyProtocol en la declaración de variables?

Justificación

Si está familiarizado con el Patrón de fábrica esto tendría sentido en el contexto de poder devolver objetos de una clase particular que implementa una cierta 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.

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


76
2017-10-16 10:12


origen


Respuestas:


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

o como el parámetro de un método:

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

Apple anunció esto en la WWDC 2017 en Sesión 402: ¿Qué hay de nuevo en Swift?

En segundo lugar, quiero hablar sobre la composición de clases y protocolos. Entonces, aquí   Introduje este protocolo para un elemento UI que puede dar   un pequeño efecto de sacudida para llamar la atención sobre sí mismo. Y he seguido adelante   y extendió algunas de las clases de UIKit para proporcionar realmente este batido   funcionalidad. Y ahora quiero escribir algo que parece simple. yo   Solo quiero escribir una función que toma un montón de controles que son   shakable y sacude los que están habilitados para llamar la atención a   ellos. ¿Qué tipo puedo escribir aquí en este conjunto? Es en realidad   frustrante y tramposo. Entonces, podría intentar usar un control de UI. Pero no   todos los controles de UI se pueden mover en este juego. Podría intentar shakable, pero   no todos los shakables son controles de UI. Y en realidad no hay una buena manera de   representar esto en Swift 3. Swift 4 introduce la noción de componer   una clase con cualquier cantidad de protocolos.


42
2017-07-24 09:07



No puedes declarar una variable como

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

ni declarar el tipo de devolución de función como

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

Puede declarar como un parámetro de función como este, pero básicamente se trata de un up-casting.

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 puedes 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 cell es idéntico a asProtocol.

Pero, en cuanto al compilador, cell tiene interfaz de UITableViewCell solo, mientras asProtocol tiene solo interfaz de protocolos. Entonces, cuando quieras llamar UITableViewCellmétodos de, tienes que usar cell variable. Cuando desee llamar al método de protocolos, use asProtocol variable.

Si está seguro de que la celda cumple con los protocolos, no tiene que usar if let ... as? ... {}. me gusta:

let cell = CellFactory.createCellForItem(special)
let asProtocol = cell as protocol<MyProtocol1,MyProtocol2>

30
2017-10-16 11:51



Desafortunadamente, Swift no es compatible con la conformidad del protocolo de nivel de objeto. Sin embargo, hay una solución alternativa algo 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
    }
}

Entonces, en cualquier lugar que necesite hacer algo que tenga UIViewController, accederá al aspecto .viewController de la estructura y cualquier cosa que necesite el aspecto del protocolo, usted 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 cualquier cosa relacionada con UIViewController, solo hace referencia a mySpecialViewController.viewController y siempre que lo necesite para realizar alguna función de protocolo, hace referencia a mySpecialViewController.protocol.

Es de esperar que Swift 4 nos permita declarar un objeto con protocolos adjuntos en el futuro. Pero por ahora, esto funciona.

¡Espero que esto ayude!


2
2017-12-19 15:53



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

Tal vez estoy equivocado, pero ¿no estás hablando de agregar la conformidad del protocolo a la UITableCellView ¿clase? El protocolo se extiende en ese caso a la clase base, y no al objeto. Ver la documentación de Apple en Declaración de adopción de protocolo con una extensión que en tu caso sería algo así 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 Swift ya mencionada, también vea el artículo de Nate Cooks Funciones genéricas para tipos incompatibles con más ejemplos.

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

¿Hay alguna otra forma más obvia que pueda estar perdiendo?

La adopción del protocolo hará justamente 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 dado no no saber algo fuera del protocolo. Pero esto puede eludirse definiendo un protocolo que tenga todos los métodos / variables / ...

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

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


1
2018-03-07 04:37



Una vez tuve una situación similar cuando traté de vincular mis conexiones interactor genéricas en Storyboards (IB no te permitirá conectar tomas de corriente a protocolos, solo instancias de objetos), lo que obtuve simplemente enmascarando la clase base ivar pública con un computador privado propiedad. Si bien esto no impide que alguien realice asignaciones ilegales per se, sí proporciona una manera conveniente de prevenir de forma segura cualquier interacción no deseada con una instancia no conforme en tiempo de ejecución. (es decir, evita llamar a métodos de delegado 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 receptor de salida (delegado a.k.a.) a través de este último (la propiedad calculada), filtrar 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, implemente o no el protocolo o incluso exista.

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


0
2017-11-05 05:51