Programação Orientada a Protocolo, Parte 3

Artigo final sobre programação orientada a protocolo.


Nesta parte, veremos como as variáveis ​​de tipo genérico são armazenadas e copiadas e como o método de despacho funciona com elas.


Versão não compartilhada


protocol Drawable { func draw() } func drawACopy(local: Drawable) { local.draw() } let line = Line() drawACopy(line) let point = Point() drawACopy(point) 

Código muito simples. drawACopy pega um parâmetro do tipo Drawable e chama seu método de desenho - isso é tudo.


Versão generalizada


Vejamos a versão generalizada do código acima:


 func drawACopy<T: Drawable>(local: T) { local.draw() } ... 

Nada parece ter mudado. Ainda podemos chamar a função drawACopy , como sua versão drawACopy , e nada mais, mas a mais interessante, como sempre, sob o capô.
O código generalizado possui dois recursos importantes:


  1. polimorfismo estático (também conhecido como paramétrico)
  2. tipo definido e exclusivo no contexto da chamada (o tipo genérico T é definido em tempo de compilação)

Considere isso com um exemplo:


 func foo<T: Drawable>(local: T) { bar(local) } func bar<T: Drawable>(local: T) { ... } let point = Point(...) foo(point) 

A parte mais interessante começa quando chamamos a função foo . O compilador sabe exatamente o tipo da variável point - é apenas Point. Além disso, o tipo T: Drawable na função foo pode ser inferido livremente pelo compilador a partir do momento em que passamos uma variável do tipo Point conhecido para esta função: T = Point. Todos os tipos são conhecidos no momento da compilação e o compilador pode executar todas as suas maravilhosas otimizações - o mais importante é alinhar a chamada foo .


 This: ```swift let point = Point(...) foo<T = Point>(point) Becomes this: ```swift bar<T = Point>(point) 

O compilador simplesmente incorpora a chamada foo com sua implementação e exibe o tipo genérico de barra T: Drawable também. Em outras palavras, primeiro o compilador incorpora uma chamada ao método foo com o tipo T = Point, depois incorpora o resultado da incorporação anterior - o método bar com o tipo T = Point.


Implementação de métodos genéricos


 func drawACopy<T: Drawable>(local: T) { local.draw() } drawACopy(Point(...)) 

Internamente, o drawACopy Swift usa uma tabela de método de protocolo (que contém todas as implementações do método T) e uma tabela de ciclo de vida (que contém todos os métodos de ciclo de vida para a instância T). No pseudocódigo, fica assim:


 func drawACopy<T: Drawable>(local: T, pwt: T.PWT, vwt: T.VWT) {...} drawACopy(Point(...), Point.pwt, Point.vwt) 

VWT e PWT são tipos associados (tipo associado) em T - como aliases de tipo (tipealias), apenas melhores. Point.pwt e Point.vwt são propriedades estáticas.


Como em nosso exemplo, T é Point, T está bem definido, portanto, a criação de um contêiner não é necessária. Na versão drawACopy anterior do drawACopy (local: Drawable), a criação de um contêiner existencial foi realizada conforme necessário - examinamos isso na segunda parte do artigo.


Uma tabela de ciclo de vida é necessária em funções devido à criação de um argumento. Como sabemos, os argumentos no Swift são passados ​​por valores, não por links, portanto, eles devem ser copiados, e o método de cópia desse argumento pertence à tabela do ciclo de vida como esse argumento. Existem também outros métodos de ciclo de vida: alocar, destruir e desalocar.


Uma tabela de ciclo de vida é necessária em funções genéricas devido ao uso de métodos para parâmetros de código genéricos.


Generalizado ou não generalizado?


É verdade que o uso de tipos genéricos torna a execução do código mais rápida do que apenas os tipos de protocolo? A função generalizada func foo<T: Drawable>(arg: T) mais rápida que sua contraparte semelhante ao protocolo fun foo(arg: Drawable) ?


Percebemos que o código genérico fornece uma forma mais estática de polimorfismo. Ele também inclui otimizações de compilador chamadas "Especialização de código genérico". Vamos ver:


Novamente, temos o mesmo código:


 func drawACopy<T: Drawable>(local: T) { local.draw() } drawACopy(Point(...)) drawACopt(Line(...)) 

A especialização de uma função genérica cria uma cópia com tipos genéricos especializados dessa função. Por exemplo, se chamarmos drawACopy com uma variável do tipo Point, o compilador criará uma versão especializada dessa função - drawACopyOfPoint (local: Point) e obteremos:


 func drawACopyOfPoint(local: Point) { local.draw() } func drawACopyOfLine(local: Line) { local.draw() } drawACopy(Point(...)) drawACopt(Line(...)) 

