Pular para o conteúdo

Genéricos avançados em Rust: const generics, const fn e PhantomData

Na página de Genéricos, um parâmetro genérico era sempre um tipo: Pair<T> podia guardar i32 ou &str. Esta página mostra três ferramentas que levam essa mesma ideia mais longe:

  • Os const generics (genéricos constantes) permitem que um número seja um parâmetro genérico, como o 3 em [i32; 3].
  • const fn permite que o compilador rode a sua função enquanto monta o programa, então a resposta já está pronta antes de o programa começar.
  • PhantomData permite que um tipo carregue uma etiqueta (como “este é o ID de um usuário” ou “este comprimento está em pés”) sem guardar nenhum dado a mais.

Pense numa caixa de ovos. Uma caixa de 6 ovos e uma de 12 são produtos diferentes, mesmo que as duas sejam “caixas de ovos”. O tamanho faz parte do que elas são. Os const generics deixam você dizer isso em Rust. Vá com calma nesta página: cada seção se sustenta sozinha, e você pode voltar às últimas quando precisar delas.

Const generics: um número como parâmetro genérico

Seção intitulada “Const generics: um número como parâmetro genérico”

Em Rust, o tamanho de um array faz parte do seu tipo. [u32; 3] e [u32; 4] são tão diferentes quanto i32 e String. Uma função que recebe [u32; 3] não aceita quatro itens:

error[E0308]: mismatched types
--> src/main.rs:7:26
|
7 | println!("{}", total(basket));
| ----- ^^^^^^ expected an array with a size of 3, found one with a size of 4
| |
| arguments to this function are incorrect

O compilador avisa que esperava um array de tamanho 3 e encontrou um de tamanho 4. Um const generic resolve isso. Você escreve const N: usize entre os sinais de menor e maior, e a função passa a servir para um array de qualquer tamanho:

fn summary<const N: usize>(prices: [u32; N]) -> String {
let mut sum = 0;
for price in prices {
sum += price;
}
// Dentro da função, N é só um número comum
format!("{N} itens custam {sum}")
}
fn main() {
let small_basket = [4, 10, 2];
let big_basket = [3, 8, 1, 12, 6];
println!("{}", summary(small_basket));
println!("{}", summary(big_basket));
}

Saída:

3 itens custam 16
5 itens custam 30
▶ Experimente você mesmo

Leia <const N: usize> como “para qualquer número N do tipo usize”. O Rust descobre o N a partir do array que você passa, do mesmo jeito que descobre o T nos genéricos comuns. E, assim como nos genéricos comuns, o compilador cria uma cópia separada de summary para cada tamanho que você usa, então não há custo em tempo de execução.

Sendo honesto: para esta funçãozinha, um slice (prices: &[u32]) funcionaria tão bem quanto, e é mais simples. Veja Slices. Os const generics valem a pena de verdade quando o tamanho precisa fazer parte de um tipo: guardado numa struct sem alocação no heap, ou usado num tipo de retorno. É o que vem a seguir.

Structs com const generics: uma grade de poltronas

Seção intitulada “Structs com const generics: uma grade de poltronas”

Imagine o mapa de poltronas de um cinema: algumas fileiras, cada uma com o mesmo número de lugares. Aqui está uma matriz em que o número de linhas R e de colunas C fazem parte do tipo:

struct Matrix<const R: usize, const C: usize> {
cells: [[i32; C]; R],
}
impl<const R: usize, const C: usize> Matrix<R, C> {
fn new(cells: [[i32; C]; R]) -> Self {
Matrix { cells }
}
fn size(&self) -> String {
format!("{R}x{C}")
}
// Linhas viram colunas: uma matriz R x C vira uma matriz C x R
fn transpose(&self) -> Matrix<C, R> {
let mut cells = [[0; R]; C];
for row in 0..R {
for col in 0..C {
cells[col][row] = self.cells[row][col];
}
}
Matrix { cells }
}
}
fn main() {
let seats = Matrix::new([[1, 2, 3], [4, 5, 6]]);
let flipped = seats.transpose();
println!("{} {:?}", seats.size(), seats.cells);
println!("{} {:?}", flipped.size(), flipped.cells);
}

Saída:

2x3 [[1, 2, 3], [4, 5, 6]]
3x2 [[1, 4], [2, 5], [3, 6]]
▶ Experimente você mesmo

