Na continuação do tópico, examinaremos os tipos de protocolo e o código generalizado.
Os seguintes problemas serão considerados ao longo do caminho:
- implementação de polimorfismo sem herança e tipos de referência
- como os objetos do tipo protocolo são armazenados e usados
- como o envio do método funciona com eles
Tipos de protocolo
Implementação de polimorfismo sem herança e tipos de referência:
protocol Drawable { func draw() } struct Point: Drawable { var x, y: Int func draw() { ... } } struct Line: Drawable { var x1, x2, y1, y2: Int func draw() { ... } } var drawbles = [Drawable]() for d in drawbles { d.draw() }
- Indique o protocolo Drawable, que possui um método draw.
- Implementamos este protocolo para Ponto e Linha - agora você pode lidar com eles como no Drawable (chame o método draw)
Ainda temos um código polimórfico. O elemento d da matriz drawables possui uma interface, que é indicada no protocolo Drawable, mas possui implementações diferentes de seus métodos, indicadas em Line e Point.
O principal princípio (ad-hoc) do polimorfismo: "Interface comum - muitas implementações"
Despacho dinâmico sem tabela virtual
Lembre-se de que a definição da implementação correta do método ao trabalhar com classes (tipos de referência) é alcançada por meio do envio dinâmico e de uma tabela virtual. Cada tipo de classe possui uma tabela virtual e armazena implementações de seus métodos. O despacho dinâmico define a implementação do método para um tipo, examinando sua tabela virtual. Tudo isso é necessário devido à possibilidade de herança e substituição de métodos.
No caso de estruturas, a herança e a redefinição de métodos são impossíveis. Então, à primeira vista, não há necessidade de uma tabela virtual, mas como então o despacho dinâmico funcionará? Como um programa pode entender qual método será chamado em d.draw ()?
Vale ressaltar que o número de implementações desse método é igual ao número de tipos que estão em conformidade com o protocolo Drawable.
Tabela de testemunhas de protocolo
é a resposta para esta pergunta. Cada tipo que implementa um protocolo possui esta tabela. Como uma tabela virtual para classes, ela armazena implementações dos métodos exigidos pelo protocolo.
daqui em diante, a Tabela de Testemunha de Protocolo será chamada de “tabela de método de protocolo”
Ok, agora sabemos onde procurar implementações de métodos. Apenas duas perguntas permanecem:
- Como encontrar a tabela de método de protocolo apropriada para um objeto que implementou esse protocolo? Como, no nosso caso, encontrar esta tabela para o elemento d da matriz de drawables?
- Os elementos da matriz devem ter o mesmo tamanho (essa é a essência da matriz). Então, como um array desenhável pode atender a esse requisito se ele pode armazenar linhas e pontos nele, e eles têm tamanhos diferentes?
MemoryLayout.size(ofValue: Line(...))
Contêiner existente
Para resolver esses dois problemas, o Swift usa um esquema de armazenamento especial para instâncias de tipos de protocolo chamados de contêiner existencial. É assim:

São necessárias 5 palavras de máquina (no sistema x64 bits 5 * 8 = 40 bits). É dividido em três partes:
buffer de valor - espaço para a própria instância
vwt - ponteiro para a tabela de testemunhas de valor
pwt - ponteiro para a tabela de testemunhas de protocolo
Considere todas as três partes em mais detalhes:
Buffer de Conteúdo
Apenas três palavras-máquina para armazenar uma instância. Se a instância puder caber no buffer de conteúdo, ela será armazenada nele. Se a instância tiver mais de três palavras de máquina, ela não caberá no buffer e o programa será forçado a alocar memória no heap, colocar a instância lá e colocar um ponteiro para essa memória no buffer de conteúdo. Considere um exemplo:
let point: Drawable = Point(...)
O Point () ocupa 2 palavras de máquina e se encaixa perfeitamente no buffer de valor - o programa o colocará lá:

let line: Drawable = Line(...)
Line () ocupa 4 palavras de máquina e não pode caber em um buffer de valor - o programa alocará memória para ele para heap e adicionará um ponteiro a essa memória no buffer de valor:

ptr aponta para uma instância de Line () colocada na pilha:

