No Swift, como posso declarar uma variável de um tipo específico que está em conformidade com um ou mais protocolos?

96

Em Swift, posso definir explicitamente o tipo de uma variável, declarando-a da seguinte maneira:

var object: TYPE_NAME

Se quisermos dar um passo adiante e declarar uma variável em conformidade com vários protocolos, podemos usar o protocoldeclarativo:

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

E se eu quiser declarar um objeto que está em conformidade com um ou mais protocolos e também é de um tipo de classe base específico? O equivalente em Objective-C seria assim:

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

Em Swift, eu esperaria que fosse assim:

var object: TYPE_NAME,ProtocolOne//etc

Isso nos dá a flexibilidade de sermos capazes de lidar com a implementação do tipo de base, bem como a interface adicionada definida no protocolo.

Existe outra maneira mais óbvia que eu possa estar perdendo?

Exemplo

Por exemplo, digamos que eu tenha uma UITableViewCellfábrica responsável por devolver células em conformidade com um protocolo. Podemos facilmente configurar uma função genérica que retorna células em conformidade com um protocolo:

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

mais tarde, quero retirar da fila essas células, aproveitando o tipo e o protocolo

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

Isso retorna um erro porque uma célula de exibição de tabela não está em conformidade com o protocolo ...

Gostaria de poder especificar que a célula é um UITableViewCelle está em conformidade com o MyProtocolna declaração da variável?

Justificação

Se você estiver familiarizado com o Factory Pattern, isso faria sentido no contexto de ser capaz de retornar objetos de uma classe específica que implementa uma determinada interface.

Assim como no meu exemplo, às vezes gostamos de definir interfaces que fazem sentido quando aplicadas a um objeto específico. Meu exemplo de célula de exibição de tabela é uma dessas justificativas.

Embora o tipo fornecido não esteja exatamente em conformidade com a interface mencionada, o objeto que a fábrica retorna está e, por isso, gostaria de ter flexibilidade para interagir com o tipo de classe base e a interface de protocolo declarada

Daniel Galasko
fonte
Desculpe, mas qual é o objetivo disso rapidamente. Os tipos já sabem quais protocolos estão em conformidade. O que não basta usar o tipo?
Kirsteins de
1
@Kirsteins Não, a menos que o tipo seja retornado de uma fábrica e, portanto, seja um tipo genérico com uma classe base comum
Daniel Galasko
Por favor, dê um exemplo, se possível.
Kirsteins de
NSSomething<ABCProtocolOne,ABCProtocolTwo> * object = ...;. Este objeto parece totalmente inútil, pois NSSomethingjá sabe a que se conforma. Se ele não estiver em conformidade com um dos protocolos em, <>você terá unrecognised selector ...travamentos. Isso não fornece nenhuma segurança de tipo.
Kirsteins de
@Kirsteins Por favor, veja meu exemplo novamente, é usado quando você sabe que o objeto que sua fábrica vende é de uma classe base particular em conformidade com um protocolo especificado
Daniel Galasko

Respostas:

72

No Swift 4 agora é possível declarar uma variável que é uma subclasse de um tipo e implementa um ou mais protocolos ao mesmo tempo.

var myVariable: MyClass & MyProtocol & MySecondProtocol

Para fazer uma variável opcional:

var myVariable: (MyClass & MyProtocol & MySecondProtocol)?

ou como parâmetro de um método:

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

A Apple anunciou isso na WWDC 2017 na Sessão 402: O que há de novo no Swift