Repare no tipo de retorno de transpose: Matrix<C, R>. O compilador sabe que virar uma matriz 2x3 dá uma 3x2. Se você errasse alguma coisa lá dentro, como montar uma matriz com o formato errado, o código não compilaria. A grade inteira mora dentro da struct, sem Vec e sem heap. A linha impl<const R: usize, const C: usize> funciona como impl<T>: ela diz “estes métodos existem para todos os tamanhos”.

Um exemplo prático: um buffer circular de tamanho fixo

Seção intitulada “Um exemplo prático: um buffer circular de tamanho fixo”

Uma câmera veicular guarda só os últimos minutos de vídeo. Quando a memória enche, a gravação mais nova sobrescreve a mais antiga. Isso é um ring buffer (buffer circular), e ele combina perfeitamente com const generics: a capacidade é fixa, então dá para guardar os itens num array.

Este aqui é genérico em duas coisas, um tipo T e um tamanho N, por isso a linha do impl diz impl<T, const N: usize>:

struct RingBuffer<T, const N: usize> {
items: [T; N],
next: usize, // onde vai entrar o próximo push
len: usize, // quantos itens estão guardados (nunca mais que N)
}
impl<T: Copy + Default, const N: usize> RingBuffer<T, N> {
fn new() -> Self {
RingBuffer { items: [T::default(); N], next: 0, len: 0 }
}
fn push(&mut self, item: T) {
self.items[self.next] = item; // sobrescreve o item mais antigo quando está cheio
self.next = (self.next + 1) % N;
if self.len < N {
self.len += 1;
}
}
// Os itens guardados, do mais antigo para o mais novo
fn to_vec(&self) -> Vec<T> {
let start = (self.next + N - self.len) % N;
(0..self.len).map(|i| self.items[(start + i) % N]).collect()
}
}
fn main() {
// Guarda só as 3 últimas leituras de temperatura
let mut recent: RingBuffer<i32, 3> = RingBuffer::new();
for temp in [18, 19, 21, 24, 22] {
recent.push(temp);
println!("depois de {temp}: {:?}", recent.to_vec());
}
}

Saída:

depois de 18: [18]
depois de 19: [18, 19]
depois de 21: [18, 19, 21]
depois de 24: [19, 21, 24]
depois de 22: [21, 24, 22]
▶ Experimente você mesmo

[T::default(); N] preenche o array com N cópias de um valor padrão (0 para i32). Repetir um valor assim exige T: Copy, e é por isso que o impl pede Copy + Default. Um RingBuffer<i32, 3> e um RingBuffer<i32, 60> são tipos diferentes, então você nunca consegue passar um buffer de 3 posições onde se espera um de 60. Se você prefere a versão da biblioteca padrão, que cresce sozinha, veja VecDeque e BinaryHeap.

Um parâmetro const pode ter um valor padrão, escrito const N: usize = 4. Você também pode passar uma constante com nome, ou uma expressão entre chaves:

struct Buffer<const N: usize = 4> {
data: [u8; N],
}
impl<const N: usize> Buffer<N> {
fn new() -> Self {
Buffer { data: [0; N] }
}
}
const PAGE: usize = 8;
fn main() {
let small: Buffer = Buffer::new(); // sem número: o padrão, 4
let page = Buffer::<PAGE>::new(); // uma constante com nome
let double = Buffer::<{ PAGE * 2 }>::new(); // uma expressão precisa de { }
println!("{} {} {}", small.data.len(), page.data.len(), double.data.len());
}

Saída:

4 8 16
▶ Experimente você mesmo

O valor padrão só entra em ação quando você escreve o tipo sem número, como em let small: Buffer. Um let small = Buffer::new(); sozinho falha com error[E0284]: type annotations needed for Buffer<_> (“é preciso anotar o tipo”), porque o Rust não deduz um valor padrão a partir de uma expressão.

