Pular para o conteúdo

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.

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ó.

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:

47
2.8
▶ Experimente você mesmo

Leia <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).

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 10
olá mundo
▶ Experimente você mesmo

Uma ú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.

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.

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 esquerda
8
▶ Experimente você mesmo

Aqui há dois tipos de bloco impl:

  • impl<T> Pair<T> quer dizer “estes métodos existem para um Pair de qualquer tipo”. Leia o primeiro <T> como “para qualquer tipo T”. Sem ele, o Rust acharia que T é 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ó para Pair<f64>. É por isso que average pode usar + e / 2.0. Experimente chamar words.average() e o compilador vai dizer que esse método não existe para Pair<&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.

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:

3
r
▶ Experimente você mesmo

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:

9
pera
▶ Experimente você mesmo

Strings 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 que T pode ser.
  • impl<T> Pair<T> adiciona métodos para todo T; 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

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