O poder dos genéricos em Swift. Parte 1

Olá pessoal! Estamos compartilhando com você uma tradução preparada especialmente para os alunos do curso “Desenvolvedor iOS. Curso avançado . Boa leitura.



Função genérica, tipo genérico e restrições de tipo

O que são genéricos?


Quando eles trabalham, você os ama e, quando não, os odeia!

Na vida real, todo mundo conhece o poder dos genéricos: acordar de manhã, decidir o que beber, encher uma xícara.

Swift é uma linguagem de tipo seguro. Sempre que trabalhamos com tipos, precisamos especificá-los explicitamente. Por exemplo, precisamos de uma função que funcione com mais de um tipo. O Swift possui os tipos Any e AnyObject , mas eles devem ser usados ​​com cuidado e nem sempre. O uso de Any e AnyObject tornará seu código não confiável, pois será impossível rastrear incompatibilidade de tipo durante a compilação. É aqui que os genéricos vêm em socorro.

O código genérico permite criar funções e tipos de dados reutilizáveis ​​que podem funcionar com qualquer tipo que atenda a certas restrições, garantindo a segurança do tipo durante a compilação. Essa abordagem permite que você escreva um código que ajude a evitar duplicação e expresse sua funcionalidade de maneira clara e abstrata. Por exemplo, tipos como Array , Set e Dictionary usam genéricos para armazenar elementos.

Digamos que precisamos criar uma matriz que consiste em valores inteiros e seqüências de caracteres. Para resolver esse problema, vou criar duas funções.

 let intArray = [1, 2, 3, 4] let stringArray = [a, b, c, d] func printInts(array: [Int]) { print(intArray.map { $0 }) } func printStrings(array: [String]) { print(stringArray.map { $0 }) } 

Agora eu preciso gerar uma matriz de elementos do tipo float ou uma matriz de objetos de usuário. Se observarmos as funções acima, veremos que apenas a diferença de tipo é usada. Portanto, em vez de duplicar o código, podemos escrever uma função genérica para reutilização.

A história dos genéricos em Swift




Funções genéricas


A função genérica pode funcionar com qualquer parâmetro universal do tipo T O nome do tipo não diz nada sobre o que deve ser, mas diz que ambas as matrizes devem ser do tipo , independentemente do que seja. O tipo em si a ser usado em vez de é determinado sempre que a função print( _: ) é chamada.

 func print<T>(array: [T]) { print(array.map { $0 }) } 

Tipos genéricos ou polimorfismo paramétrico


O tipo genérico T do exemplo acima é um parâmetro de tipo. Você pode especificar vários parâmetros de tipo escrevendo vários nomes de parâmetros de tipo entre colchetes angulares, separados por vírgulas.

Se você observar Matriz e Dicionário <Chave, Elemento>, notará que eles nomearam parâmetros de tipo, isto é, Elemento e Chave, Elemento, que falam da relação entre o parâmetro de tipo e o tipo ou função genérica em que é usado .

Nota: Sempre forneça nomes para digitar parâmetros em uma notação CamelCase (por exemplo, T e TypeParameter ) para mostrar que eles são um nome para o tipo, não um valor.

Tipos genéricos


Essas são classes, estruturas e enumerações personalizadas que podem funcionar com qualquer tipo, semelhante a matrizes e dicionários.

Vamos criar uma pilha

 import Foundation enum StackError: Error { case Empty(message: String) } public struct Stack { var array: [Int] = [] init(capacity: Int) { array.reserveCapacity(capacity) } public mutating func push(element: Int) { array.append(element) } public mutating func pop() -> Int? { return array.popLast() } public func peek() throws -> Int { guard !isEmpty(), let lastElement = array.last else { throw StackError.Empty(message: "Array is empty") } return lastElement } func isEmpty() -> Bool { return array.isEmpty } } extension Stack: CustomStringConvertible { public var description: String { let elements = array.map{ "\($0)" }.joined(separator: "\n") return elements } } var stack = Stack(capacity: 10) stack.push(element: 1) stack.push(element: 2) print(stack) stack.pop() stack.pop() stack.push(element: 5) stack.push(element: 3) stack.push(element: 4) print(stack) 

Agora, essa pilha pode aceitar apenas elementos inteiros e, se eu precisar armazenar elementos de um tipo diferente, precisarei criar outra pilha ou convertê-la em uma aparência genérica.

 enum StackError: Error { case Empty(message: String) } public struct Stack<T> { var array: [T] = [] init(capacity: Int) { array.reserveCapacity(capacity) } public mutating func push(element: T) { array.append(element) } public mutating func pop() -> T? { return array.popLast() } public func peek() throws -> T { guard !isEmpty(), let lastElement = array.last else { throw StackError.Empty(message: "Array is empty") } return lastElement } func isEmpty() -> Bool { return array.isEmpty } } extension Stack: CustomStringConvertible { public var description: String { let elements = array.map{ "\($0)" }.joined(separator: "\n") return elements } } var stack = Stack<Int>(capacity: 10) stack.push(element: 1) stack.push(element: 2) print(stack) var strigStack = Stack<String>(capacity: 10) strigStack.push(element: "aaina") print(strigStack) 

Limitações de tipo genérico


Como um genérico pode ser de qualquer tipo, você não pode fazer muito com ele. Às vezes, é útil aplicar restrições a tipos que podem ser usados ​​com funções ou tipos genéricos. As restrições de tipo indicam que o parâmetro de tipo deve corresponder a um protocolo ou composição de protocolo específica.

Por exemplo, o tipo Swift Dictionary impõe restrições aos tipos que podem ser usados ​​como chaves para um dicionário. O dicionário requer que as chaves sejam hash para poder verificar se já contém valores para uma chave específica.

 func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) { // function body goes here } 

Essencialmente, criamos uma pilha do tipo T, mas não podemos comparar duas pilhas, porque aqui os tipos não correspondem a Equatable . Precisamos mudar isso para usar a Stack< T: Equatable > .

Como os genéricos funcionam? Vejamos um exemplo.

 func min<T: Comparable>(_ x: T, _ y: T) -> T { return y < x ? y : x } 

O compilador não possui duas coisas necessárias para criar o código da função:

  • Tamanhos de variáveis ​​do tipo T;
  • Os endereços da sobrecarga específica da função <, que deve ser chamada em tempo de execução.

Sempre que o compilador encontra um valor do tipo genérico, coloca o valor em um contêiner. Este contêiner possui um tamanho fixo para armazenar valores. Caso o valor seja muito grande, o Swift o alocará na pilha e armazenará um link para ela no contêiner.

O compilador também mantém uma lista de uma ou mais tabelas de testemunhas para cada parâmetro genérico: uma tabela de testemunhas para valores, mais uma tabela de testemunhas para cada protocolo de restrição de tipo. As tabelas de testemunha são usadas para enviar dinamicamente chamadas de função para as implementações desejadas em tempo de execução.

O fim da primeira parte. Por tradição, estamos aguardando seus comentários, amigos.

Segunda parte

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


All Articles