Os const generics no Rust estável são úteis, mas limitados de propósito. Vale a pena conhecer os limites antes de esbarrar neles:

  • Tipos permitidos: só inteiros (usize, u8, i32, …), bool e char. Tentar const NAME: &'static str dá o erro “&'static str is forbidden as the type of a const generic parameter” (esse tipo é proibido para um parâmetro const).
  • Nada de contas com um parâmetro genérico dentro de um tipo. [f32; N * 2] dentro de uma struct ou assinatura genérica é um erro (“generic parameters may not be used in const operations”). Um N puro funciona, e N * 2 também funciona dentro do corpo de uma função, onde N é só um valor. O recurso que permitiria N * 2 em tipos (generic_const_exprs) ainda só existe no nightly.
  • Verificar um valor. Você não consegue escrever uma restrição como “N tem que ser maior que 0”. O mais próximo disso é um bloco const dentro de uma função: const { assert!(N > 0, "need at least one slot") };. Ele falha quando você compila o programa com um N inválido (o cargo check pode não pegar).

Algumas lojas imprimem a tabela de preços uma vez só, em vez de calcular cada preço no caixa. Uma const fn é uma função que o compilador pode rodar enquanto monta o seu programa, então a resposta já vem “impressa” no programa com antecedência.

Você já conheceu os itens const na página de Variáveis. O valor deles precisa ser conhecido em tempo de compilação, então normalmente você só pode usar literais e contas simples. Com const fn, dá para chamar as suas próprias funções ali também:

const fn square(n: usize) -> usize {
n * n
}
const BOARD_SIDE: usize = 4;
const CELLS: usize = square(BOARD_SIDE); // calculado pelo compilador
fn main() {
// O tamanho de um array precisa ser conhecido na compilação, e CELLS é
let board = [0u8; CELLS];
println!("um tabuleiro {BOARD_SIDE}x{BOARD_SIDE} tem {} casas", board.len());
println!("square(12) em tempo de execução dá {}", square(12));
}

Saída:

um tabuleiro 4x4 tem 16 casas
square(12) em tempo de execução dá 144
▶ Experimente você mesmo

Uma const fn continua sendo uma função normal. square(12) dentro do main é uma chamada comum, em tempo de execução. Ela só precisa rodar na compilação quando é usada num contexto const: um item const ou static, o tamanho de um array, um argumento const generic ou um bloco const { ... }.

Um uso clássico é a tabela de consulta (lookup table): você calcula uma lista de valores uma vez, na compilação, e em tempo de execução só lê. Aqui o compilador monta a tabela “em que dia do ano começa cada mês” para um ano não bissexto:

