Genéricos em Rust
Você já vem usando genéricos (generics) mesmo sem chamá-los pelo nome: Vec<i32> e Option<T> são genéricos. Esta página mostra como escrever as suas próprias funções e structs genéricas.
O problema que os genéricos resolvem
Seção intitulada “O problema que os genéricos resolvem”Imagine que você quer uma função que devolve o maior número de uma lista, tanto para i32 quanto para f64. Sem genéricos, você teria que escrevê-la duas vezes:
fn largest_i32(numbers: &[i32]) -> i32 { let mut largest = numbers[0]; for &n in numbers { if n > largest { largest = n; } } largest}
fn largest_f64(numbers: &[f64]) -> f64 { let mut largest = numbers[0]; for &n in numbers { if n > largest { largest = n; } } largest}É a mesma lógica, copiada e colada para cada tipo. Com genéricos, você escreve uma vez só.
Uma função genérica
Seção intitulada “Uma função genérica”fn largest<T: PartialOrd + Copy>(numbers: &[T]) -> T { let mut largest = numbers[0]; for &n in numbers { if n > largest { largest = n; } } largest}
fn main() { let integers = [10, 25, 3, 47, 8]; let floats = [1.5, 2.8, 0.3];
println!("{}", largest(&integers)); println!("{}", largest(&floats));}Saída:
472.8Leia <T: PartialOrd + Copy> como “para qualquer tipo T, desde que ele possa ser comparado (PartialOrd) e copiado (Copy)”. O T é só um marcador de lugar: o Rust gera uma versão da função para cada tipo concreto com que você realmente a chama (i32, f64 e assim por diante).
Uma struct genérica
Seção intitulada “Uma struct genérica”Structs também podem ser genéricas:
struct Pair<T> { first: T, second: T,}
fn main() { let numbers = Pair { first: 5, second: 10 }; let words = Pair { first: "olá", second: "mundo" };
println!("{} {}", numbers.first, numbers.second); println!("{} {}", words.first, words.second);}Saída:
5 10olá mundoUma única definição de Pair<T> funciona com números, texto ou qualquer outro tipo. numbers é um Pair<i32> e words é um Pair<&str>, mas você só escreveu a struct uma vez.
Isso tem algum custo em tempo de execução?
Seção intitulada “Isso tem algum custo em tempo de execução?”Não. O Rust gera uma versão separada e especializada da função ou da struct para cada tipo concreto que você realmente usa (isso se chama monomorfização, ou monomorphization). Quando o programa roda, não sobra nenhum código genérico: só funções comuns, com tipos bem definidos, tão rápidas quanto se você tivesse escrito cada uma à mão.
Métodos em uma struct genérica
Seção intitulada “Métodos em uma struct genérica”Você pode adicionar métodos a uma struct genérica com um bloco impl, igualzinho ao que viu na página de Structs. A única novidade é que o impl também precisa do seu próprio <T>:
struct Pair<T> { first: T, second: T,}
impl<T> Pair<T> { fn new(first: T, second: T) -> Self { Pair { first, second } }
fn swap(self) -> Pair<T> { Pair { first: self.second, second: self.first } }}
impl Pair<f64> { fn average(&self) -> f64 { (self.first + self.second) / 2.0 }}
fn main() { let words = Pair::new("esquerda", "direita").swap(); println!("{} {}", words.first, words.second);
let scores = Pair::new(7.0, 9.0); println!("{}", scores.average());}Saída:
direita esquerda8Aqui há dois tipos de bloco impl:
impl<T> Pair<T>quer dizer “estes métodos existem para umPairde qualquer tipo”. Leia o primeiro<T>como “para qualquer tipoT”. Sem ele, o Rust acharia queTé o nome de um tipo de verdade e reclamaria que não consegue encontrá-lo.impl Pair<f64>não tem<T>. Os métodos dele existem só paraPair<f64>. É por isso queaveragepode usar+e/ 2.0. Experimente chamarwords.average()e o compilador vai dizer que esse método não existe paraPair<&str>.
Uma struct também pode ter mais de um parâmetro de tipo, como struct Pair<T, U> { first: T, second: U }, quando os dois campos podem ter tipos diferentes. E quando os trait bounds ficam compridos, você pode passá-los para uma cláusula where para manter a assinatura legível. Isso é explicado na página de Traits avançados.
Prática
Seção intitulada “Prática”1. O menor, de forma genérica
Seção intitulada “1. O menor, de forma genérica”Escreva fn smallest<T: PartialOrd + Copy>(items: &[T]) -> T, que devolve o menor item. Teste com [8, 3, 5] e com os caracteres ['r', 'u', 's', 't']. Dica: é a função largest desta página com uma comparação invertida.
Ver solução
fn smallest<T: PartialOrd + Copy>(items: &[T]) -> T { let mut smallest = items[0]; for &item in items { if item < smallest { smallest = item; } } smallest}
fn main() { println!("{}", smallest(&[8, 3, 5])); println!("{}", smallest(&['r', 'u', 's', 't']));}Saída:
3r2. A maior metade de um Pair
Seção intitulada “2. A maior metade de um Pair”Adicione a Pair<T> um método larger(&self) -> &T que devolve uma referência ao campo que for maior. Teste com um Pair de números e um Pair de palavras. Dica: comparar exige PartialOrd, então coloque o bound no próprio impl: impl<T: PartialOrd> Pair<T>.
Ver solução
struct Pair<T> { first: T, second: T,}
impl<T: PartialOrd> Pair<T> { fn larger(&self) -> &T { if self.first > self.second { &self.first } else { &self.second } }}
fn main() { let numbers = Pair { first: 3, second: 9 }; let words = Pair { first: "pera", second: "maçã" };
println!("{}", numbers.larger()); println!("{}", words.larger());}Saída:
9peraStrings são comparadas em ordem alfabética (pelo valor de cada caractere), e como p vem depois de m, "pera" é a “maior”.
- Genéricos permitem escrever uma única função ou struct que funciona com vários tipos, em vez de duplicar código.
<T>é um tipo que serve de marcador de lugar; trait bounds (T: PartialOrd) limitam o queTpode ser.impl<T> Pair<T>adiciona métodos para todoT;impl Pair<f64>adiciona métodos só para um tipo concreto.- O código genérico é compilado em versões especializadas para cada tipo, sem nenhum custo de desempenho em tempo de execução.
Teste o que aprendeu
Seção intitulada “Teste o que aprendeu”Teste o que aprendeu
1. Que problema os genéricos resolvem?
2. Em fn largest<T: PartialOrd + Copy>(...), o que PartialOrd + Copy faz?
3. Usar genéricos tem algum custo em tempo de execução comparado com código escrito à mão?
4. O que uma única definição de struct Pair<T> permite fazer?
Pontuação: 0 / 4
Para saber mais
Seção intitulada “Para saber mais”- Generic Data Types — The Rust Book (em inglês): genéricos em funções, structs, enums e métodos, incluindo o exemplo do
largest. - Generics — Rust by Example (em inglês): exemplos curtos que você pode rodar, com funções e structs genéricas e a sintaxe delas.
- Implementation — Rust by Example (em inglês): mais sobre blocos
impl<T>e sobre implementar métodos para um único tipo concreto. - Bounds — Rust by Example (em inglês): como os trait bounds limitam o que um tipo genérico pode ser, e por que você precisa deles.
- Generic parameters — The Rust Reference (em inglês): as regras exatas, para quando você quiser todos os detalhes.