Como usar Genéricos no Swift
() translation by (you can also view the original English article)
Genéricos permitem à você declarar uma variável que, em execução, pode ser atribuído a um conjunto de tipos definidos por nós.
Em Swift, um array pode manter dados de qualquer tipo. Se precisarmos de um array de inteiros, strings ou floats, podemos criar um com a biblioteca padrão do Swift. O tipo que o array deverá manter é definido quando ele é declarado. Os arrays são um exemplo comum do uso dos genéricos. Se você for implementar sua própria coleção, você definitivamente gostaria de usar os genéricos.
Vamos explorar os genéricos e as grandes coisas que eles nos permitem fazer.
1. Funções genéricas
Começaremos criando uma função genérica simples. Nosso objetivo é criar uma função que verifique se dois objetos quaisquer são do mesmo tipo. Se eles forem do mesmo tipo, então tornaremos o valor do segundo objeto igual ao valor do primeiro objeto. Se eles não forem do mesmo tipo, então iremos imprimir a mensagem "not the same type". Aqui é uma tentativa de implementar essa função em Swift.
1 |
func sameType (one: Int, inout two: Int) -> Void { |
2 |
// This will always be true
|
3 |
if(one.dynamicType == two.dynamicType) { |
4 |
two = one |
5 |
}
|
6 |
else { |
7 |
print("not same type") |
8 |
}
|
9 |
}
|
Em um mundo sem genéricos, encontrariamos um problema grave. Na definição da função precisaríamos especificar o tipo de cada argumento. Como resultado, se quisermos que nossa função trabalhe com todo tipo possível, teríamos que criar uma definição da nossa função com parâmetros diferentes para todas as combinações possíveis de tipos. Isso não é uma opção viável.
1 |
func sameType (one: Int, inout two: String) -> Void { |
2 |
// This would always be false
|
3 |
if(one.dynamicType == two.dynamicType) { |
4 |
two = one |
5 |
}
|
6 |
else { |
7 |
print("not same type") |
8 |
}
|
9 |
}
|
Podemos evitar este problema usando genéricos. Dê uma olhada no exemplo a seguir em que aproveitamos dos genéricos.
1 |
func sameType<T,E>(one: T, inout two: E) -> Void { |
2 |
if(one.dynamicType == two.dynamicType) { |
3 |
two = one |
4 |
}
|
5 |
else { |
6 |
print("not same type") |
7 |
}
|
8 |
}
|
Aqui vemos a sintaxe do uso dos genéricos. Os tipos genéricos são simbolizados por T
e E
. Os tipos são especificados colocando <T, E>
na definição da nossa função, após o nome da função. Pense em T
e E
como marcadores para qualquer tipo que usarmos nossa função.
Há um grande problema com essa função. Ela não compila. O compilador emite um erro, indicando que T
não é conversível para E
. Os genéricos assumem que já que T
e E
são indicadores diferentes, eles também devem ser de tipos diferentes. Está tudo bem, ainda podemos realizar nosso objetivo com duas definições da nossa função.
1 |
func sameType<T,E>(one: T, inout two: E) -> Void { |
2 |
print("not same type") |
3 |
}
|
4 |
|
5 |
func sameType<T>(one: T, inout two: T) -> Void { |
6 |
two = one |
7 |
}
|
Há dois casos para argumentos da nossa função:
- Se eles forem do mesmo tipo, a segunda implementação será chamada. O valor da
two
é então atribuído àone
. - Se eles forem de tipos diferentes, a primeira implementação é chamada e a string "not same type" é exibida no console.
Reduzimos a quantidade de definições da nossa função de uma potencial quantidade infinita de combinações de tipos de argumentos para apenas dois. Agora nossa função trabalha com qualquer combinação de tipos como argumentos.
1 |
var s = "apple" |
2 |
var p = 1 |
3 |
|
4 |
sameType(2,two: &p) |
5 |
print(p) |
6 |
sameType("apple", two: &p) |
7 |
|
8 |
// Output:
|
9 |
1
|
10 |
"not same type" |
Também podemos aplicar o conceito de genéricos à classes e estruturas. Vamos dar uma olhada em como isso funciona.
2. Classes e estruturas genéricas
Considere a situação onde gostaríamos de fazer nosso próprio tipo de dado, uma árvore binária. Se usarmos a tradicional abordagem a qual não usamos genéricos, então faríamos uma árvore binária que pode manter apenas um tipo de dado. Felizmente, temos os genéricos.
Uma árvore binária consiste em nós que possuem:
- dois filhos ou braços, que são outros nós
- um pedaço de dado que é um elemento genérico
- um nó pai que geralmente não é referenciado pelo nó