Em segundo lugar, quero falar sobre redação de classes e protocolos. Então, aqui eu apresentei este protocolo instável para um elemento de IU que pode dar um pequeno efeito de vibração para chamar a atenção para si mesmo. E fui em frente e estendi algumas das classes UIKit para realmente fornecer essa funcionalidade de shake. E agora quero escrever algo que pareça simples. Eu só quero escrever uma função que pega um monte de controles que são abaláveis ​​e abana aqueles que estão habilitados para chamar a atenção para eles. Que tipo posso escrever aqui nesta matriz? Na verdade, é frustrante e complicado. Então, eu poderia tentar usar um controle de IU. Mas nem todos os controles da IU são abaláveis ​​neste jogo. Eu poderia tentar shakable, mas nem todos os shakables são controles de IU. E, na verdade, não há uma boa maneira de representar isso no Swift 3.O Swift 4 introduz a noção de compor uma classe com qualquer número de protocolos.

Philipp Otto
fonte
3
Apenas adicionando um link para a proposta de evolução rápida github.com/apple/swift-evolution/blob/master/proposals/…
Daniel Galasko
Obrigado, Philipp!
Omar Albeik,
e se precisar de uma variável opcional desse tipo?
Vyachaslav Gerchicov de
2
@VyachaslavGerchicov: Você pode colocar parênteses em torno dele e, em seguida, o ponto de interrogação como este: var myVariable: (MyClass & MyProtocol & MySecondProtocol)?
Philipp Otto
30

Você não pode declarar variável como

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

nem declarar tipo de retorno de função como

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

Você pode declarar como um parâmetro de função como este, mas é basicamente um 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 agora, tudo que você pode fazer é:

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

Com isso, cellé tecnicamente idêntico a asProtocol.

Mas, quanto ao compilador, cellpossui interface de UITableViewCellapenas, enquanto asProtocolpossui apenas interface de protocolos. Então, quando você quiser chamar UITableViewCellos métodos de, você deve usar a cellvariável. Quando você quiser chamar o método de protocolos, use a asProtocolvariável.

Se você tiver certeza de que a célula está em conformidade com os protocolos que você não precisa usar if let ... as? ... {}. gostar:

let cell = CellFactory.createCellForItem(special)
let asProtocol = cell as protocol<MyProtocol1,MyProtocol2>
Rintaro
fonte
Uma vez que a fábrica especifica os tipos de retorno, eu não preciso tecnicamente para executar a conversão opcional? Eu poderia apenas contar com a digitação implícita de swifts para realizar a digitação em que declaro explicitamente os protocolos?
Daniel Galasko,
Não entendo o que você quer dizer, desculpe por meu péssimo conhecimento do inglês. Se você está falando sobre -> UITableViewCell<MyProtocol>, isso é inválido, porque UITableViewCellnão é um tipo genérico. Acho que nem mesmo compila.
rintaro
Não estou me referindo à sua implementação genérica, mas sim ao seu exemplo de ilustração de implementação. onde você diz let asProtocol = ...
Daniel Galasko
ou, eu poderia simplesmente fazer: var cell: protocol <ProtocolOne, ProtocolTwo> = someObject as UITableViewCell e obter o benefício de ambos em uma variável
Daniel Galasko
2
Acho que não. Mesmo que você pudesse fazer assim, celltem apenas métodos de protocolos (para compilador).
rintaro
2

Infelizmente, o Swift não oferece suporte à conformidade de protocolo em nível de objeto. No entanto, há uma solução um tanto estranha que pode servir aos seus propósitos.