Tabela do ciclo de vida
Assim como a tabela de método de protocolo, cada tabela que possui o protocolo possui essa tabela. Ele contém uma implementação de quatro métodos: alocar, copiar, destruir, desalocar. Esses métodos controlam todo o ciclo de vida de um objeto. Considere um exemplo:
- Ao criar um objeto (Ponto (...) como Drawable), o método de alocação de T.Zh. esse objeto. O método de alocação decidirá onde o conteúdo do objeto deve ser colocado (no buffer de valor ou na pilha) e, se for colocado na pilha, alocará a quantidade necessária de memória
- O método de cópia colocará o conteúdo do objeto no local apropriado.
- Após concluir o trabalho com o objeto, o método destruct será chamado, o que reduzirá todas as contagens de links, se houver
- Após a destruição, o método de desalocação será chamado, o que liberará a memória alocada no heap, se houver
Tabela de método de protocolo
Como descrito acima, ele contém implementações dos métodos exigidos pelo protocolo para o tipo ao qual esta tabela está vinculada.
Contêiner Existencial - Respostas
Assim, respondemos a duas perguntas colocadas:
- A tabela de método de protocolo é armazenada no contêiner existencial desse objeto e pode ser facilmente obtida dele
- Se o tipo de elemento da matriz for um protocolo, qualquer elemento dessa matriz terá um valor fixo de 5 palavras de máquina - é exatamente isso que é necessário para um contêiner Existencial. Se o conteúdo do elemento não puder ser colocado no buffer de valor, ele será colocado no heap. Se possível, todo o conteúdo será colocado no buffer de valor. De qualquer forma, concluímos que o tamanho do objeto com o tipo de protocolo é de 5 palavras-máquina (40 bits), e segue-se que todos os elementos da matriz terão o mesmo tamanho.
let line: Drawable = Line(...) MemoryLayout.size(ofValue: line)
Contêiner Existencial - Exemplo
Considere o comportamento de um contêiner existencial neste código:
func drawACopy(local: Drawable) { local.draw() } let val: Drawable = Line(...) drawACopy(val)
Um contêiner existencial pode ser representado assim:
struct ExistContDrawable { var valueBuffer: (Int, Int, Int) var vwt: ValueWitnessTable var pwt: ProtocolWitnessTable }
Pseudo código
Nos bastidores, a função drawACopy recebe ExistContDrawable:
func drawACopy(val: ExistContDrawable) { ... }
O parâmetro da função é criado manualmente: crie um contêiner, preencha seus campos a partir do argumento recebido:
func drawACopy(val: ExistContDrawable) { var local = ExistContDrawable() let vwt = val.vwt let pwt = val.pwt local.type = type local.pwt = pwt ... }
Decidimos onde o conteúdo será armazenado (no buffer ou na pilha). Chamamos vwt.allocate e vwt.copy para preencher o conteúdo local com val:
func drawACopy(val: ExistContDrawable) { ... vwt.allocateBufferAndCopy(&local, val) }
Chamamos o método draw e passamos um ponteiro para self (o método projectBuffer decidirá onde o self está localizado - no buffer ou na pilha - e retornará o ponteiro correto):
func drawACopy(val: ExistContDrawable) { ... pwt.draw(vwt.projectBuffer(&local)) }
Terminamos o trabalho com o local. Limpamos todos os links de quadril do local. A função retorna um valor - limpamos toda a memória alocada para drawACopy (quadro da pilha):
func drawACopy(val: ExistContDrawable) { ... vwt.destructAndDeallocateBuffer(&local) }
Contêiner Existencial - Finalidade
Usar um contêiner existencial requer muito trabalho - o exemplo acima confirmou isso - mas por que é necessário, qual é o objetivo? O objetivo é implementar o polimorfismo usando protocolos e os tipos que os implementam. No OOP, usamos classes abstratas e as herdamos substituindo métodos. No EPP, usamos protocolos e implementamos seus requisitos. Novamente, mesmo com protocolos, implementar o polimorfismo é um trabalho grande e que consome energia. Portanto, para evitar trabalho "desnecessário", você precisa entender quando o polimorfismo é necessário e quando não.
O polimorfismo na implementação do EPP vence porque, usando estruturas, não precisamos de contagens constantes de referências, não há herança de classe. Sim, tudo é muito parecido, as classes usam uma tabela virtual para determinar a implementação de um método, os protocolos usam o método de protocolo. As aulas são colocadas na pilha, às vezes as estruturas também podem ser colocadas lá. Mas o problema é que o maior número possível de ponteiros pode ser direcionado para a classe colocada na pilha, e a contagem de referência é necessária, e apenas um ponteiro para as estruturas colocadas na pilha e ele é armazenado em um contêiner existencial.
De fato, é importante observar que uma estrutura que é armazenada em um contêiner existencial manterá a semântica dos tipos de valor, independentemente de ser colocada na pilha ou na pilha. A Tabela de Ciclo de Vida é responsável pela preservação da semântica, pois descreve métodos que determinam a semântica.
Contêiner Existencial - Propriedades Armazenadas
Examinamos como uma variável do tipo protocolo é passada e usada por uma função. Vamos considerar como essas variáveis são armazenadas:
struct Pair { init(_ f: Drawable, _ s: Drawable) { first = f second = s } var first: Drawable var second: Drawable } var pair = Pair(Line(), Point())
Como essas duas estruturas Drawable são armazenadas dentro da estrutura Pair? Qual é o conteúdo do par? Consiste em dois contêineres existenciais - um para o primeiro e outro para o segundo. A linha não pode caber no buffer e é colocada no heap. Ponto de ajuste no buffer. Também permite que a estrutura Pair armazene objetos de tamanhos diferentes:
pair.second = Line()
Agora, o conteúdo do segundo também é colocado no heap, pois ele não se encaixava no buffer. Considere o que isso pode levar a:
let aLine = Line(...) let pair = Pair(aLine, aLine) let copy = pair
Após executar este código, o programa receberá o seguinte status de memória:

