Nesta postagem abordo o que são códigos genéricos e qual a utilidade na rotina de um desenvolvedor Swift.
O que são códigos genéricos?
A melhor definição de genéricos (ou generics
) é a que encontramos no nosso dicionário: Que abarca muitas coisas gerais (ao mesmo tempo). Em programação, o conceito não é diferente. Generic
é utilizado para escrever funções e tipos flexíveis e reutilizáveis que pode funcionar com qualquer tipo, sujeito aos requisitos definidos.
Essa abordagem permite escrever código onde os tipos são especificados posteriormente, quando são instanciados. O maior benefício é reduzir a duplicação e expressar sua intenção de maneira clara e abstrata.
Deste modo, ao invés de criar uma função ou bloco de código que atenda cada tipo, como String
ou Int
, podemos especificar um tipo genérico que atenda qualquer situação. Assim deixamos a própria linguagem inferir o tipo com base no valor informado. O termo Generic
também pode ser conhecido como tipo placeholder
.
Swift
é considerada uma linguagem type-safe
. Isso significa que, o tipo deve ser definido antes da compilação. Por esse motivo, todo tipo deve ser definido.
Criando código genérico
Uma função genérica em Swift possui um tipo reservado (um placeholder
) antes de seu nome e entre sinais menor e maior. Dessa forma: <Tipo>
.
Os parâmetros também devem receber o mesmo tipo reservado.
Este tipo reservado, normalmente, é identificado pelas letras T
, U
, V
e entre outras.
Entendendo com exemplos
Utilizando o exemplo clássico, vamos supor que precisamos trocar dois valores numéricos, entre a
e b
. Para reproduzir a criação dessa função, devemos receber dois parâmetros do tipo inteiro. Especificando o tipo Int
teríamos algo como:
func swapTwoInts(a: Int, b: Int) -> (Int, Int) {
return (b, a)
}
swapTwoInts(a: 1, b: 2) // (2, 1)
Agora vamos supor que seja necessário trocar dois números de ponto flutuante (Double
) ou mesmo textos (String
). Nesta situação, precisaríamos escrever outra função para esta tarefa, pois a função acima aceita apenas a entrada de números inteiros.
Convertendo a mesma função para o tipo genérico, teríamos o código abaixo com o mesmo comportamento:
func swapTwoValues<T>(a: T, b: T) -> (T, T) {
return (b, a)
}
swapTwoValues(a: 10.5, b: 12.8) // (12.8, 10.5)
swapTwoValues(a: "Hello", b: "World") // ("World", "Hello")
Assim é possível trocar qualquer tipo de valor, não será necessário escrever outras funções para cada situação.
Vale lembrar que não é possível informar tipos diferentes nos parâmetros ao invocar a mesma função genérica.
Restringindo o tipo genérico
Códigos genéricos também são muito utilizados para restringir instâncias somente para tipos de possuem alguma conformidade, com class
ou protocol
, por exemplo.
Conformidade com classe
No código a seguir, T
está em conformidade com o tipo BankAccount
. Portanto, não é possível invocar a função displayBankAccount()
com nenhum outro valor que não seja do tipo BankAccount
.
class BankAccount {
var holder: String
var number: Int
init(holder: String, number: Int) {
self.holder = holder
self.number = number
}
}
func displayBankAccount<T: BankAccount>(for account: T) -> String {
return "\(account.number) account belongs to \(account.holder)"
}
var myBankAccount = BankAccount(holder: "Felipe", number: 12345678)
displayBankAccount(for: myBankAccount) // 12345678 account belongs to Felipe
Da mesma forma que aplicamos esse comportamento nos métodos, também é possível deixar o Swift inferir o tipo de uma class
ou struct
e suas propriedades.
O código genérico permite ao Swift inferir o tipo ou ao desenvolvedor preestabelecer essa informação. A struct
abaixo pode ser instanciada de duas maneiras:
struct GenericStruct<T> {
var property: T?
}
let explictStruct = GenericStruct<Bool>() // T is Bool
let implicitStruct = GenericStruct(property: 10) // T is Int
Conformidade com protocolo
No exemplo abaixo estamos restringindo a função compareValues()
somente para os tipos que conformam com o protocolo Equatable.
func compareValues<T: Equatable>(between a: T, and b: T) -> String {
return a == b ? "They are equals" : "They aren't equals"
}
compareValues(between: 2, and: 3) // They aren't equals
compareValues(between: "Hi", and: "Hi") // They are equals
Protocolos genéricos
Protocolos por si só possibilitam inúmeras implementações, por isso é tão utilizado e difundido entre desenvolvedores Swift
. E com o uso de generics
, podemos ampliar ainda mais este conceito. Utilizando a técnica de associated type
e da cláusula where
podemos criar protocolos genéricos e fazer restrições por tipo, semelhante aos placeholders
dos genéricos.
Visto que, protocolos genéricos fazem uso de associated types
, este conceito também é conhecido como Protocol Associated Types
ou PATs
.
Uso convencional de protocolo
Antes de aprofundar sobre protocolo genérico, vamos criar um protocolo que nos obriga a adicionar uma propriedade do tipo String
.
A criação do protocolo seria dessa maneira:
protocol MyProtocol {
var property: String { get set }
}
E uma classe em conformidade com este protocolo seria assim:
class MyClass: MyProtocol {
var property: String = "Property value"
}
Para aplicar o conceito de generics
na nossa propriedade e permitir informar qualquer outro tipo diferente de String
, podemos fazer o uso de associated types
.
Protocolos com Associated Types
Em protocolos genéricos, a solução de placeholder
ou algo como <T>
é relativo à associated type
:
protocol MyGenericProtocol {
associatedtype T
var property: T { get set }
}
Dessa maneira, para que uma class
ou struct
se conforme com o protocolo será necessário, além de implementar a propriedade, definir o seu tipo. Está definição pode ser feita de forma implícita
ou explícita
.
Associated Type
implícito
De forma implícita, ou seja, que fica subentendido pelo compilador:
class MyClass: MyGenericProtocol {
var property = true // T is `Bool`
}
Associated Type
explícito
De forma explícita, devemos utilizar um typealias
para especificar o tipo desejado:
class MyClass: MyGenericProtocol {
typealias T = String
var property: T = "My generic property" // T is `String`
}