struct VCWithSomeProtocol {
    let protocol: SomeProtocol
    let viewController: UIViewController

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

Então, em qualquer lugar que você precise fazer qualquer coisa que UIViewController tenha, você acessará o aspecto .viewController da estrutura e qualquer coisa que você precisar do aspecto de protocolo, você fará referência ao .protocol.

Por exemplo:

class SomeClass {
   let mySpecialViewController: VCWithSomeProtocol

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

Agora, sempre que você precisar de mySpecialViewController para fazer qualquer coisa relacionada ao UIViewController, basta fazer referência a mySpecialViewController.viewController e sempre que precisar fazer alguma função de protocolo, consulte mySpecialViewController.protocol.

Esperançosamente, o Swift 4 nos permitirá declarar um objeto com protocolos anexados a ele no futuro. Mas, por enquanto, isso funciona.

Espero que isto ajude!

Michael Curtis
fonte
1

EDIT: Eu estava enganado , mas se alguém ler este mal-entendido como eu, deixo esta resposta por aí. O OP perguntou sobre a verificação da conformidade do protocolo do objeto de uma determinada subclasse, e essa é outra história, como mostra a resposta aceita. Esta resposta fala sobre a conformidade do protocolo para a classe base.

Talvez eu esteja enganado, mas você não está falando sobre adicionar conformidade de protocolo à UITableCellViewclasse? O protocolo é, nesse caso, estendido à classe base, e não ao objeto. Consulte a documentação da Apple sobre como declarar a adoção de protocolo com uma extensão que, no seu caso, seria algo como:

extension UITableCellView : ProtocolOne {}

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

Além da documentação do Swift já referenciada, consulte também o artigo de Nate Cooks Funções genéricas para tipos incompatíveis com outros exemplos.

Isso nos dá a flexibilidade de sermos capazes de lidar com a implementação do tipo de base, bem como a interface adicionada definida no protocolo.

Existe outra maneira mais óbvia que eu possa estar perdendo?

A adoção de protocolo fará exatamente isso, fará um objeto aderir ao protocolo fornecido. No entanto, esteja ciente do lado adverso, que uma variável de um determinado tipo de protocolo não conhece nada fora do protocolo. Mas isso pode ser contornado definindo um protocolo que tem todos os métodos / variáveis ​​necessários / ...

Embora o tipo fornecido não esteja exatamente em conformidade com a interface mencionada, o objeto que a fábrica retorna está e, por isso, gostaria de ter flexibilidade para interagir com o tipo de classe base e a interface de protocolo declarada

Se você quiser que um método genérico, variável esteja em conformidade com um protocolo e tipos de classe base, você pode estar sem sorte. Mas parece que você precisa definir o protocolo amplo o suficiente para ter os métodos de conformidade necessários e, ao mesmo tempo, estreito o suficiente para ter a opção de adotá-lo para classes básicas sem muito trabalho (ou seja, apenas declarar que uma classe está em conformidade com o protocolo).

Holroy
fonte
1
Não é disso que estou falando, mas obrigado :) Eu queria ser capaz de fazer interface com um objeto por meio de sua classe e de um protocolo específico. Assim como em obj-c posso fazer NSObject <MyProtocol> obj = ... Desnecessário dizer que isso não pode ser feito rapidamente, você tem que converter o objeto para seu protocolo
Daniel Galasko
0

Certa vez, tive uma situação semelhante ao tentar vincular minhas conexões de interator genérico em Storyboards (o IB não permite que você conecte saídas a protocolos, apenas instâncias de objeto), que contornei simplesmente mascarando a classe base pública ivar com um privado computado propriedade. Embora isso não impeça alguém de fazer atribuições ilegais por si só, ele fornece uma maneira conveniente de evitar com segurança qualquer interação indesejada com uma instância não conforme em tempo de execução. (ou seja, evite chamar métodos delegados para objetos que não estejam em conformidade com o protocolo.)

Exemplo:

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

O "outputReceiver" é declarado opcional, assim como o "protocolOutputReceiver" privado. Sempre acessando o outputReceiver (também conhecido como delegado) por meio do último (a propriedade computada), eu efetivamente filtro quaisquer objetos que não estejam em conformidade com o protocolo. Agora posso simplesmente usar o encadeamento opcional para chamar com segurança o objeto delegado, quer ele implemente o protocolo ou exista ou não.

Para aplicar isso à sua situação, você pode fazer com que o ivar público seja do tipo "YourBaseClass?" (ao contrário de AnyObject) e use a propriedade computada privada para impor a conformidade do protocolo. FWIW.

rapidinho
fonte