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:
- polimorfismo estático (também conhecido como paramétrico)
- 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