Generici avanzati in Rust: const generics, const fn e PhantomData
Nella pagina sui generici, un parametro generico era sempre un tipo: Pair<T> poteva contenere degli i32 o degli &str. Questa pagina mostra tre strumenti che portano la stessa idea più in là:
- I const generics permettono a un numero di essere un parametro generico, come il
3in[i32; 3]. const fnpermette al compilatore di eseguire la tua funzione mentre costruisce il programma, così il risultato è pronto prima ancora che il programma parta.PhantomDatapermette a un tipo di portare un’etichetta (come «questo è l’ID di un utente» o «questa lunghezza è in piedi») senza memorizzare nessun dato in più.
Pensa ai contenitori per le uova. Una confezione da 6 e una da 12 sono prodotti diversi, anche se sono entrambe «confezioni di uova». La dimensione fa parte di ciò che sono. I const generics ti permettono di dirlo in Rust. Prendi questa pagina con calma: ogni sezione sta in piedi da sola, e puoi tornare alle ultime quando ti serviranno.
Const generics: un numero come parametro generico
Sezione intitolata “Const generics: un numero come parametro generico”In Rust la lunghezza di un array fa parte del suo tipo. [u32; 3] e [u32; 4] sono diversi quanto i32 e String. Una funzione che prende [u32; 3] non accetta quattro elementi:
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 incorrectIl compilatore dice che si aspettava un array di 3 elementi e ne ha trovato uno di 4. Un const generic risolve il problema. Scrivi const N: usize tra le parentesi angolari, e la funzione funziona per un array di qualsiasi lunghezza:
fn summary<const N: usize>(prices: [u32; N]) -> String { let mut sum = 0; for price in prices { sum += price; } // Dentro la funzione, N è solo un numero normale format!("{N} articoli costano {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));}Output:
3 articoli costano 165 articoli costano 30Leggi <const N: usize> come «per qualsiasi numero N di tipo usize». Rust ricava N dall’array che passi, proprio come ricava T per i generici normali. E proprio come con i generici normali, il compilatore crea una copia separata di summary per ogni lunghezza che usi, quindi non c’è nessun costo durante l’esecuzione.
A essere sinceri: per questa piccola funzione una slice (prices: &[u32]) andrebbe altrettanto bene ed è più semplice. Vedi Slice. I const generics ripagano davvero quando la lunghezza deve far parte di un tipo: salvata in una struct senza allocazioni nell’heap, oppure usata in un tipo di ritorno. È il prossimo passo.
Struct con const generics: una griglia di posti
Sezione intitolata “Struct con const generics: una griglia di posti”Immagina la piantina dei posti di un cinema: alcune file, ognuna con lo stesso numero di posti. Ecco una matrice in cui il numero di righe R e di colonne C fanno entrambi parte del 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}") }
// Le righe diventano colonne: una matrice R x C diventa una matrice 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);}Output:
2x3 [[1, 2, 3], [4, 5, 6]]3x2 [[1, 4], [2, 5], [3, 6]]Guarda il tipo di ritorno di transpose: Matrix<C, R>. Il compilatore sa che girando una matrice 2x3 si ottiene una 3x2. Se sbagliassi qualcosa all’interno, per esempio costruendo una matrice della forma sbagliata, non compilerebbe. L’intera griglia vive dentro la struct, senza Vec e senza heap. La riga impl<const R: usize, const C: usize> funziona come impl<T>: dice «questi metodi esistono per ogni dimensione».
Un esempio pratico: un buffer circolare di dimensione fissa
Sezione intitolata “Un esempio pratico: un buffer circolare di dimensione fissa”Una dashcam in auto conserva solo gli ultimi minuti di video. Quando è piena, la registrazione più recente sovrascrive la più vecchia. Questo è un buffer circolare (ring buffer), ed è perfetto per i const generics: la capacità è fissa, quindi puoi conservare gli elementi in un array.
Questo è generico sia su un tipo T sia su una dimensione N, quindi la riga dell’impl dice impl<T, const N: usize>:
struct RingBuffer<T, const N: usize> { items: [T; N], next: usize, // dove va il prossimo push len: usize, // quanti elementi sono salvati (mai più di 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; // quando è pieno, sovrascrive l'elemento più vecchio self.next = (self.next + 1) % N; if self.len < N { self.len += 1; } }
// Gli elementi salvati, dal più vecchio 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() { // Tieni solo le ultime 3 letture della temperatura let mut recent: RingBuffer<i32, 3> = RingBuffer::new();
for temp in [18, 19, 21, 24, 22] { recent.push(temp); println!("dopo {temp}: {:?}", recent.to_vec()); }}Output:
dopo 18: [18]dopo 19: [18, 19]dopo 21: [18, 19, 21]dopo 24: [19, 21, 24]dopo 22: [21, 24, 22][T::default(); N] riempie l’array con N copie di un valore predefinito (0 per i32). Ripetere un valore in questo modo richiede T: Copy, ed è per questo che l’impl chiede Copy + Default. Un RingBuffer<i32, 3> e un RingBuffer<i32, 60> sono tipi diversi, quindi non puoi mai passare un buffer da 3 posti dove ne serve uno da 60. Se invece vuoi la versione espandibile della libreria standard, vedi VecDeque e BinaryHeap.
Valori predefiniti, graffe e i limiti di oggi
Sezione intitolata “Valori predefiniti, graffe e i limiti di oggi”Un parametro const può avere un valore predefinito, scritto const N: usize = 4. Puoi anche passare una costante con un nome, oppure un’espressione tra parentesi graffe:
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(); // nessun numero: il predefinito, 4 let page = Buffer::<PAGE>::new(); // una costante con un nome let double = Buffer::<{ PAGE * 2 }>::new(); // un'espressione ha bisogno di { }
println!("{} {} {}", small.data.len(), page.data.len(), double.data.len());}Output:
4 8 16Il valore predefinito entra in gioco solo quando scrivi il tipo senza un numero, come in let small: Buffer. Un semplice let small = Buffer::new(); fallisce con error[E0284]: type annotations needed for Buffer<_>, perché Rust non ricava un predefinito da un’espressione.
Nel Rust stabile i const generics sono utili, ma volutamente limitati. Conviene conoscerne i confini prima di sbatterci contro:
- Tipi ammessi: solo interi (
usize,u8,i32, …),boolechar. Provareconst NAME: &'static strdà «&'static stris forbidden as the type of a const generic parameter». - Niente calcoli con un parametro generico in un tipo.
[f32; N * 2]dentro una struct o una firma generica è un errore («generic parameters may not be used in const operations»).Nda solo va bene, e va bene ancheN * 2dentro il corpo di una funzione, doveNè solo un valore. La funzionalità che permetterebbeN * 2nei tipi (generic_const_exprs) è ancora solo nightly. - Controllare un valore. Non puoi scrivere un vincolo come «
Ndeve essere maggiore di 0». Il massimo che puoi fare è un bloccoconstinline in una funzione:const { assert!(N > 0, "need at least one slot") };. Fallisce quando compili il programma con unNsbagliato (cargo checkpotrebbe non accorgersene).
const fn: lascia fare i conti al compilatore
Sezione intitolata “const fn: lascia fare i conti al compilatore”Alcuni negozi stampano il listino una volta sola, invece di calcolare ogni prezzo alla cassa. Una const fn è una funzione che il compilatore può eseguire mentre costruisce il programma, così il risultato è «stampato» nel programma in anticipo.
Hai già incontrato gli elementi const nella pagina sulle variabili. Il loro valore deve essere noto in fase di compilazione, quindi di solito puoi usare solo letterali e calcoli semplici. Con const fn lì puoi chiamare anche le tue funzioni:
const fn square(n: usize) -> usize { n * n}
const BOARD_SIDE: usize = 4;const CELLS: usize = square(BOARD_SIDE); // calcolato dal compilatore
fn main() { // La lunghezza degli array deve essere nota in compilazione, e CELLS lo è let board = [0u8; CELLS];
println!("una scacchiera {BOARD_SIDE}x{BOARD_SIDE} ha {} caselle", board.len()); println!("square(12) durante l'esecuzione vale {}", square(12));}Output:
una scacchiera 4x4 ha 16 casellesquare(12) durante l'esecuzione vale 144Una const fn resta comunque una funzione normale. square(12) in main è una normale chiamata durante l’esecuzione. Deve girare in fase di compilazione solo quando viene usata in un contesto const: un elemento const o static, la lunghezza di un array, un argomento const generic o un blocco const { ... }.
Un uso classico è una tabella di consultazione (lookup table): calcoli un elenco di valori una volta, in fase di compilazione, e durante l’esecuzione ti limiti a leggerlo. Qui il compilatore costruisce la tabella «in che giorno dell’anno comincia ogni mese» per un anno non bisestile:
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}
// Costruita una volta dal compilatore. Durante l'esecuzione sono solo 12 numeri.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!("il 1° marzo è il giorno {}", day_of_year(3, 1)); println!("il 25 dicembre è il giorno {}", day_of_year(12, 25));}Output:
[0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]il 1° marzo è il giorno 60il 25 dicembre è il giorno 359Nota il ciclo while invece di un ciclo for. Non è una questione di stile. Ecco cosa puoi usare oggi dentro una const fn:
letelet mut, aritmetica, confronti,if,match,loopewhile- array, tuple, struct ed enum, e indicizzazioni come
table[month] - chiamate ad altre
const fn(molte funzioni standard sono const, comeu32::powoslice.len()) - riferimenti
&mute calcoli in virgola mobile (stabili da Rust 1.83 e 1.82) panic!eassert!, che diventano errori di compilazione quando scattano in fase di compilazione
E cosa non puoi ancora usare: i cicli for (hanno bisogno del trait Iterator, e nel Rust stabile i metodi dei trait non sono const), le chiamate a funzioni normali come String::from o println!, e qualsiasi cosa allochi nell’heap, come fare push in un Vec.
const o static? Sono entrambi valori globali che non cambiano mai, ma vengono memorizzati in modo diverso:
const |
static |
|
|---|---|---|
| Dove vive | copiato in ogni punto in cui lo usi | un unico posto fisso in memoria |
| Indirizzo | può cambiare ogni volta | sempre lo stesso, vive per tutto il programma ('static) |
| Può essere mutabile | mai | esiste static mut, ma richiede unsafe |
| Usalo per | numeri, piccole tabelle, impostazioni | una tabella grande da memorizzare una volta sola, o un globale di cui ti serve un riferimento |
Nel dubbio, usa const. Se ti serve un globale che puoi modificare, usa uno static con dentro un atomico o un Mutex invece di static mut. Vedi Stato condiviso.
PhantomData: un’etichetta che non puoi confondere
Sezione intitolata “PhantomData: un’etichetta che non puoi confondere”Ecco un bug che capita di continuo nei sistemi reali. Gli ID degli utenti e gli ID degli ordini sono entrambi solo numeri, quindi niente ti impedisce di passare l’ID di un utente a cancel_order. Vorresti che il tipo portasse un’etichetta: Id<User> contro Id<Order>. Il tuo primo tentativo potrebbe essere questo:
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`Rust rifiuta un parametro di tipo che nessun campo usa. La soluzione è quella che suggerisce il compilatore. std::marker::PhantomData<T> è un campo che dice «questa struct è collegata a T» ma non contiene niente. Occupa zero byte:
use std::marker::PhantomData;
struct User;struct Order;
struct Id<T> { value: u32, _marker: PhantomData<T>, // zero byte, solo un'etichetta}
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 { "sconosciuto" }}
fn cancel_order(id: &Id<Order>) { println!("annullo l'ordine #{}", id.value);}
fn main() { let user_id: Id<User> = Id::new(7); let order_id: Id<Order> = Id::new(7);
println!("l'utente #{} è {}", user_id.value, find_user(&user_id)); cancel_order(&order_id); println!("un Id occupa {} byte", std::mem::size_of::<Id<User>>());}Output:
l'utente #7 è Amaraannullo l'ordine #7un Id occupa 4 byteEntrambi gli ID contengono il numero 7, ed entrambi occupano 4 byte, esattamente come un semplice u32. Ma ora aggiungi cancel_order(&user_id); come ultima riga di 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 incorrectIl compilatore si aspettava un &Id<Order> e ha trovato un &Id<User>: lo scambio viene scoperto prima ancora che il programma parta. PhantomData è sia un valore sia un tipo, ed è per questo che scrivi _marker: PhantomData quando costruisci la struct. Il _ iniziale è solo un’abitudine comune per dire «questo campo non lo leggi mai».
Unità di misura: i metri non sono piedi
Sezione intitolata “Unità di misura: i metri non sono piedi”Nel 1999 la NASA ha perso il Mars Climate Orbiter perché un software produceva numeri in unità imperiali e un altro si aspettava unità metriche. Erano entrambi «solo numeri». Con PhantomData, l’unità diventa parte del tipo, e un unico Length<U> generico copre tutte le unità:
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 } }}
// Un solo impl per ogni unità, ma i due lati devono usare la stessa unità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!("salita: {:.1} m", climb_m.value);
let total = run + climb_m; println!("totale: {:.1} m", total.value);}Output:
salita: 304.8 mtotale: 954.8 mScrivi run + climb (dimenticando to_meters()) e Rust rifiuta:
error[E0308]: mismatched types --> src/main.rs:39:23 |39 | let total = run + climb; | ^^^^^ expected `Length<Meters>`, found `Length<Feet>`Nella pagina sui trait avanzati, Meters e Centimeters erano due struct separate, ognuna con il suo impl di Add. Qui un solo impl<U> Add for Length<U> serve per tutte le unità che sommerai.
Typestate: una porta che non si può chiudere a chiave se è aperta
Sezione intitolata “Typestate: una porta che non si può chiudere a chiave se è aperta”Lo stesso trucco può tenere traccia dello stato di qualcosa. Una porta può essere aperta, chiusa o chiusa a chiave. Puoi chiudere a chiave una porta chiusa, ma chiudere a chiave una porta aperta non ha senso. Con un’etichetta di stato nel tipo, le mosse non valide semplicemente non esistono:
use std::marker::PhantomData;
struct Open;struct Closed;struct Locked;
struct Door<State> { room: String, _state: PhantomData<State>,}
// Disponibile in ogni statoimpl<State> Door<State> { fn room(&self) -> &str { &self.room }
// Stessa porta, nuova etichetta di stato 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("aperta") } fn lock(self) -> Door<Locked> { self.into_state("chiusa a chiave") }}
impl Door<Open> { fn close(self) -> Door<Closed> { self.into_state("chiusa") }}
impl Door<Locked> { fn unlock(self) -> Door<Closed> { self.into_state("sbloccata") }}
fn main() { let door = Door::new("sala server").open().close().lock(); println!("{} è chiusa a chiave per la notte", door.room());
let door = door.unlock().open(); println!("{} è di nuovo aperta", door.room());}Output:
sala server: apertasala server: chiusasala server: chiusa a chiavesala server è chiusa a chiave per la nottesala server: sbloccatasala server: apertasala server è di nuovo apertaOra prova a chiudere a chiave una porta aperta, Door::new("sala server").open().lock():
error[E0599]: no method named `lock` found for struct `Door<Open>` in the current scope --> src/main.rs:50:48 |50 | let door = Door::new("sala server").open().lock(); | ^^^^ method not found in `Door<Open>` | = note: the method was found for `Door<Closed>`Il compilatore non trova nessun metodo lock per Door<Open>, e nota che il metodo esiste per Door<Closed>. Ogni metodo prende self per valore, quindi la vecchia porta viene consumata e non puoi continuare a usare una porta «chiusa» dopo averla aperta. La pagina sui design pattern mostra la stessa idea di typestate, in cui ogni stato contiene anche dati suoi. PhantomData è la versione leggera per quando gli stati sono pure etichette.
Metodi per una sola dimensione e costanti associate
Sezione intitolata “Metodi per una sola dimensione e costanti associate”Nella pagina sui generici hai visto impl Pair<f64>: metodi per un solo tipo concreto. Lo stesso funziona con i const generics. Alcuni metodi hanno senso solo per certe dimensioni:
struct Matrix<const R: usize, const C: usize> { cells: [[i32; C]; R],}
// Solo matrici quadrate: righe e colonne sono lo stesso 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 }}
// Solo matrici 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!("piccola: traccia {}, determinante {}", small.trace(), small.determinant()); println!("grande: traccia {}", big.trace());}Output:
piccola: traccia 9, determinante -14grande: traccia 15big.determinant() non compila, e nemmeno trace() su una matrice 2x3: ottieni errori come error[E0599]: no method named trace found for struct Matrix<2, 3>, con la nota the method was found for Matrix<N, N>. La regola «solo le matrici quadrate hanno una traccia» ora è scritta nei tipi.
Una costante associata è una costante che appartiene a un tipo o a un trait, come un metodo. In un trait, ogni implementazione sceglie il proprio valore, e il codice generico può leggerlo come U::NOME. Diamo alle unità di prima un simbolo e una dimensione:
use std::fmt;use std::marker::PhantomData;
trait Unit { const SYMBOL: &'static str; const IN_METERS: f64; // quanti metri vale una di queste unità}
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 } }
// Funziona tra due unità qualsiasi, usando le loro costanti 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>());}Output:
26.20 mi = 42164.81 m29032.00 ft = 8848.95 m29032.00 ft = 5.50 miU::IN_METERS viene letto in fase di compilazione, quindi durante l’esecuzione non c’è nessuna tabella in cui cercare. Aggiungere una nuova unità, per esempio Kilometers, richiede un piccolo blocco impl Unit, e convert e Display funzionano subito anche per lei. Anche un impl inerente può avere costanti associate, per esempio impl<T, const N: usize> RingBuffer<T, N> { const CAPACITY: usize = N; }, che si legge come RingBuffer::<i32, 3>::CAPACITY.
Se vuoi un’unica implementazione di un trait per ogni tipo che soddisfa una condizione, quella è una blanket impl, trattata nella pagina sui trait avanzati.
Quando non usare questi strumenti
Sezione intitolata “Quando non usare questi strumenti”Queste funzionalità sono potenti, e proprio per questo invitanti. Qualche consiglio sincero:
- Spesso basta una slice o un
Vec. Se una funzione legge soltanto gli elementi,&[T]accetta qualsiasi lunghezza ed è più facile da leggere. Se la lunghezza si conosce solo durante l’esecuzione (un file, l’input dell’utente), ti serve comunque unVec. - Gli array grandi vivono nello stack. Un
RingBuffer<u8, 10_000_000>sono dieci megabyte dentro la struct, che possono far traboccare lo stack. Mettilo in unBox(vedi Smart pointer) o usa unVec. - Ogni nuova dimensione o tipo è una nuova copia del codice. Usare un generico con 20 valori diversi di
Ncostruisce 20 versioni. Costa tempo di compilazione e dimensione del binario. Vedi Prestazioni. - Il typestate aggiunge tipi e messaggi di errore più lunghi. È ottimo per un’API che useranno in tanti (un builder, una connessione, un flusso di pagamento). Per uno stato che cambia in base a dati di runtime, come uno stato letto da un database, usa un enum e
match.
Una buona regola: parti semplice, e usa questi strumenti quando compare un bug reale o un costo reale che eviterebbero.
Errori comuni
Sezione intitolata “Errori comuni”Fare calcoli con un parametro const in un tipo. Sembra naturale, ma il Rust stabile non lo permette ancora:
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`Il compilatore dice che i parametri const possono comparire qui solo da soli, cioè come N. Soluzioni: usa [[f32; 2]; N] (un array di coppie), aggiungi un secondo parametro, o usa un Vec se la dimensione non deve stare nel tipo.
Dimenticare le graffe intorno a un’espressione. Buffer::<2 + 2>::new() dà error: expressions must be enclosed in braces to be used as const generic arguments, e la riga di aiuto mostra la soluzione: Buffer::<{ 2 + 2 }>::new(). Un numero semplice o il nome di una const non hanno bisogno di graffe.
Chiamare qualcosa che non è const in un contesto const. Una const non può chiamare una funzione normale, e una const fn non può usare un ciclo 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 variantsQui rustc spiega che nelle costanti si possono chiamare solo funzioni costanti. Per il testo, usa const GREETING: &str = "hello";. Dentro una const fn, sostituisci for con un ciclo while e un indice, come nella tabella di consultazione qui sopra.
#[derive(Clone, Copy)] su una struct con PhantomData. Ti aspetteresti che Id<User> sia Copy, visto che contiene solo un u32. Ma derive aggiunge il vincolo T: Copy, e User non è Copy, quindi l’ID viene spostato:
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 parametersLa nota finale spiega che il Clone derivato aggiunge vincoli impliciti sui parametri di tipo. La soluzione più semplice è derivare Clone, Copy anche sui tipi marcatori (#[derive(Clone, Copy)] struct User;). Oppure scrivi gli impl a mano, che non richiedono nessun vincolo su T:
impl<T> Clone for Id<T> { fn clone(&self) -> Self { *self }}impl<T> Copy for Id<T> {}Pratica
Sezione intitolata “Pratica”1. Completa i punteggi
Sezione intitolata “1. Completa i punteggi”Scrivi fn pad_scores<const N: usize>(scores: &[u32]) -> [u32; N], che copia i punteggi in un array di esattamente N elementi. Riempi i posti mancanti con 0 e scarta i punteggi in più. Chiamala come pad_scores::<5>(&[90, 85]) e come let top3: [u32; 3] = pad_scores(&[70, 95, 88, 60]);. Suggerimento: parti da [0; N], poi scorri 0..N e usa scores.get(i).
Mostra la soluzione
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);}Output:
[90, 85, 0, 0, 0][70, 95, 88]Qui N non può venire dall’argomento (una slice non ha la lunghezza nel suo tipo), quindi viene dal turbofish ::<5> o dal tipo che chiedi, [u32; 3].
2. Timeout calcolati in compilazione
Sezione intitolata “2. Timeout calcolati in compilazione”Scrivi due const fn, minutes(n: u64) -> u64 e hours(n: u64) -> u64, che restituiscono un numero di secondi. Usale per definire const SESSION_TIMEOUT: u64 = hours(1) + minutes(30); e un array const RETRY_DELAYS: [u64; 3] con 1, 5 e 15 minuti. Stampali entrambi. Suggerimento: hours può chiamare minutes.
Mostra la soluzione
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!("timeout della sessione: {SESSION_TIMEOUT} secondi"); println!("attese tra i tentativi: {:?}", RETRY_DELAYS);}Output:
timeout della sessione: 5400 secondiattese tra i tentativi: [60, 300, 900]Il compilatore calcola 5400 e la tabella delle attese durante la costruzione, e hours(1) + minutes(30) si legge molto meglio di un 5400 buttato lì. Se togli const da minutes, gli elementi const smettono di compilare con E0015.
3. Soldi che non si possono mischiare
Sezione intitolata “3. Soldi che non si possono mischiare”Crea struct Euro; e struct Dollar;, e un Money<C> con un campo cents: i64 e un campo PhantomData<C>. Dagli new(cents) e plus(self, other: Money<C>) -> Money<C>. Somma un pranzo e un caffè in euro e stampa il totale. Suggerimento: visto che other ha lo stesso C, un Money<Euro> e un Money<Dollar> non si possono sommare.
Mostra la soluzione
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!("totale: {}.{:02} EUR", total.cents / 100, total.cents % 100); println!("biglietto: {}.{:02} USD", ticket.cents / 100, ticket.cents % 100);}Output:
totale: 15.70 EURbiglietto: 49.99 USDProva lunch.plus(ticket): fallisce con expected Money<Euro>, found Money<Dollar>. Salvare i centesimi come intero invece che come f64 evita anche gli errori di arrotondamento con i soldi.
Riepilogo
Sezione intitolata “Riepilogo”- I const generics (
const N: usize) rendono un numero parte di un tipo. Array di lunghezze diverse sono tipi diversi, eimpl<T, const N: usize>scrive metodi per ogni dimensione. - Nel Rust stabile, i parametri const possono essere interi,
boolochar. Le espressioni usate come argomenti hanno bisogno delle graffe ({ PAGE * 2 }), e i calcoli con unNgenerico dentro un tipo non sono ancora permessi. - Una
const fnpuò essere eseguita in fase di compilazione nei contesti const (const,static, lunghezze degli array). Usawhileinvece difor, e chiama solo altreconst fn. PhantomData<T>è un campo di dimensione zero che lega una struct a un tipo che non memorizza. Usalo per ID tipizzati, unità di misura e typestate.impl Matrix<2, 2>eimpl<const N: usize> Matrix<N, N>danno metodi solo ad alcune dimensioni. Le costanti associate comeU::IN_METERSdanno a ogni tipo un suo valore calcolato in compilazione.- Questi strumenti costano tempo di compilazione e leggibilità. Usali quando prevengono un bug reale.
Verifica veloce
Sezione intitolata “Verifica veloce”Verifica veloce
1. Perché non puoi passare un [u32; 4] a una funzione che prende [u32; 3]?
2. Che cosa aggiunge PhantomData<T> a una struct durante l'esecuzione?
3. Quale di queste cose NON puoi usare dentro una const fn nel Rust stabile?
4. Che cosa significa impl<const N: usize> Matrix<N, N>?
Punteggio: 0 / 4
Per approfondire
Sezione intitolata “Per approfondire”- Const generics — The Rust Reference (in inglese): le regole esatte per i parametri const, quali tipi sono ammessi e quando servono le graffe.
- Const functions — The Rust Reference (in inglese): cosa sono i contesti const e cosa può fare una
const fn. PhantomData— documentazione della libreria standard (in inglese): la spiegazione ufficiale, con esempi di parametri di tipo inutilizzati.- Phantom type parameters — Rust by Example (in inglese): brevi esempi eseguibili, compreso un caso di studio sulle unità di misura.
- Const generics MVP hits beta — Rust Blog (in inglese): l’annuncio che spiega perché sono stati aggiunti i const generics e cosa sa fare la prima versione.