Temos quatro alocações de memória no heap, o que não é bom. Vamos tentar consertar:
- Criar uma linha de classe analógica
class LineStorage: Drawable { var x1, y1, x2, y2: Double func draw() {} }
- Usamos em par
let lineStorage = LineStorage(...) let pair = Pair(lineStorage, lineStorage) let copy = pair
Temos um posicionamento no heap e 4 ponteiros para ele:

Mas estamos lidando com comportamento referencial. Alterar copy.first afetará pair.first (o mesmo para .second), que nem sempre é o que queremos.
Armazenamento indireto e cópia na alteração (copiar na gravação)
Antes disso, foi mencionado que String é uma estrutura de cópia na gravação (armazena seu conteúdo no heap e o copia quando ele muda). Considere como você pode implementar sua estrutura, que é copiada ao alterar:
struct BetterLine: Drawable { private var storage: LineStorage init() { storage = LineStorage((0, 0), (10, 10)) } func draw() -> Double { ... } mutating func move() { if !isKnownUniquelyReferenced(&storage) { storage = LineStorage(self.storage) }
- O BetterLine armazena todas as propriedades no armazenamento, e o armazenamento é uma classe e é armazenado no heap.
- O armazenamento só pode ser alterado usando o método de movimentação. Nele, verificamos que apenas um ponteiro aponta para o armazenamento. Se houver mais indicadores, este BetterLine compartilhará o armazenamento com alguém e, para que o BetterLine se comporte completamente como uma estrutura, o armazenamento deve ser individual - fazemos uma cópia e trabalhamos com ele no futuro.
Vamos ver como isso funciona na memória:
let aLine = BetterLine() let pair = Pair(aLine, aLine) let copy = pair copy.second.x1 = 3.0
Como resultado da execução desse código, obtemos:

Em outras palavras, temos duas instâncias de Pair que compartilham o mesmo armazenamento: LineStorage. Ao alterar o armazenamento em um de seus usuários (primeiro / segundo), uma cópia separada do armazenamento para esse usuário será criada para que a alteração não afete outros. Isso resolve o problema de violação da semântica dos tipos de valor do exemplo anterior.
Tipos de protocolo - Resumo
- Valores pequenos . Se trabalharmos com objetos que ocupam pouca memória e podem ser colocados no buffer de um contêiner existencial, então:
- não haverá posicionamento na pilha
- sem contagem de referência
- polimorfismo (envio dinâmico) usando uma tabela de protocolo
- Ótimo valor. Se trabalharmos com objetos que não cabem no buffer, então:
- posicionamento de pilha
- referência contando se os objetos contêm links.
Os mecanismos de uso da reescrita para alteração e armazenamento indireto foram demonstrados e podem melhorar significativamente a situação com a contagem de referência no caso de um grande número deles.
Descobrimos que tipos de protocolo, como classes, são capazes de realizar polimorfismos. Isso acontece armazenando em um contêiner existencial e usando tabelas de protocolo - tabelas de ciclo de vida e tabelas de método de protocolo.