const DAYS_IN_MONTH: [u32; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
const fn days_before_each_month() -> [u32; 12] {
let mut table = [0; 12];
let mut month = 1;
while month < 12 {
table[month] = table[month - 1] + DAYS_IN_MONTH[month - 1];
month += 1;
}
table
}
// Montada uma vez pelo compilador. Na execução, são só 12 números.
const DAYS_BEFORE: [u32; 12] = days_before_each_month();
fn day_of_year(month: usize, day: u32) -> u32 {
DAYS_BEFORE[month - 1] + day
}
fn main() {
println!("{:?}", DAYS_BEFORE);
println!("1º de março é o dia {}", day_of_year(3, 1));
println!("25 de dezembro é o dia {}", day_of_year(12, 25));
}

Saída:

[0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]
1º de março é o dia 60
25 de dezembro é o dia 359
▶ Experimente você mesmo

Repare no laço while no lugar de um for. Não é questão de estilo. Veja o que você pode usar dentro de uma const fn hoje:

  • let e let mut, aritmética, comparações, if, match, loop e while
  • arrays, tuplas, structs e enums, e indexação como table[month]
  • chamadas a outras const fn (muitas funções da biblioteca padrão são const, como u32::pow ou slice.len())
  • referências &mut e contas com ponto flutuante (estáveis desde o Rust 1.83 e 1.82)
  • panic! e assert!, que viram erros de compilação quando são atingidos na compilação

E o que você ainda não pode usar: laços for (eles precisam da trait Iterator, e métodos de trait não são const no Rust estável), chamadas a funções normais como String::from ou println!, e qualquer coisa que aloque no heap, como dar push num Vec.

const ou static? Os dois são valores globais que nunca mudam, mas são guardados de jeitos diferentes:

const static
Onde fica copiado em cada lugar onde você o usa um único lugar fixo na memória
Endereço pode mudar a cada uso sempre o mesmo, vive durante o programa inteiro ('static)
Pode ser mutável nunca static mut existe, mas exige unsafe
Use para números, tabelas pequenas, configurações uma tabela grande que você quer guardar uma vez só, ou um global do qual você precisa de uma referência

Na dúvida, use const. Se você precisa de um global que dá para alterar, use um static com um tipo atômico ou um Mutex dentro, em vez de static mut. Veja Estado compartilhado.

PhantomData: uma etiqueta que não dá para confundir

Seção intitulada “PhantomData: uma etiqueta que não dá para confundir”

Este é um bug que acontece o tempo todo em sistemas reais. IDs de usuário e IDs de pedido são só números, então nada impede você de passar um ID de usuário para cancel_order. O ideal seria o tipo carregar uma etiqueta: Id<User> versus Id<Order>. A sua primeira tentativa talvez seja esta:

struct Id<T> {
value: u32,
}
error[E0392]: type parameter `T` is never used
--> src/main.rs:1:11
|
1 | struct Id<T> {
| ^ unused type parameter
|
= help: consider removing `T`, referring to it in a field, or using a marker such as `PhantomData`

O Rust recusa um parâmetro de tipo que nenhum campo usa (“type parameter T is never used”). A solução é a que o próprio compilador sugere. std::marker::PhantomData<T> é um campo que diz “esta struct está ligada a T”, mas não guarda nada. Ele ocupa zero bytes:

use std::marker::PhantomData;
struct User;
struct Order;
struct Id<T> {
value: u32,
_marker: PhantomData<T>, // zero bytes, só uma etiqueta
}
impl<T> Id<T> {
fn new(value: u32) -> Self {
Id { value, _marker: PhantomData }
}
}
fn find_user(id: &Id<User>) -> &'static str {
if id.value == 7 { "Amara" } else { "desconhecido" }
}
fn cancel_order(id: &Id<Order>) {
println!("cancelando o pedido #{}", id.value);
}
fn main() {
let user_id: Id<User> = Id::new(7);
let order_id: Id<Order> = Id::new(7);
println!("o usuário #{} é {}", user_id.value, find_user(&user_id));
cancel_order(&order_id);
println!("um Id ocupa {} bytes", std::mem::size_of::<Id<User>>());
}

Saída:

o usuário #7 é Amara
cancelando o pedido #7
um Id ocupa 4 bytes
▶ Experimente você mesmo

Os dois IDs guardam o número 7, e os dois ocupam 4 bytes, exatamente como um u32 comum. Mas agora coloque cancel_order(&user_id); como última linha do main:

error[E0308]: mismatched types
--> src/main.rs:32:18
|
32 | cancel_order(&user_id);
| ------------ ^^^^^^^^ expected `&Id<Order>`, found `&Id<User>`
| |
| arguments to this function are incorrect

O compilador esperava um &Id<Order> e encontrou um &Id<User>: a confusão é pega antes de o programa sequer rodar. PhantomData é um valor além de ser um tipo, e é por isso que você escreve _marker: PhantomData ao montar a struct. O _ no começo é só um costume comum para dizer “este campo nunca é lido”.

Em 1999, a NASA perdeu a sonda Mars Climate Orbiter porque um software produzia números em unidades imperiais e outro esperava o sistema métrico. Os dois eram “só números”. Com PhantomData, a unidade passa a fazer parte do tipo, e um único Length<U> genérico serve para todas as unidades:

use std::marker::PhantomData;
use std::ops::Add;
struct Meters;
struct Feet;
struct Length<U> {
value: f64,
_unit: PhantomData<U>,
}
impl<U> Length<U> {
fn new(value: f64) -> Self {
Length { value, _unit: PhantomData }
}
}
// Um impl para todas as unidades, mas os dois lados precisam ter a mesma
impl<U> Add for Length<U> {
type Output = Length<U>;
fn add(self, other: Length<U>) -> Length<U> {
Length::new(self.value + other.value)
}
}
impl Length<Feet> {
fn to_meters(self) -> Length<Meters> {
Length::new(self.value * 0.3048)
}
}
fn main() {
let run: Length<Meters> = Length::new(400.0) + Length::new(250.0);
let climb: Length<Feet> = Length::new(1000.0);
let climb_m = climb.to_meters();
println!("subida: {:.1} m", climb_m.value);
let total = run + climb_m;
println!("total: {:.1} m", total.value);
}

Saída:

subida: 304.8 m
total: 954.8 m
▶ Experimente você mesmo

Escreva run + climb (esquecendo o to_meters()) e o Rust recusa:

error[E0308]: mismatched types
--> src/main.rs:39:23
|
39 | let total = run + climb;
| ^^^^^ expected `Length<Meters>`, found `Length<Feet>`

Ele esperava metros e encontrou pés. Na página de Traits avançados, Meters e Centimeters eram duas structs separadas, cada uma com o seu próprio impl de Add. Aqui, um único impl<U> Add for Length<U> atende qualquer unidade que você for somar.

Typestate: uma porta que não pode ser trancada enquanto está aberta

Seção intitulada “Typestate: uma porta que não pode ser trancada enquanto está aberta”

O mesmo truque serve para acompanhar o estado de alguma coisa. Esse padrão se chama typestate (estado no tipo). Uma porta pode estar aberta, fechada ou trancada. Dá para trancar uma porta fechada, mas trancar uma porta aberta não faz sentido. Com uma etiqueta de estado no tipo, as mudanças inválidas simplesmente não existem:

use std::marker::PhantomData;
struct Open;
struct Closed;
struct Locked;
struct Door<State> {
room: String,
_state: PhantomData<State>,
}
// Disponível em todos os estados
impl<State> Door<State> {
fn room(&self) -> &str {
&self.room
}
// A mesma porta, com uma nova etiqueta de estado
fn into_state<Next>(self, action: &str) -> Door<Next> {
println!("{}: {action}", self.room);
Door { room: self.room, _state: PhantomData }
}
}
impl Door<Closed> {
fn new(room: &str) -> Self {
Door { room: room.to_string(), _state: PhantomData }
}
fn open(self) -> Door<Open> {
self.into_state("aberta")
}
fn lock(self) -> Door<Locked> {
self.into_state("trancada")
}
}
impl Door<Open> {
fn close(self) -> Door<Closed> {
self.into_state("fechada")
}
}
impl Door<Locked> {
fn unlock(self) -> Door<Closed> {
self.into_state("destrancada")
}
}
fn main() {
let door = Door::new("Sala dos servidores").open().close().lock();
println!("{} fica trancada durante a noite", door.room());
let door = door.unlock().open();
println!("{} está aberta de novo", door.room());
}

Saída:

Sala dos servidores: aberta
Sala dos servidores: fechada
Sala dos servidores: trancada
Sala dos servidores fica trancada durante a noite
Sala dos servidores: destrancada
Sala dos servidores: aberta
Sala dos servidores está aberta de novo
▶ Experimente você mesmo

Agora tente trancar uma porta aberta, Door::new("Sala dos servidores").open().lock():

error[E0599]: no method named `lock` found for struct `Door<Open>` in the current scope
--> src/main.rs:50:56
|
7 | struct Door<State> {
| ------------------ method `lock` not found for this struct
...
50 | let door = Door::new("Sala dos servidores").open().lock();
| ^^^^ method not found in `Door<Open>`
|
= note: the method was found for `Door<Closed>`

A mensagem diz que não existe um método lock para Door<Open>, e a nota acrescenta que esse método existe, sim, para Door<Closed>. Cada método recebe self por valor, então a porta antiga é consumida e você não consegue continuar usando uma porta “fechada” depois de abri-la. A página de Padrões de projeto mostra a mesma ideia de typestate com estados que também guardam os seus próprios dados. PhantomData é a versão mais leve, para quando os estados são só etiquetas.

Você viu impl Pair<f64> na página de Genéricos: métodos só para um tipo concreto. O mesmo vale para const generics. Alguns métodos só fazem sentido para certos tamanhos:

struct Matrix<const R: usize, const C: usize> {
cells: [[i32; C]; R],
}
// Só matrizes quadradas: linhas e colunas são o mesmo N
impl<const N: usize> Matrix<N, N> {
fn trace(&self) -> i32 {
let mut sum = 0;
for i in 0..N {
sum += self.cells[i][i];
}
sum
}
}
// Só matrizes 2x2
impl Matrix<2, 2> {
fn determinant(&self) -> i32 {
let [[a, b], [c, d]] = self.cells;
a * d - b * c
}
}
fn main() {
let small = Matrix { cells: [[3, 8], [4, 6]] };
let big = Matrix { cells: [[1, 2, 3], [4, 5, 6], [7, 8, 9]] };
println!("pequena: traço {}, determinante {}", small.trace(), small.determinant());
println!("grande: traço {}", big.trace());
}

Saída:

pequena: traço 9, determinante -14
grande: traço 15
▶ Experimente você mesmo

big.determinant() não compila, e trace() numa matriz 2x3 também não: você recebe erros como error[E0599]: no method named trace found for struct Matrix<2, 3> (não existe o método trace para Matrix<2, 3>), com a nota the method was found for Matrix<N, N>. A regra “só matrizes quadradas têm traço” agora está escrita nos tipos.

Uma const associada (associated const) é uma constante que pertence a um tipo ou a uma trait, do mesmo jeito que um método. Numa trait, cada implementação escolhe o seu próprio valor, e o código genérico pode lê-lo como U::NAME. Vamos dar às unidades de antes um símbolo e um tamanho:

use std::fmt;
use std::marker::PhantomData;
trait Unit {
const SYMBOL: &'static str;
const IN_METERS: f64; // quantos metros tem uma desta unidade
}
struct Meters;
struct Feet;
struct Miles;
impl Unit for Meters {
const SYMBOL: &'static str = "m";
const IN_METERS: f64 = 1.0;
}
impl Unit for Feet {
const SYMBOL: &'static str = "ft";
const IN_METERS: f64 = 0.3048;
}
impl Unit for Miles {
const SYMBOL: &'static str = "mi";
const IN_METERS: f64 = 1609.344;
}
struct Length<U> {
value: f64,
_unit: PhantomData<U>,
}
impl<U: Unit> Length<U> {
fn new(value: f64) -> Self {
Length { value, _unit: PhantomData }
}
// Funciona entre quaisquer duas unidades, usando as constantes delas
fn convert<To: Unit>(&self) -> Length<To> {
Length::new(self.value * U::IN_METERS / To::IN_METERS)
}
}
impl<U: Unit> fmt::Display for Length<U> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:.2} {}", self.value, U::SYMBOL)
}
}
fn main() {
let marathon: Length<Miles> = Length::new(26.2);
let everest: Length<Feet> = Length::new(29032.0);
println!("{} = {}", marathon, marathon.convert::<Meters>());
println!("{} = {}", everest, everest.convert::<Meters>());
println!("{} = {}", everest, everest.convert::<Miles>());
}

