Hoje continuamos a série de publicações sobre o tema do desenvolvimento móvel para iOS. E se a última vez foi sobre o que você precisa e não precisa perguntar nas entrevistas, neste artigo abordaremos o assunto dos protocolos, o que é importante no Swift. Será sobre como os protocolos são organizados, como eles diferem entre si e como eles se combinam com as interfaces Objective-C.

Como dissemos anteriormente, o novo idioma da Apple continua a evoluir e a maioria de seus parâmetros e recursos são claramente indicados na documentação. Mas quem lê a documentação quando o código precisa ser escrito aqui e agora? Então, vamos falar sobre os principais recursos dos protocolos Swift em nosso post.
Para começar, deve-se notar que os protocolos da Apple são um termo alternativo para o conceito de "Interface", usado em outras linguagens de programação. No Swift, protocolos são usados para indicar padrões de certas estruturas (o chamado blueprint) que podem ser trabalhadas em um nível abstrato. Em palavras simples, o protocolo define vários métodos e variáveis que um determinado tipo deve herdar sem falhas.
Mais adiante neste artigo, os momentos serão gradualmente revelados da seguinte forma: dos simples e frequentemente usados aos mais complexos. Em princípio, nas entrevistas, você pode fazer perguntas nesta ordem, pois elas determinam o nível de competência do candidato - do nível de juniores ao nível de seniores.
Quais são os protocolos necessários no Swift?
Os desenvolvedores móveis geralmente ficam sem usar protocolos, mas perdem a capacidade de trabalhar com algumas entidades de maneira abstrata. Se destacarmos os principais recursos dos protocolos no Swift, obtemos os 7 pontos a seguir:
- Protocolos fornecem herança múltipla
- Protocolos não podem armazenar estado
- Os protocolos podem ser herdados por outros protocolos.
- Os protocolos podem ser aplicados a estruturas (struct), classes (classe) e enumerações (enum), definindo a funcionalidade do tipo
- Protocolos genéricos permitem especificar dependências complexas entre tipos e protocolos durante sua herança
- Os protocolos não definem referências de variáveis "fortes" ou "fracas"
- Nas extensões dos protocolos, podem ser descritas implementações específicas de métodos e valores computados
- Protocolos de classe permitem que apenas classes herdem
Como você sabe, todos os tipos simples (string, int) no Swift são estruturas. Na biblioteca padrão Swift, isso, por exemplo, se parece com isso:
public struct Int: FixedWidthInteger, SignedInteger {
Ao mesmo tempo, tipos de coleção (coleção), ou seja, matriz, conjunto, dicionário, também podem ser compactados no protocolo, porque também são estruturas. Por exemplo, um dicionário é definido da seguinte forma
public struct Dictionary<Key, Value> where Key: Hashable {
Geralmente na programação orientada a objetos, o conceito de classes é usado, e todo mundo conhece o mecanismo para herdar métodos e variáveis da classe pai da classe descendente. Ao mesmo tempo, ninguém o proíbe de conter métodos e variáveis adicionais.
No caso de protocolos, você pode criar uma hierarquia de relacionamentos muito mais interessante. Para descrever a próxima aula, você pode usar vários protocolos ao mesmo tempo, o que permite criar designs bastante complexos que satisfarão muitas condições ao mesmo tempo. Por outro lado, as limitações de diferentes protocolos possibilitam formar algum objeto destinado apenas a aplicações restritas e contendo um certo número de funções.
A implementação do protocolo no Swift é bastante simples. A sintaxe implica um nome, vários métodos e parâmetros (variáveis) que ela conterá.
protocol Employee { func work() var hours: Int { get } }
Além disso, no Swift, os protocolos podem conter não apenas os nomes dos métodos, mas também sua implementação. O código do método no protocolo é adicionado através de extensões. Na documentação, você pode encontrar muitas menções de extensões, mas com relação aos protocolos nas extensões, é possível colocar o nome da função e o corpo da função.
extension Employee { func work() { print ("do my job") } }
Você pode fazer o mesmo com variáveis.
extension Employee { var hours: Int { return 8 } }
Se usarmos o objeto associado ao protocolo em algum lugar, podemos definir uma variável com um valor fixo ou transmitido. De fato, uma variável é uma função pequena sem parâmetros de entrada ... ou com a possibilidade de atribuir diretamente um parâmetro.
A extensão do protocolo no Swift permite implementar o corpo da variável e, de fato, será um valor calculado - um parâmetro calculado com as funções get e set. Ou seja, essa variável não armazenará nenhum valor, mas desempenhará o papel de uma função ou funções, ou desempenhará o papel de um proxy para alguma outra variável.
Ou, se pegarmos alguma classe ou estrutura e implementarmos o protocolo, poderemos usar a variável usual:
class Programmer { var hours: Int = 24 } extension Programmer: Employee { }
Vale ressaltar que as variáveis na definição do protocolo não podem ser fracas. (fraco é uma implementação de variação).Existem exemplos mais interessantes: você pode implementar a extensão da matriz e adicionar uma função relacionada ao tipo de dados da matriz. Por exemplo, se a matriz contiver valores inteiros ou o formato eqüitativo (adequado para comparação), a função poderá, por exemplo, comparar todos os valores das células da matriz.
extension Array where Element: Equatable { var areAllElementsEqualToEachOther: Bool { if isEmpty { return false } var previousElement = self[0] for (index, element) in self.enumerated() where index > 0 { if element != previousElement { return false } previousElement = element } return true } } [1,1,1,1].areAllElementsEqualToEachOther
Uma pequena observação. Variáveis e funções nos protocolos podem ser estáticas.
Usando @
objc
A principal coisa que você precisa saber sobre esse assunto é que os protocolos
@
objc Swift são visíveis no código Objective-C. A rigor, para esta “palavra mágica”
@
objc existe. Mas tudo o resto permanece inalterado
@objc protocol Typable { @objc optional func test() func type() } extension Typable { func test() { print("Extension test") } func type() { print("Extension type") } } class Typewriter: Typable { func test() { print("test") } func type() { print("type") } }
Protocolos desse tipo só podem ser herdados por classes. Para listagens e estruturas, isso não pode ser feito.
Essa é a única maneira.
@objc protocol Dummy { } class DummyClass: Dummy { }
Vale ressaltar que, neste caso, torna-se possível definir funções opcionais (@obj func opcional), que, se desejado, podem não ser implementadas, como para a função test () no exemplo anterior. Mas funções condicionais opcionalmente também podem ser implementadas expandindo o protocolo com implementação vazia.
protocol Dummy { func ohPlease() } extension Dummy { func ohPlease() { } }
Herança de tipo
Ao criar uma classe, estrutura ou enumeração, podemos denotar a herança de um determinado protocolo - nesse caso, os parâmetros herdados funcionarão para nossa classe e para todas as outras classes que herdam essa classe, mesmo que não tenhamos acesso a eles.
A propósito, neste contexto, um problema muito interessante aparece. Digamos que temos um protocolo. Há alguma aula. E a classe implementa o protocolo e possui uma função work (). O que acontece se tivermos uma extensão do protocolo, que também possui um método work (). Qual será chamado quando o método for chamado?
protocol Person { func work() } extension Person { func work() { print("Person") } } class Employee { } extension Employee: Person { func work() { print("Employee") } }
O método de classe será lançado - esses são os recursos dos métodos de despacho no Swift. E esta resposta é dada por muitos candidatos. Mas na questão de como garantir que o código não tenha um método de classe, mas um método de protocolo, apenas alguns sabem a resposta. No entanto, também existe uma solução para esta tarefa - ela envolve remover a função da definição de protocolo e chamar o método da seguinte maneira:
protocol Person { // func work() // } extension Person { func work() { print("Person") } } class Employee { } extension Employee: Person { func work() { print("Employee") } } let person: Person = Employee() person.work() //output: Person
Protocolos genéricos
O Swift também possui protocolos genéricos com tipos associados que permitem definir variáveis de tipo. Esse protocolo pode receber condições adicionais impostas aos tipos associativos. Vários desses protocolos permitem construir estruturas complexas necessárias para a formação da arquitetura do aplicativo.
No entanto, você não pode implementar uma variável como um protocolo genérico. Só pode ser herdado. Essas construções são usadas para criar dependências nas classes. Ou seja, podemos descrever algumas classes genéricas abstratas para determinar os tipos usados nela.
protocol Printer { associatedtype PrintableClass: Hashable func printSome(printable: PrintableClass) } extension Printer { func printSome(printable: PrintableClass) { print(printable.hashValue) } } class TheIntPrinter: Printer { typealias PrintableClass = Int } let intPrinter = TheIntPrinter() intPrinter.printSome(printable: 0) let intPrinterError: Printer = TheIntPrinter()
Deve-se lembrar que protocolos genéricos têm um alto nível de abstração. Portanto, nos próprios aplicativos, eles podem ser redundantes. Mas, ao mesmo tempo, protocolos genéricos são usados ao programar bibliotecas.
Protocolos de classe
Swift também possui protocolos vinculados à classe. Dois tipos de sintaxe são usados para descrevê-los.
protocol Employee: AnyObject { }
Ou
protocol Employee: class { }
De acordo com os desenvolvedores da linguagem, o uso dessas sintaxes é equivalente, mas a palavra-chave class é usada apenas nesse local, diferente do AnyObject, que é um protocolo.
Enquanto isso, como vemos nas entrevistas, as pessoas geralmente não conseguem explicar o que é um protocolo de classe e por que é necessário. Sua essência está no fato de termos a oportunidade de usar algum objeto, que seria um protocolo e, ao mesmo tempo, funcionaria como um tipo de referência. Um exemplo:
protocol Handler: class {} class Presenter: Handler { weak var renderer: Renderer? } protocol Renderer {} class View: Renderer { }
O que é o sal?
O IOS usa o gerenciamento de memória usando o método de contagem de referência automática, o que implica a presença de links fortes e fracos. E, em alguns casos, você deve considerar quais variáveis - fortes (fortes) ou fracas (fracas) - são usadas nas classes.
O problema é que, ao usar algum protocolo como um tipo, ao descrever uma variável (que é um link forte), um ciclo de retenção pode ocorrer, levando a vazamentos de memória, porque os objetos serão mantidos em todos os lugares por links fortes. Além disso, podem surgir problemas se você ainda decidir escrever o código de acordo com os princípios do SOLID.
protocol Handler {} class Presenter: Handler { var renderer: Renderer? } protocol Renderer {} class View: Renderer { var handler: Handler? }
Para evitar tais situações, o Swift usa protocolos de classe que permitem definir inicialmente variáveis "fracas". O protocolo de classe permite manter um objeto em uma referência fraca. Um exemplo em que isso geralmente vale a pena considerar é chamado de delegado.
protocol TableDelegate: class {} class Table { weak var tableDelegate: TableDelegate? }
Outro exemplo em que protocolos de classe devem ser usados é uma indicação explícita de que o objeto é passado por referência.
Herança e despacho de vários métodos
Conforme declarado no início do artigo, os protocolos podem ser herdados várias vezes. Isto é,
protocol Pet { func waitingForItsOwner() } protocol Sleeper { func sleepOnAChair() } class Kitty: Pet, Sleeper { func eat() { print("yammy") } func waitingForItsOwner() { print("looking at the door") } func sleepOnAChair() { print("dreams") } }
Isso é útil, mas que armadilhas estão escondidas aqui? O problema é que as dificuldades, pelo menos à primeira vista, surgem devido ao envio de métodos (envio de método). Em palavras simples, pode não estar claro qual método será chamado - o pai ou o tipo atual.
Logo acima, já abordamos o tópico de como o código funciona; ele chama o método de classe. Ou seja, como esperado.
protocol Pet { func waitingForItsOwner() } extension Pet { func waitingForItsOwner() { print("Pet is looking at the door") } } class Kitty: Pet { func waitingForItsOwner() { print("Kitty is looking at the door") } } let kitty: Pet = Kitty() kitty.waitingForItsOwner()
Mas se você tentar remover a assinatura do método da definição de protocolo, a "mágica" ocorrerá. De fato, esta é uma pergunta da entrevista: “Como fazer uma função ser chamada de um protocolo?”
protocol Pet { } extension Pet { func waitingForItsOwner() { print("Pet is looking at the door") } } class Kitty: Pet { func waitingForItsOwner() { print("Kitty is looking at the door") } } let kitty: Pet = Kitty() kitty.waitingForItsOwner()
Mas se você usar a variável não como um protocolo, mas como uma classe, tudo ficará bem.
protocol Pet { } extension Pet { func waitingForItsOwner() { print("Pet is looking at the door") } } class Kitty: Pet { func waitingForItsOwner() { print("Kitty is looking at the door") } } let kitty = Kitty() kitty.waitingForItsOwner()
É tudo sobre métodos de envio estático ao estender o protocolo. E isso deve ser levado em consideração. E aqui está a herança múltipla? Mas com isso: se você usar dois protocolos com funções implementadas, esse código não funcionará. Para que a função seja executada, você precisará converter explicitamente no protocolo desejado. Esse é o eco da herança múltipla do C ++.
protocol Pet { func waitingForItsOwner() } extension Pet { func yawn() { print ("Pet yawns") } } protocol Sleeper { func sleepOnAChair() } extension Sleeper { func yawn() { print ("Sleeper yawns") } } class Kitty: Pet, Sleeper { func eat() { print("yammy") } func waitingForItsOwner() { print("looking at the door") } func sleepOnAChair() { print("dreams") } } let kitty = Kitty() kitty.yawn()
Uma história semelhante será se você herdar um protocolo de outro, onde há funções que são implementadas em extensões. O compilador não o deixará construir.
protocol Pet { func waitingForItsOwner() } extension Pet { func yawn() { print ("Pet yawns") } } protocol Cat { func walk() } extension Cat { func yawn() { print ("Cat yawns") } } class Kitty:Cat { func eat() { print("yammy") } func waitingForItsOwner() { print("looking at the door") } func sleepOnAChair() { print("dreams") } } let kitty = Kitty()
Os dois últimos exemplos mostram que não vale a pena substituir completamente os protocolos por classes. Você pode ficar confuso no agendamento estático.
Genéricos e protocolos
Podemos dizer que esta é uma pergunta com um asterisco, que não precisa ser feita. Mas os codificadores adoram construções super-abstratas e, é claro, algumas classes genéricas desnecessárias devem estar no projeto (onde sem ela). Mas um programador não seria um programador se não quisesse agrupar tudo em mais uma abstração. E Swift, sendo uma linguagem jovem, mas com desenvolvimento dinâmico, oferece essa oportunidade, mas de maneira limitada. (Sim, não se trata de telefones celulares).
Primeiramente, um teste completo para possível herança é apenas no Swift 4.2, ou seja, somente no outono será possível usá-lo normalmente em projetos. No Swift 4.1, aparece uma mensagem de que a oportunidade ainda não foi implementada.
protocol Property { } protocol PropertyConnection { } class SomeProperty { } extension SomeProperty: Property { } extension SomeProperty: PropertyConnection { } protocol ViewConfigurator { } protocol Connection { } class Configurator<T> where T: Property { var property: T init(property: T) { self.property = property } } extension Configurator: ViewConfigurator { } extension Configurator: Connection where T: PropertyConnection { } [Configurator(property: SomeProperty()) as ViewConfigurator] .forEach { configurator in if let connection = configurator as? Connection { print(connection) } }
Para o Swift 4.1, o seguinte é exibido:
warning: Swift runtime does not yet support dynamically querying conditional conformance ('__lldb_expr_1.Configurator<__lldb_expr_1.SomeProperty>': '__lldb_expr_1.Connection')
Enquanto no Swift 4.2 tudo funciona como esperado:
__lldb_expr_5.Configurator<__lldb_expr_5.SomeProperty> connection
Também é importante notar que você pode herdar o protocolo com apenas um tipo de relacionamento. Se houver dois tipos de links, a herança será proibida no nível do compilador. Uma explicação detalhada do que é e do que não é possível é mostrada
aqui .
protocol ObjectConfigurator { } protocol Property { } class FirstProperty: Property { } class SecondProperty: Property { } class Configurator<T> where T: Property { var properties: T init(properties: T) { self.properties = properties } } extension Configurator: ObjectConfigurator where T == FirstProperty { }
Mas, apesar dessas dificuldades, trabalhar com conexões em genéricos é bastante conveniente.
Resumir
Os protocolos foram fornecidos no Swift em sua forma atual para tornar o desenvolvimento mais estrutural e fornecer modelos de herança mais avançados do que no mesmo Objective-C. Portanto, estamos confiantes de que o uso de protocolos é uma medida justificável e não se esqueça de perguntar aos candidatos aos desenvolvedores o que eles sabem sobre esses elementos da linguagem Swift. Nos posts a seguir, abordaremos os métodos de envio.