Toda árvore binária tem um nó raiz que não tem pai. Os dois filhos são geralmente diferenciados como nós da esquerda e da direita.
Qualquer dado no filho da esquerda deve ser menor do que o do nó pai. Qualquer dado no filho da direita deve ser maior que o do nó pai.
1 |
class BTree <T: Comparable> { |
2 |
|
3 |
var data: T? = nil |
4 |
var left: BTree<T>? = nil |
5 |
var right: BTree<T>? = nil |
6 |
|
7 |
func insert(newData: T) { |
8 |
if (self.data > newData) { |
9 |
// Insert into left subtree
|
10 |
}
|
11 |
else if (self.data < newData) { |
12 |
// Insert into right subtree
|
13 |
}
|
14 |
else if (self.data == nil) { |
15 |
self.data = newData |
16 |
return
|
17 |
}
|
18 |
|
19 |
}
|
20 |
|
21 |
}
|
A declaração da classe BTree
também declara o genérico T
, que é atrelado ao protocolo Comparable
. Iremos discutir sobre protocolos e restrições um pouco mais a frente.
Nosso item de dado da árvore é especificado como sendo do tipo T
. Qualquer elemento inserido deve ser também do tipo T
como especificado na declaração do nosso método insert(_:)
. Para uma classe genérica, o tipo é especificado quando o objetivo é declarado.
1 |
var tree: BTree<int> |
Neste exemplo, criamos uma árvore binária de inteiros. Fazer uma classe genérica é muito simples. Tudo que precisamos fazer é incluir o genérico na declaração e referencia-lo no corpo quando necessário.
3. Protocolos e restrições
Em muitas situações, temos que manipular arrays para realizar um objetivo programático. Isto poderia ser sorteando, pesquisando, etc. Daremos uma olhada em como os genéricos podem nos ajudar com a pesquisa.
A principal razão para usarmos uma função genérica para pesquisar, é que queremos ser capazes de pesquisar um array independentemente do tipo de objetos que ele contém.
1 |
func find <T> (array: [T], item : T) ->Int? { |
2 |
var index = 0 |
3 |
while(index < array.count) { |
4 |
if(item == array[index]) { |
5 |
return index |
6 |
}
|
7 |
index++ |
8 |
}
|
9 |
return nil; |
10 |
}
|
No exemplo acima, a função find(array:item:)
aceita um array do tipo genérico T
e pesquisa por um correspondente do item
, que é do tipo T
.
Há um problema. Se você tentar compilar o exemplo acima, o compilador irá emitir outro erro. O compilador nos dirá que o operador binário ==
não pode ser aplicado a dois operadores T
. A razão é obvio se você pensar sobre isso. Não podemos garantir que o tipo genérico T
suporte o operador ==
. Felizmente, o Swift abrange isso. Dê uma olhada na atualização do exemplo a seguir.
1 |
func find <T: Equatable> (array: [T], item : T) ->Int? { |
2 |
var index = 0 |
3 |
while(index < array.count) { |
4 |
if(item == array[index]) { |
5 |
return index |
6 |
}
|
7 |
index++ |
8 |
}
|
9 |
return nil; |
10 |
}
|
Se especificarmos que o tipo genérico deve estar em conformidade ao protocolo Equatable
, então o compilador nos permitirá seguir. Em outras palavras, aplicamos uma restrição sobre quais os tipos o T
pode representar. Para adicionar uma restrição a um genérico, sua lista de protocolos deve ficar entre os colchetes.
Mas o que significa algo ser Equatable
? Isto simplesmente significa que ele suporta o operador de comparação ==
.
O Equatable
não é o único protocolo que podemos usar. O Swift tem outros protocolos, como o Hashable
e Comparable
. Vimos o Comparable
anteriormente no exemplo da árvore binária. Se um tipo estiver em conformidade com o protocolo Comparable
, então significa que os operadores <
e >
são suportados. Espero ter deixado claro que você pode usar qualquer protocolo que gostar e aplica-lo como uma restrição.
4. Definindo protocolos
Vamos usar um game como exemplo para demonstrar as restrições e os protocolos em ação. Em todo jogo, temos uma quantidade de objetos que precisamos atualizar a todo momento. Esta atualização pode ser a posição do objeto, vida, etc. Por enquanto vamos usar o exemplo da vida do objeto.
Na implementação do jogo, temos muitos objetos diferentes com vida que podem ser os inimigos, aliados, neutros, etc. Eles não são todos da mesma classe, como todos os nossos objetos diferentes podem ter funções diferentes.
Gostaríamos de criar uma função chamada check(_:)
para verificar a vida de um determinado objeto e atualizar seu status atual. Dependendo do status do objetos, podemos fazer alterações em sua vida. Queremos que esta função funcione com todos os objetos, independente do seu tipo. Isso significa que precisamos tornar a função check(_:)
uma função genérica. Fazendo isso, podemos percorrer através de diferentes objetos e chamar a check(_:)
para cada objeto.
Todos estes objetos devem ter uma variável para representar sua vida e uma função para alterar seu status de vivo. Vamos declarar um protocolo para ele e chama-lo de Healthy
,
1 |
protocol Healthy { |
2 |
mutating func setAlive(status: Bool) |
3 |
var health: Int { get } |
4 |
}
|
O protocolo define quais propriedades e métodos o tipo que esta em conformidade com o protocolo precisa implementar. Por exemplo, o protocolo requer que qualquer tipo em conformidade com o protocolo Healthy
implemente a função mutável setAlive(_:)
. O protocolo também requer uma propriedade chamada health
.
Agora vamos revisitar a função check(_:)
que declaramos anteriormente. Especificamos na declaração uma restrição que o tipo T
deve estar em conformidade com o protocolo Healthy
.
1 |
func check<T:Healthy>(inout object: T) { |
2 |
if (object.health <= 0) { |
3 |
object.setAlive(false) |
4 |
}
|
5 |
}
|
Verificamos a propriedade health
do objeto. Se for menor ou igual a zero, chamamos a setAlive(_:)
do objeto, passando false
. Como o T
precisa estar em conformidade com o protocolo healthy
, sabemos que a função setAlive(_:)
pode ser chamada por qualquer objeto que for passado à função check(_:)
.
5. Tipos associados
Se você gostaria de ter mais controle sobre seus protocolos, você pode usar tipos associados. Vamos ver o exemplo de árvore binária. Gostaríamos de criar uma função para fazer operações em uma árvore binária. Precisamos de alguma maneira para nos certificar que o argumento de entrada satisfaça o que definimos como uma árvore binária. Para resolver isso, podemos criar um protocolo BinaryTree
.
1 |
protocol BinaryTree { |
2 |
typealias dataType |
3 |
mutating func insert(data: dataType) |
4 |
func index(i: Int) -> dataType |
5 |
var data: dataType { get } |
6 |
}
|
Ele usa um tipo associado typealias dataType
. O dataType
é similar ao genérico. O T
usado anteriormente, se comporta de forma semelhante ao dataType
. Especificamos que uma árvore binária deve implementar as funções insert(_:)
e index(_:)
. O insert(_:)
recebe um argumento do tipo dataType
. O index(_:)
retorna um objeto dataType
. Também especificamos que a árvore binária deve ter uma propriedade data que é do tipo dataType
.
Graças ao nosso tipo associado sabemos que nossa árvore binária será consistente. Podemos assumir que o tipo passado ao insert(_:)
, recebido do index(_:)
e mantido pela data
são o mesmo para cada um. Se os tipos não forem do mesmo tipo, teremos problemas.
6. Clausura Where
O Swift também permite a você usar clausuras where com genéricos. Vamos ver como isso funciona. Há duas coisas que cláusulas where nos permitem cumprir com os genéricos:
- Podemos impor que tipos associados ou variáveis dentro de um protocolo são do mesmo tipo.
- Podemos atribuir um protocolo à um tipo associado.
Para mostrar isto em ação, vamos implementar uma função para manipular as árvores binárias. O objetivo é buscar o valor máximo entre as duas árvores binárias.
Pra simplificar, iremos adicionar uma função no protocolo BinaryTree
, chamado inorder()
. In-order é um dos três tipos de busca primária de profundidade. É uma ordenação de nós da árvore que percorre recursivamente, a sub-árvore esquerda, o nó atual e a sub-árvore direita.
1 |
protocol BinaryTree { |
2 |
typealias dataType |
3 |
mutating func insert(data: dataType) |
4 |
func index(i: Int) -> dataType |
5 |
var data: dataType { get } |
6 |
// NEW
|
7 |
func inorder() -> [dataType] |
8 |
}
|
Esperamos que a função inorder()
retorne um array de objetos de tipo associado. Também implementamos a função twoMax(treeOne:treeTwo:)
, que recebe duas árvores binárias.
1 |
func twoMax<B: BinaryTree, T: BinaryTree where B.dataType == T.dataType, B.dataType: Comparable, T.dataType: Comparable> (inout treeOne: B, inout treeTwo: T) -> B.dataType { |
2 |
var inorderOne = treeOne.inorder() |
3 |
var inorderTwo = treeTwo.inorder() |
4 |
|
5 |
if (inorderOne[inorderOne.count] > inorderTwo[inorderTwo.count]) { |
6 |
return inorderOne[inorderOne.count] |
7 |
} else { |
8 |
return inorderTwo[inorderTwo.count] |
9 |
}
|
10 |
}
|
Nossa declaração é um pouco longa devido a clausura where
. A primeira exigência, B.dataType == T.dataType
, afirma que os tipos associados das duas árvores binárias devem ser os mesmo. Isso significa que seus objetos data
devem ser do mesmo tipo.
O segundo requerimento definido, B.dataType: Comparable, T.dataType: Comparable
, afirma que os tipos associados de ambos devem estar em conformidade com o protocolo Comparable
. Dessa maneira podemos verificar qual é o valor máximo quando realizarmos uma comparação.
Curiosamente, devido à natureza de uma árvore binaria, sabemos que o ultimo elemento de um in-order será o elemento de valor máximo dentro daquela árvore. Isto porque em uma árvore binária o nó mais a direita é o maior. Precisamos apenas olhar estes dois elementos para determinar o de valor maior.
Temos três casos:
- Se a árvore um conter o valor maior, então seu último elemento do inorder irá ser maior e retornaremos na primeira instrução
if
. - Se a árvore dois conter o valor maior, então seu ultimo elemento do inorder será o maior e retornaremos na clausura
else
da primeira instruçãoif
. - O máximo deles são iguais, então retornarem o ultimo elemento do inorder da árvore dois, que ainda é o maior entre ambos.
Conclusão
Neste tutorial, focamos nos genéricos do Swift. Aprendemos sobre valores genéricos e exploramos como usa-los em funções, classe e estruturas. Também fizemos uso dos genéricos nos protocolos e exploramos tipos associados e clausuras where.
Com uma boa compreensão de genéricos, você pode criar códigos mais versáteis e ser capaz de lidar melhor com problemas de codificação complicados.
Seja o primeiro a saber sobre novas traduções–siga @tutsplus_pt no Twitter!