Saída:

26.20 mi = 42164.81 m
29032.00 ft = 8848.95 m
29032.00 ft = 5.50 mi
▶ Experimente você mesmo

U::IN_METERS é resolvido em tempo de compilação, então não existe tabela nenhuma para procurar durante a execução. Adicionar uma unidade nova, digamos Kilometers, é só um bloquinho impl Unit, e convert e Display já funcionam com ela na hora. Um impl comum (inerente) também pode ter consts associadas, por exemplo impl<T, const N: usize> RingBuffer<T, N> { const CAPACITY: usize = N; }, lida como RingBuffer::<i32, 3>::CAPACITY.

Se você quer uma implementação de trait para todo tipo que cumpre uma condição, isso é um blanket impl, explicado na página de Traits avançados.

Esses recursos são poderosos, e é justamente isso que os torna tentadores. Alguns conselhos sinceros:

  • Um slice ou um Vec muitas vezes basta. Se uma função só lê os itens, &[T] aceita qualquer tamanho e é mais fácil de ler. Se o tamanho só é conhecido em tempo de execução (um arquivo, a entrada do usuário), você vai precisar de um Vec de qualquer jeito.
  • Arrays grandes ficam na stack (pilha). Um RingBuffer<u8, 10_000_000> são dez megabytes dentro da struct, o que pode estourar a pilha. Coloque-o num Box (veja Ponteiros inteligentes) ou use um Vec.
  • Cada tamanho ou tipo novo é uma nova cópia do código. Usar um genérico com 20 valores diferentes de N gera 20 versões. Isso custa tempo de compilação e tamanho do binário. Veja Desempenho.
  • Typestate acrescenta tipos e mensagens de erro mais longas. Ele é ótimo para uma API que muita gente vai usar (um builder, uma conexão, um fluxo de pagamento). Para um estado que muda conforme dados de execução, como um status carregado de um banco de dados, use um enum e match.