O que pode ser reduzido pela otimização bruta do compilador antes disso:


 Point(...).draw() Line(...).draw() 

Todos esses truques estão disponíveis porque funções genéricas só podem ser chamadas se todos os tipos genéricos estiverem definidos - no método drawACopy tipo genérico (T) está bem definido.


Propriedades armazenadas genéricas


Considere um par simples de estrutura:


 struct Pair { let fst: Drawable let snd: Drawable } let pair = Pair(fst: Line(...), snd: Line(...)) 

Quando usamos isso dessa maneira, obtemos 2 alocações no heap (as condições exatas de memória neste cenário foram descritas na segunda parte), mas podemos evitar isso com a ajuda de um código generalizado.


A versão genérica do Pair tem a seguinte aparência:


 struct Pair<T: Drawable> { let fst: T let snd: T } 

A partir do momento em que o tipo T é definido na versão generalizada, os tipos de propriedades fst e snd mesmos e também são definidos. Como o tipo é definido, o compilador pode alocar uma quantidade especializada de memória para essas duas propriedades - fst e snd .


Em mais detalhes sobre a quantidade especializada de memória:


quando estamos trabalhando com uma versão fst de Pair , os tipos de propriedade fst e snd são Drawable. Qualquer tipo pode corresponder a Drawable, mesmo que ocupe 10 KB de memória. Ou seja, Swift não poderá tirar uma conclusão sobre o tamanho desse tipo e usará um local de memória universal, por exemplo, um contêiner existencial. Qualquer tipo pode ser armazenado neste contêiner. No caso de código genérico, o tipo é bem reconhecido, o tamanho real das propriedades também é reconhecível e o Swift pode criar um local de memória especializado. Por exemplo (versão generalizada):


 let pair = Pair(Point(...), Point(...)) 

O tipo T agora é ponto. O ponto leva N bytes de memória e, em Pair, temos dois deles. O Swift alocará 2 * N de memória e colocará o pair lá.


Portanto, com a versão generalizada do Pair, eliminamos alocações desnecessárias na pilha, porque os tipos são facilmente reconhecíveis e podem ser localizados especificamente - sem a necessidade de criar modelos de memória universal, pois tudo é conhecido.


Conclusão


1. Código genérico especializado - tipos de valor


tem a melhor velocidade de execução, pois:


  • nenhuma alocação de pilha ao copiar
  • código genérico - você escreve uma função para um tipo especializado
  • sem contagem de referência
  • envio de método estático

2. Código generalizado especializado - tipos de referência


Tem uma velocidade média de execução, pois:


  • alocações por heap ao instanciar
  • existe uma contagem de referência
  • envio de método dinâmico via tabela virtual

3. Código generalizado não especializado - valores pequenos


  • sem alocação de heap - o valor é colocado no buffer de valor do contêiner existencial
  • nenhuma contagem de referência (já que nada é colocado na pilha)
  • método dinâmico enviando via tabela de método de protocolo

4. Código generalizado não especializado - grandes valores


  • posicionamento na pilha - o valor é colocado no buffer de valores
  • existe uma contagem de referência
  • expedição dinâmica via tabela de método de protocolo

Esse material não significa que as classes são ruins, as estruturas são boas e as estruturas em combinação com o código generalizado são as melhores. Queremos dizer que, como programador, você tem a responsabilidade de escolher uma ferramenta para suas tarefas. As aulas são realmente boas quando você precisa manter valores grandes e que existe uma semântica de links. As estruturas são melhores para valores pequenos e quando você precisar da semântica deles. Os protocolos são mais adequados para código e estruturas genéricos, e assim por diante. Todas as ferramentas são específicas para a tarefa que você está resolvendo e têm lados positivos e negativos.


E também não pague pelo dinamismo quando você não precisar dele . Encontre a abstração correta com os menos requisitos de tempo de execução.


  • tipos estruturais - semântica de significados
  • tipos de classe - identidade
  • código generalizado - polimorfismo estático
  • tipos de protocolo - polimorfismo dinâmico

Use armazenamento indireto para trabalhar com grandes valores.


E não se esqueça - é sua responsabilidade escolher a ferramenta certa.
Obrigado por sua atenção a este tópico. Esperamos que esses artigos tenham ajudado e tenham sido interessantes.


Boa sorte

Source: https://habr.com/ru/post/pt474830/


All Articles