O código genérico permite escrever funções e tipos flexíveis e reutilizáveis que podem funcionar com qualquer tipo, sujeito aos requisitos que você definir. Você pode escrever um código que evite duplicação e expresse sua intenção de maneira clara e abstrata. - Documentos rápidos
Todo mundo que escrevia no Swift usava genéricos. Array
, Dictionary
, Set
- as opções mais básicas para usar genéricos da biblioteca padrão. Como eles são representados por dentro? Vamos ver como esse recurso fundamental da linguagem é implementado pelos engenheiros da Apple.
Os parâmetros genéricos podem ser limitados por protocolos ou não, embora, basicamente, os genéricos sejam usados em conjunto com protocolos que descrevem o que exatamente pode ser feito com parâmetros de método ou campos de tipo.
Para implementar genéricos, o Swift usa duas abordagens:
- Modo de tempo de execução - o código genérico é um invólucro (Boxe).
- Compiletime-way - o código genérico é convertido em um tipo específico de código para otimização (Especialização).
Boxe
Considere um método simples com um parâmetro genérico de protocolo ilimitado:
func test<T>(value: T) -> T { let copy = value print(copy) return copy }
O compilador rápido cria um único bloco de código que será chamado para trabalhar com qualquer <T>
. Ou seja, independentemente de escrevermos test(value: 1)
ou test(value: "Hello")
, o mesmo código será chamado e informações adicionais sobre o tipo <T>
contém todas as informações necessárias serão transferidas para o método .
Pouco pode ser feito com esses parâmetros de protocolo ilimitados, mas já para implementar esse método, você precisa saber como copiar um parâmetro, precisa saber o seu tamanho para alocar memória para ele em tempo de execução, precisa saber como destruí-lo quando o parâmetro sair do campo visibilidade. A Value Witness Table
( VWT
) é usada para armazenar essas informações. VWT
é criado no estágio de compilação para todos os tipos e o compilador garante que, em tempo de execução, exista exatamente esse layout do objeto. Deixe-me lembrá-lo de que as estruturas no Swift são passadas por valor e as classes por referência; portanto, coisas diferentes serão feitas para let copy = value
com T == MyClass
e T == MyStruct
.
Ou seja, chamar o método de test
com a aprovação da estrutura declarada eventualmente se parecerá com isso:
As coisas ficam um pouco mais complicadas quando o MyStruct
uma estrutura genérica e assume o formato MyStruct<T>
. Dependendo do <T>
dentro do MyStruct
, os metadados e o VWT
serão diferentes para os tipos MyStruct<Int>
e MyStruct<Bool>
. Esses são dois tipos diferentes em tempo de execução. Mas a criação de metadados para todas as combinações possíveis de MyStruct
e T
extremamente ineficiente, então o Swift segue o caminho contrário e, nesses casos, constrói metadados em tempo de execução em movimento. O compilador cria um padrão de metadados para a estrutura genérica, que pode ser combinada com um tipo específico e, como resultado, receber informações completas do tipo em tempo de execução com o VWT
correto.
Quando combinamos informações, obtemos metadados com os quais podemos trabalhar (copiar, mover, destruir).
Ainda é um pouco mais complicado quando restrições de protocolo são adicionadas aos genéricos. Por exemplo, restringimos <T>
protocolo Equatable
. Que seja um método muito simples que compara os dois argumentos passados. O resultado é apenas um invólucro sobre o método de comparação.
func isEquals<T: Equatable>(first: T, second: T) -> Bool { return first == second }
Para que o programa funcione corretamente, você deve ter um ponteiro para o método de comparação static func ==(lhs:T, rhs:T)
. Como conseguir isso? Obviamente, a transmissão VWT
não VWT
suficiente, não contém essas informações. Para resolver esse problema, existe uma Protocol Witness Table
ou PWT
. Esse VWT
é semelhante ao VWT
e é criado no estágio de compilação de protocolos e descreve esses protocolos.
isEquals(first: 1, second: 2)
- Dois argumentos passados
- Passe metadados para
Int
para poder copiar / mover / destruir objetos - Passamos as informações que
Int
implementa Equatable
.
Se a restrição exigisse a implementação de outro protocolo, por exemplo, T: Equatable & MyProtocol
, as informações sobre o MyProtocol
seriam adicionadas com o seguinte parâmetro:
isEquals(..., intIsEquatable: Equatable.witnessTable, intIsMyProtocol: MyProtocol.witnessTable)
O uso de wrappers para implementar genéricos permite implementar de forma flexível todos os recursos necessários, mas possui uma sobrecarga que pode ser otimizada.
Especialização genérica
Para eliminar a necessidade desnecessária de obter informações durante a execução do programa, foi utilizada a chamada abordagem de especialização genérica. Permite substituir um wrapper genérico por um tipo específico por uma implementação específica. Por exemplo, para duas chamadas para isEquals(first: 1, second: 2)
e isEquals(first: "Hello", second: "world")
, além da implementação principal do "wrapper", duas versões adicionais completamente diferentes do método para Int
e para String
.
Código fonte
Primeiro, crie um arquivo generic.swift e escreva uma pequena função genérica que consideraremos.
func isEquals<T: Equatable>(first: T, second: T) -> Bool { return first == second } isEquals(first: 10, second: 11)
Agora você precisa entender o que eventualmente se transforma em um compilador.
Isso pode ser visto claramente compilando nosso arquivo .swift no Swift Intermediate Language ou SIL
.
Um pouco sobre o SIL e o processo de compilação
SIL
é o resultado de um dos vários estágios da compilação rápida.
O código-fonte .swift é passado para o Lexer, que cria uma árvore de sintaxe abstrata ( AST
) do idioma, com base em que verificação de tipo e análise semântica do código são realizadas. O SilGen converte AST
para SIL
, chamado raw SIL
, com base no qual o código é otimizado e um canonical SIL
otimizado canonical SIL
obtido, que é passado para o IRGen
para conversão em IR
- um formato especial que o LLVM
entende, que será convertido em , .
` .o , .
, .
SIL`.
E novamente para os genéricos
Crie um arquivo SIL
partir do nosso código-fonte.
swiftc generic.swift -O -emit-sil -o generic-sil.s
Temos um novo arquivo com a extensão *.s
. Olhando para dentro, veremos um código muito menos legível que o original, mas ainda relativamente claro.
Encontre a linha com o comentário // isEquals<A>(first:second:)
. Este é o começo da descrição do nosso método. Ele termina com um comentário // end sil function '$s4main8isEquals5first6secondSbx_xtSQRzlF'
. Seu nome pode ser um pouco diferente. Vamos analisar um pouco a descrição do método.
%0
e %1
na linha 21 são o first
e o second
parâmetros, respectivamente- Na linha 24, obtemos informações de tipo e passamos para
%4
- Na linha 25, obtemos um ponteiro para um método de comparação a partir de informações de tipo
- na linha 26 Chamamos o método por ponteiro, passando os parâmetros e as informações de tipo
- Na linha 27, damos o resultado.
Como resultado, vemos: para executar as ações necessárias na implementação do método genérico, precisamos obter informações da descrição do tipo <T>
durante a execução do programa.
Prosseguimos diretamente para a especialização.
No arquivo SIL
compilado, imediatamente após a declaração do método geral isEquals
, isEquals
a declaração do especialista para o tipo Int
.
Na linha 39, em vez de obter o método em tempo de execução a partir das informações de tipo, o método para comparar números inteiros "cmp_eq_Int64"
chamado imediatamente.
Para que o método seja "especializado", a otimização deve estar ativada . Você também precisa saber que
O otimizador só pode executar especialização se a definição da declaração genérica estiver visível no Módulo atual ( Origem )
Ou seja, o método não pode ser especializado entre diferentes módulos Swift (por exemplo, o método genérico da biblioteca Cocoapods). Uma exceção é a biblioteca Swift padrão, na qual tipos básicos como Array
, Set
e Dictionary
. Todos os genéricos da biblioteca base são especializados em tipos específicos.
Nota: Os atributos @inlinable
e @usableFromInline
foram implementados no Swift 4.2, que permite ao otimizador ver os corpos dos métodos de outros módulos e parece que há uma oportunidade de especializá-los, mas esse comportamento não foi testado por mim ( fonte )
Referências
- Descrição dos genéricos
- Otimização no Swift
- Apresentação mais detalhada e aprofundada sobre o tema.
- Artigo em inglês