Uma boa regra: comece simples, e recorra a essas ferramentas quando aparecer um bug real ou um custo real que elas evitariam.

Fazer contas com um parâmetro const dentro de um tipo. Parece natural, mas o Rust estável ainda não permite:

struct Pairs<const N: usize> {
values: [f32; N * 2],
}
error: generic parameters may not be used in const operations
--> src/main.rs:2:19
|
2 | values: [f32; N * 2],
| ^ cannot perform const operation using `N`
|
= help: const parameters may only be used as standalone arguments here, i.e. `N`

A mensagem diz que parâmetros genéricos não podem ser usados em operações const, e que aqui o N só pode aparecer sozinho. Soluções: use [[f32; 2]; N] (um array de pares), acrescente um segundo parâmetro, ou use um Vec se o tamanho não precisa estar no tipo.

Esquecer as chaves em volta de uma expressão. Buffer::<2 + 2>::new() dá error: expressions must be enclosed in braces to be used as const generic arguments (expressões precisam estar entre chaves), e a linha de ajuda mostra a correção: Buffer::<{ 2 + 2 }>::new(). Um número puro ou o nome de uma const não precisam de chaves.

Chamar algo que não é const num contexto const. Uma const não pode chamar uma função normal, e uma const fn não pode usar um laço for:

const GREETING: String = String::from("hello");
error[E0015]: cannot call non-const associated function `<String as From<&str>>::from` in constants
--> src/main.rs:1:26
|
1 | const GREETING: String = String::from("hello");
| ^^^^^^^^^^^^^^^^^^^^^
|
= note: calls in constants are limited to constant functions, tuple structs and tuple variants

O Rust avisa que String::from não é uma função const, e que dentro de constantes só dá para chamar funções const. Para texto, use const GREETING: &str = "hello";. Dentro de uma const fn, troque o for por um laço while com um índice, como na tabela de consulta lá em cima.

#[derive(Clone, Copy)] numa struct com PhantomData. Você esperaria que Id<User> fosse Copy, já que ele só guarda um u32. Mas o derive acrescenta a restrição T: Copy, e User não é Copy, então o ID é movido em vez de copiado:

error[E0382]: use of moved value: `id`
--> src/main.rs:16:10
|
14 | let id: Id<User> = Id { value: 1, _marker: PhantomData };
| -- move occurs because `id` has type `Id<User>`, which does not implement the `Copy` trait
15 | show(id);
| -- value moved here
16 | show(id);
| ^^ value used here after move
|
note: if all bounds were met, you could clone the value
--> src/main.rs:4:1
|
3 | #[derive(Clone, Copy)]
| ----- derived `Clone` adds implicit bounds on type parameters

