Swift: Tutorial de Genéricos do Swift
Os genéricos permitem escrever código flexível que funciona com qualquer tipo, evitando a necessidade de duplicar a mesma lógica para cada tipo -- como um molde universal que produz a forma correta independentemente do material derramado.
1. O Que Você Vai Aprender
- Como definir funções genéricas e tipos genéricos
- Como as restrições de tipo limitam o escopo dos parâmetros genéricos
- Tipos associados em protocolos
- Uso avançado de cláusulas where
- Aplicações reais de genéricos na biblioteca padrão
2. Uma História Real de um Engenheiro Back-End
(1) Problema: Escrever a Mesma Lógica para Cada Tipo
Charlie estava construindo um serviço de cache e precisava de uma estrutura de dados de pilha último-a-entrar-primeiro-a-sair. Ele primeiro escreveu uma versão para Int:
struct IntStack {
private var items: [Int] = []
mutating func push(_ item: Int) { items.append(item) }
mutating func pop() -> Int? { items.popLast() }
}
Mas logo ele precisou suportar String, Double e até um tipo Usuario personalizado. Charlie se viu copiando e colando -- apenas o nome do tipo mudava; a lógica era idêntica.
(2) A Solução Genérica
Genéricos transformam o tipo em um parâmetro -- uma implementação serve para todos:
struct Stack<Element> {
private var items: [Element] = []
mutating func push(_ item: Element) { items.append(item) }
mutating func pop() -> Element? { items.popLast() }
}
Agora Pilha<Int>, Pilha<String> e Pilha<Usuario> usam a mesma implementação, com o compilador garantindo a segurança de tipo.
(3) Benefícios: 60% Menos Código
| Dimensão | Abordagem Não-Genérica | Abordagem Genérica |
|---|---|---|
| Linhas de código | 120 linhas (3 tipos) | 20 linhas (1 genérico) |
| Custo de adicionar novo tipo | 40 linhas de copiar-colar | 1 linha: Pilha<NovoTipo> |
| Segurança de tipo | Cada tipo independente | Verificado em tempo de compilação |
| Custo de manutenção | Alterar lógica em 3 lugares | Alterar apenas em 1 lugar |
3. Funções Genéricas
Funções genéricas permitem usar tipos placeholder nas definições de função; o tipo concreto é determinado no local da chamada.
graph LR
A["func trocar<T>(a: inout T, b: inout T)"] --> B["Chamada com Int"]
A --> C["Chamada com String"]
A --> D["Chamada com Double"]
B --> E["T = Int: troca segura"]
C --> F["T = String: troca segura"]
D --> G["T = Double: troca segura"]
(1) Sintaxe de Parâmetro Genérico
Parâmetros genéricos são escritos entre chaves angulares <> após o nome da função, convencionalmente usando letras maiúsculas T, U, V como placeholders.
| Sintaxe | Significado | Exemplo |
|---|---|---|
<T> |
Parâmetro genérico único | func identidade<T>(_ valor: T) -> T |
<T, U> |
Dois parâmetros genéricos | func par<T, U>(_ a: T, _ b: U) -> (T, U) |
<T: Equatable> |
Genérico restrito | func ehIgual<T: Equatable>(_ a: T, _ b: T) -> Bool |
▶ Exemplo: Trocando Valores de Duas Variáveis
// ============================================
// Generic function: swap two values of any type
// ============================================
func swapValues<T>(_ a: inout T, _ b: inout T) {
let temp = a
a = b
b = temp
}
var x = 10
var y = 20
swapValues(&x, &y)
print("x = \(x), y = \(y)")
var firstName = "Alice"
var lastName = "Bob"
swapValues(&firstName, &lastName)
print("firstName = \(firstName), lastName = \(lastName)")
Saída:
TEXT 📖 Somente leiturax = 20, y = 10 firstName = Bob, lastName = Alice
(2) Múltiplos Parâmetros Genéricos
Funções podem ter múltiplos parâmetros genéricos, cada um representando um tipo diferente em uma posição diferente.
▶ Exemplo: Construindo um Par Chave-Valor
// ============================================
// Multiple generic parameters to build a key-value pair
// ============================================
func makePair<K, V>(_ key: K, _ value: V) -> (K, V) {
return (key, value)
}
let pair1 = makePair("id", 1001)
let pair2 = makePair(3.14, "Pi")
print(pair1)
print(pair2)
Saída:
TEXT 📖 Somente leitura(id, 1001) (3.14, Pi)
4. Tipos Genéricos
Tipos genéricos permitem definir structs, classes ou enums com placeholders, permitindo que lidem com dados de qualquer tipo.
graph TB
A["Pilha<Elemento>"] --> B["empilhar(Elemento)"]
A --> C["desempilhar() -> Elemento?"]
A --> D["topo() -> Elemento?"]
A --> E["contagem: Int"]
B --> F["adicionar a itens"]
C --> G["remover último de itens"]
D --> H["retornar itens.last"]
(1) Structs Genéricas
struct Stack<Element> { ... }
| Caso de Uso | Sintaxe | Descrição |
|---|---|---|
| Pilha de Int | Pilha<Int> |
Só pode empilhar/desempilhar valores Int |
| Pilha de String | Pilha<String> |
Só pode empilhar/desempilhar valores String |
| Pilha de tipo personalizado | Pilha<Usuario> |
Só pode empilhar/desempilhar valores Usuario |
▶ Exemplo: Estrutura de Dados Pilha Genérica
// ============================================
// Generic stack: supports last-in-first-out for any type
// ============================================
struct Stack<Element> {
private var items: [Element] = []
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element? {
items.popLast()
}
func peek() -> Element? {
items.last
}
var count: Int { items.count }
}
var intStack = Stack<Int>()
intStack.push(10)
intStack.push(20)
intStack.push(30)
print("Pop: \(intStack.pop() ?? 0)")
var stringStack = Stack<String>()
stringStack.push("Alice")
stringStack.push("Bob")
print("Peek: \(stringStack.peek() ?? "")")
Saída:
TEXT 📖 Somente leituraPop: 30 Peek: Bob
(2) Extensões Genéricas
Ao estender um tipo genérico, você não precisa redeclarar o parâmetro de tipo -- basta usar o nome do placeholder diretamente.
▶ Exemplo: Adicionando um Método map à Pilha Genérica
// ============================================
// Adding map functionality to Stack via extension
// ============================================
extension Stack {
func map<T>(_ transform: (Element) -> T) -> Stack<T> {
var result = Stack<T>()
for item in items {
result.push(transform(item))
}
return result
}
}
var s = Stack<Int>()
s.push(1); s.push(2); s.push(3)
let doubled = s.map { $0 * 2 }
print("Count: \(doubled.count)")
Saída:
TEXT 📖 Somente leituraCount: 3
5. Restrições de Tipo e Tipos Associados
As restrições de tipo limitam os parâmetros genéricos para satisfazer condições específicas. Tipos associados são "placeholders genéricos" em protocolos, especificados pelo implementador.
graph TB
A["<T: Equatable>"] --> B["Pode usar =="]
A --> C["func encontrarIndice<T: Equatable>(de: T, em: [T]) -> Int?"]
B --> D["Int =="]
B --> E["String =="]
B --> F["Personalizado com Equatable"]
(1) Restrições de Tipo
| Sintaxe de Restrição | Significado | Caso de Uso |
|---|---|---|
<T: Equatable> |
T deve ser equacionável | Encontrar elementos, desduplicação |
<T: Hashable> |
T deve ser hasheável | Chaves de dicionário |
<T: Comparable> |
T deve ser comparável | Ordenação, encontrar mín/máx |
<T: Codable> |
T deve ser codável/decodável | Serialização JSON |
▶ Exemplo: Usando Restrição Equatable para Encontrar um Elemento
// ============================================
// Constraining generic parameter to be equatable
// ============================================
func findIndex<T: Equatable>(of value: T, in array: [T]) -> Int? {
for (index, item) in array.enumerated() {
if item == value {
return index
}
}
return nil
}
let numbers = [10, 20, 30, 40]
if let index = findIndex(of: 30, in: numbers) {
print("Found at index \(index)")
}
let names = ["Alice", "Bob", "Charlie"]
if let index = findIndex(of: "Bob", in: names) {
print("Found at index \(index)")
}
Saída:
TEXT 📖 Somente leituraFound at index 2 Found at index 1
(2) Tipos Associados e Cláusulas where
A palavra-chave associatedtype permite que protocolos suportem genéricos -- o próprio protocolo não especifica um tipo concreto; o implementador decide.
▶ Exemplo: Protocolo com Tipo Associado e Cláusula where
// ============================================
// Associated type protocol + where clause
// ============================================
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
struct Box<T>: Container {
typealias Item = T
private var items: [T] = []
mutating func append(_ item: T) { items.append(item) }
var count: Int { items.count }
subscript(i: Int) -> T { items[i] }
}
func allItemsMatch<C1: Container, C2: Container>(
_ c1: C1, _ c2: C2
) -> Bool where C1.Item == C2.Item, C1.Item: Equatable {
guard c1.count == c2.count else { return false }
for i in 0..<c1.count {
if c1[i] != c2[i] { return false }
}
return true
}
var box1 = Box<Int>()
box1.append(1); box1.append(2); box1.append(3)
var box2 = Box<Int>()
box2.append(1); box2.append(2); box2.append(3)
print("All match: \(allItemsMatch(box1, box2))")
Saída:
TEXT 📖 Somente leituraAll match: true
6. Exemplo Completo: Fila Genérica e Sistema de Cache
// ============================================
// Full example: Generic queue + bounded cache
// Features: general-purpose queue, cache wrapper, statistics
// ============================================
import Foundation
// 1. Generic queue
struct Queue<Element> {
private var items: [Element] = []
mutating func enqueue(_ item: Element) { items.append(item) }
mutating func dequeue() -> Element? { items.isEmpty ? nil : items.removeFirst() }
var count: Int { items.count }
}
// 2. Generic cache (with capacity limit)
class Cache<Key: Hashable, Value> {
private var storage: [Key: Value] = [:]
private let capacity: Int
init(capacity: Int = 100) {
self.capacity = capacity
}
func set(_ value: Value, for key: Key) {
if storage.count >= capacity {
storage.removeFirst()
}
storage[key] = value
}
func get(for key: Key) -> Value? { storage[key] }
var count: Int { storage.count }
}
// 3. Usage example
var queue = Queue<String>()
queue.enqueue("Task 1")
queue.enqueue("Task 2")
queue.enqueue("Task 3")
print("Queue count: \(queue.count)")
print("Dequeue: \(queue.dequeue() ?? "")")
let cache = Cache<String, Int>(capacity: 3)
cache.set(42, for: "answer")
cache.set(100, for: "score")
print("Cached answer: \(cache.get(for: "answer") ?? 0)")
print("Cache count: \(cache.count)")
Saída:
TEXT 📖 Somente leituraQueue count: 3 Dequeue: Task 1 Cached answer: 42 Cache count: 2
❓ Perguntas Frequentes
P: Qual é a diferença entre genéricos e Any? R: Genéricos preservam informações de tipo e são seguros em tempo de compilação. Any verifica os tipos em tempo de execução. Genéricos significam "o compilador conhece o tipo concreto"; Any significa "o compilador desiste da verificação de tipo". P: Quando devo usar parâmetros genéricos vs restrições de protocolo? R: Parâmetros genéricos funcionam para cenários onde "o tipo pode ser arbitrário, mas as operações são consistentes". Restrições de protocolo funcionam para cenários onde "o tipo deve satisfazer uma interface específica". Os dois são frequentemente usados juntos. P: Qual é a diferença entre tipos associados e parâmetros genéricos? R: Tipos associados são definidos dentro de um protocolo e especificados pelo implementador. Parâmetros genéricos são definidos em uma função/tipo e especificados pelo chamador. Tipos associados são essencialmente "os parâmetros genéricos do protocolo". P: Como o Array genérico é implementado? R: O Array da biblioteca padrão é uma struct genérica
Array<Element>, onde Element é o parâmetro genérico. Todas as operações de array (append, sort, map) são métodos genéricos. P: Genéricos impactam o desempenho? R: Não. O Swift usa "especialização genérica" -- o compilador gera versões especializadas para cada tipo concreto, então não há sobrecarga em tempo de execução dos genéricos.
📖 Resumo
- Funções genéricas usam
<T>para declarar parâmetros de tipo; os tipos concretos são inferidos no local da chamada - Tipos genéricos (structs/classes/enums) podem lidar com dados de qualquer tipo com uma única implementação
- Restrições de tipo
<T: Protocolo>limitam o escopo dos parâmetros genéricos e garantem segurança de tipo - Tipos associados
associatedtypepermitem que protocolos suportem genéricos; o implementador decide o tipo concreto - Cláusulas where expressam relações de tipo complexas com múltiplas condições
- A biblioteca padrão usa genéricos extensivamente: Array, Dictionary e Optional são todos tipos genéricos
📝 Exercícios
- Básico: Escreva uma função genérica
encontrarMax<T: Comparable>que aceita dois parâmetros e retorna o maior. Teste com os tipos Int, Double e String. - Intermediário: Estenda o tipo
Pilhada Seção 4 adicionando um métodofilterque aceita uma closure(Elemento) -> Boole retorna uma nova pilha filtrada. - Desafio: Use genéricos para implementar um
BufferCircular<Elemento>com métodosescrever(_:)eler() -> Elemento?, capacidade fixa e sobrescrita automática dos dados mais antigos quando cheio.