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
3em[i32; 3]. const fnpermite 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.PhantomDatapermite 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 incorrectO 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 165 itens custam 30Leia <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]]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][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.
Valores padrão, chaves e os limites de hoje
Seção intitulada “Valores padrão, chaves e os limites de hoje”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 16O 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, …),boolechar. Tentarconst NAME: &'static strdá o erro “&'static stris 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”). UmNpuro funciona, eN * 2também funciona dentro do corpo de uma função, ondeNé só um valor. O recurso que permitiriaN * 2em tipos (generic_const_exprs) ainda só existe no nightly. - Verificar um valor. Você não consegue escrever uma restrição como “
Ntem que ser maior que 0”. O mais próximo disso é um blococonstdentro de uma função:const { assert!(N > 0, "need at least one slot") };. Ele falha quando você compila o programa com umNinválido (ocargo checkpode não pegar).
const fn: deixe o compilador fazer as contas
Seção intitulada “const fn: deixe o compilador fazer as contas”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 casassquare(12) em tempo de execução dá 144Uma 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 6025 de dezembro é o dia 359Repare 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:
letelet mut, aritmética, comparações,if,match,loopewhile- 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, comou32::powouslice.len()) - referências
&mute contas com ponto flutuante (estáveis desde o Rust 1.83 e 1.82) panic!eassert!, 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 é Amaracancelando o pedido #7um Id ocupa 4 bytesOs 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 incorrectO 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”.
Unidades de medida: metros não são pés
Seção intitulada “Unidades de medida: metros não são pés”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 mesmaimpl<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 mtotal: 954.8 mEscreva 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 estadosimpl<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: abertaSala dos servidores: fechadaSala dos servidores: trancadaSala dos servidores fica trancada durante a noiteSala dos servidores: destrancadaSala dos servidores: abertaSala dos servidores está aberta de novoAgora 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.
Métodos para um só tamanho, e consts associadas
Seção intitulada “Métodos para um só tamanho, e consts associadas”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 Nimpl<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 2x2impl 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 -14grande: traço 15big.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 m29032.00 ft = 8848.95 m29032.00 ft = 5.50 miU::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.
Quando não usar essas ferramentas
Seção intitulada “Quando não usar essas ferramentas”Esses recursos são poderosos, e é justamente isso que os torna tentadores. Alguns conselhos sinceros:
- Um slice ou um
Vecmuitas 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 umVecde 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 numBox(veja Ponteiros inteligentes) ou use umVec. - Cada tamanho ou tipo novo é uma nova cópia do código. Usar um genérico com 20 valores diferentes de
Ngera 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.
Erros comuns
Seção intitulada “Erros comuns”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 variantsO 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` trait15 | show(id); | -- value moved here16 | 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 parametersO 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> {}Prática
Seção intitulada “Prática”1. Completar as pontuações
Seção intitulada “1. Completar as pontuações”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]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].
2. Tempos limite calculados na compilação
Seção intitulada “2. Tempos limite calculados na compilação”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 segundosesperas entre tentativas: [60, 300, 900]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.
3. Dinheiro que não se mistura
Seção intitulada “3. Dinheiro que não se mistura”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 EURingresso: 49,99 USDTente 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, eimpl<T, const N: usize>escreve métodos para todos os tamanhos. - No Rust estável, os parâmetros const podem ser inteiros,
boolouchar. Expressões como argumento precisam de chaves ({ PAGE * 2 }), e contas com umNgenérico dentro de um tipo ainda não são permitidas. - Uma
const fnpode rodar em tempo de compilação em contextos const (const,static, tamanhos de array). Usewhileem vez defor, e chame só outrasconst 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>eimpl<const N: usize> Matrix<N, N>dão métodos só a alguns tamanhos. Consts associadas comoU::IN_METERSdã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
Seção intitulada “Teste o que aprendeu”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
Para saber mais
Seção intitulada “Para saber mais”- Const generics — The Rust Reference (em inglês): as regras exatas dos parâmetros const, quais tipos são permitidos e quando você precisa de chaves.
- Const functions — The Rust Reference (em inglês): o que são contextos const e o que uma
const fnpode fazer. PhantomData— documentação da biblioteca padrão (em inglês): a explicação oficial, com exemplos de parâmetros de tipo não usados.- Phantom type parameters — Rust by Example (em inglês): exemplos curtos que você pode rodar, incluindo um estudo de caso com unidades de medida.
- Const generics MVP hits beta — Rust Blog (em inglês): o anúncio que explica por que os const generics foram criados e o que a primeira versão consegue fazer.