O erro diz que id foi movido na primeira chamada a show e depois usado de novo, e a nota explica que o Clone derivado acrescenta restrições aos parâmetros de tipo. A correção mais simples é derivar Clone, Copy também nos tipos marcadores (#[derive(Clone, Copy)] struct User;). Ou escreva os impls à mão, o que não exige nenhuma restrição sobre T:

impl<T> Clone for Id<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for Id<T> {}

Escreva fn pad_scores<const N: usize>(scores: &[u32]) -> [u32; N], que copia as pontuações para um array de exatamente N itens. Preencha as posições que faltarem com 0 e descarte as pontuações que sobrarem. Chame como pad_scores::<5>(&[90, 85]) e como let top3: [u32; 3] = pad_scores(&[70, 95, 88, 60]);. Dica: comece com [0; N], depois percorra 0..N e use scores.get(i).

Ver solução
fn pad_scores<const N: usize>(scores: &[u32]) -> [u32; N] {
let mut out = [0; N];
for i in 0..N {
if let Some(&score) = scores.get(i) {
out[i] = score;
}
}
out
}
fn main() {
let five = pad_scores::<5>(&[90, 85]);
let top3: [u32; 3] = pad_scores(&[70, 95, 88, 60]);
println!("{:?}", five);
println!("{:?}", top3);
}

Saída:

[90, 85, 0, 0, 0]
[70, 95, 88]
▶ Experimente você mesmo

Aqui o N não pode vir do argumento (um slice não tem o tamanho no tipo), então ele vem do turbofish ::<5> ou do tipo que você pede, [u32; 3].

Escreva duas const fn, minutes(n: u64) -> u64 e hours(n: u64) -> u64, que devolvem um número de segundos. Use-as para definir const SESSION_TIMEOUT: u64 = hours(1) + minutes(30); e um array const RETRY_DELAYS: [u64; 3] com 1, 5 e 15 minutos. Imprima os dois. Dica: hours pode chamar minutes.

Ver solução
const fn minutes(n: u64) -> u64 {
n * 60
}
const fn hours(n: u64) -> u64 {
minutes(n * 60)
}
const SESSION_TIMEOUT: u64 = hours(1) + minutes(30);
const RETRY_DELAYS: [u64; 3] = [minutes(1), minutes(5), minutes(15)];
fn main() {
println!("tempo limite da sessão: {SESSION_TIMEOUT} segundos");
println!("esperas entre tentativas: {:?}", RETRY_DELAYS);
}

Saída:

tempo limite da sessão: 5400 segundos
esperas entre tentativas: [60, 300, 900]
▶ Experimente você mesmo

O compilador calcula o 5400 e a tabela de esperas durante a compilação, e hours(1) + minutes(30) é muito mais fácil de ler do que um 5400 solto. Se você tirar o const de minutes, os itens const deixam de compilar com o erro E0015.

Crie struct Euro; e struct Dollar;, e um Money<C> com um campo cents: i64 e um campo PhantomData<C>. Dê a ele new(cents) e plus(self, other: Money<C>) -> Money<C>. Some um almoço e um café em euros e imprima o total. Dica: como other tem o mesmo C, não dá para somar um Money<Euro> com um Money<Dollar>.

Ver solução
use std::marker::PhantomData;
struct Euro;
struct Dollar;
struct Money<C> {
cents: i64,
_currency: PhantomData<C>,
}
impl<C> Money<C> {
fn new(cents: i64) -> Self {
Money { cents, _currency: PhantomData }
}
fn plus(self, other: Money<C>) -> Money<C> {
Money::new(self.cents + other.cents)
}
}
fn main() {
let lunch: Money<Euro> = Money::new(1250);
let coffee: Money<Euro> = Money::new(320);
let ticket: Money<Dollar> = Money::new(4999);
let total = lunch.plus(coffee);
println!("total: {},{:02} EUR", total.cents / 100, total.cents % 100);
println!("ingresso: {},{:02} USD", ticket.cents / 100, ticket.cents % 100);
}

Saída:

total: 15,70 EUR
ingresso: 49,99 USD
▶ Experimente você mesmo

Tente lunch.plus(ticket): falha com expected Money<Euro>, found Money<Dollar> (esperava euros e encontrou dólares). Guardar os centavos como inteiro em vez de f64 também evita erros de arredondamento com dinheiro.

  • Os const generics (const N: usize) fazem de um número parte de um tipo. Arrays de tamanhos diferentes são tipos diferentes, e impl<T, const N: usize> escreve métodos para todos os tamanhos.
  • No Rust estável, os parâmetros const podem ser inteiros, bool ou char. Expressões como argumento precisam de chaves ({ PAGE * 2 }), e contas com um N genérico dentro de um tipo ainda não são permitidas.
  • Uma const fn pode rodar em tempo de compilação em contextos const (const, static, tamanhos de array). Use while em vez de for, e chame só outras const fn.
  • PhantomData<T> é um campo de tamanho zero que liga uma struct a um tipo que ela não guarda. Use-o para IDs com tipo, unidades de medida e typestate.
  • impl Matrix<2, 2> e impl<const N: usize> Matrix<N, N> dão métodos só a alguns tamanhos. Consts associadas como U::IN_METERS dão a cada tipo o seu próprio valor em tempo de compilação.
  • Essas ferramentas custam tempo de compilação e legibilidade. Use-as quando elas evitarem um bug de verdade.

Teste o que aprendeu

1. Por que você não pode passar um [u32; 4] para uma função que recebe [u32; 3]?

2. O que PhantomData<T> acrescenta a uma struct em tempo de execução?

3. Qual destes você NÃO pode usar dentro de uma const fn no Rust estável?

4. O que significa impl<const N: usize> Matrix<N, N>?

Pontuação: 0 / 4