Costanti in Rust
In Rust esistono due modi principali per dichiarare valori che non cambiano durante l’esecuzione del programma: le costanti con const e le variabili statiche con static. Sebbene possano sembrare simili, hanno comportamenti e casi d’uso distinti. In questa guida esploreremo entrambi i meccanismi e le loro differenze.
Costanti con const
La parola chiave const dichiara un valore che viene calcolato a tempo di compilazione e non occupa una posizione fissa in memoria. Il compilatore può “inline” il valore ovunque venga utilizzato:
const PI_GRECO: f64 = 3.141592653589793;
const MAX_TENTATIVI: u32 = 5;
const NOME_APP: &str = "La Mia Applicazione";
fn main() {
println!("Pi greco: {}", PI_GRECO);
println!("Tentativi massimi: {}", MAX_TENTATIVI);
println!("Nome: {}", NOME_APP);
}
Regole per le Costanti
Le costanti in Rust hanno alcune regole specifiche:
- L’annotazione del tipo è obbligatoria: Non è possibile omettere il tipo.
- Il valore deve essere un’espressione costante: Deve essere calcolabile a tempo di compilazione.
- Non è possibile usare
mut: Le costanti sono sempre immutabili. - Possono essere dichiarate in qualsiasi scope: Incluso lo scope globale.
const SECONDI_PER_ORA: u32 = 60 * 60; // Espressione costante valida
const SECONDI_PER_GIORNO: u32 = SECONDI_PER_ORA * 24; // Usa un'altra costante
fn main() {
println!("Secondi in un'ora: {}", SECONDI_PER_ORA);
println!("Secondi in un giorno: {}", SECONDI_PER_GIORNO);
// Le costanti possono essere dichiarate anche dentro una funzione
const LIMITE_LOCALE: i32 = 100;
println!("Limite: {}", LIMITE_LOCALE);
}
Cosa NON Può Essere una Costante
Le costanti non possono contenere valori che richiedono computazioni a runtime:
// Errori di compilazione:
// const TEMPO: std::time::Instant = std::time::Instant::now(); // Non è const
// const INPUT: String = String::new(); // L'allocazione non è disponibile a compile time
Variabili Statiche con static
La parola chiave static dichiara una variabile globale con una posizione fissa in memoria e un lifetime che dura per tutta l’esecuzione del programma ('static):
static VERSIONE: &str = "1.0.0";
static CONTATORE_GLOBALE: i32 = 0;
fn main() {
println!("Versione: {}", VERSIONE);
println!("Contatore: {}", CONTATORE_GLOBALE);
}
Variabili Statiche Mutabili
Le variabili statiche possono essere dichiarate come mutabili con static mut, ma l’accesso è considerato unsafe perché potrebbe causare data race in programmi concorrenti:
static mut CONTATORE: u32 = 0;
fn incrementa() {
unsafe {
CONTATORE += 1;
}
}
fn main() {
incrementa();
incrementa();
incrementa();
unsafe {
println!("Contatore: {}", CONTATORE); // Stampa: 3
}
}
L’uso di static mut è sconsigliato nella programmazione Rust moderna. Per condividere stato mutabile globale, è preferibile utilizzare tipi thread-safe come Mutex o AtomicU32:
use std::sync::atomic::{AtomicU32, Ordering};
static CONTATORE_SICURO: AtomicU32 = AtomicU32::new(0);
fn incrementa() {
CONTATORE_SICURO.fetch_add(1, Ordering::SeqCst);
}
fn main() {
incrementa();
incrementa();
incrementa();
println!("Contatore: {}", CONTATORE_SICURO.load(Ordering::SeqCst));
}
Differenze tra const e static
Ecco le differenze principali tra const e static:
| Caratteristica | const |
static |
|---|---|---|
| Posizione in memoria | Può essere inlined | Posizione fissa |
| Lifetime | Nessun lifetime specifico | 'static |
| Mutabilità | Mai mutabile | Può essere mut (unsafe) |
| Indirizzo di memoria | Può variare ad ogni uso | Sempre lo stesso |
| Uso tipico | Valori costanti | Dati globali condivisi |
const COSTANTE: i32 = 42;
static STATICA: i32 = 42;
fn main() {
// La costante potrebbe non avere un indirizzo fisso
// La statica ha sempre lo stesso indirizzo
let ref_statica1 = &STATICA;
let ref_statica2 = &STATICA;
// Stesso indirizzo: true
println!("Stesso indirizzo: {}", std::ptr::eq(ref_statica1, ref_statica2));
}
Convenzione di Denominazione: SCREAMING_SNAKE_CASE
Sia le costanti che le variabili statiche seguono la convenzione SCREAMING_SNAKE_CASE (tutte maiuscole con underscore come separatore):
const TEMPERATURA_MASSIMA: f64 = 100.0;
const NUMERO_MASSIMO_UTENTI: u32 = 1_000_000;
const URL_BASE_API: &str = "https://api.esempio.com";
static CONFIGURAZIONE_GLOBALE: &str = "produzione";
static VERSIONE_PROTOCOLLO: u8 = 2;
fn main() {
println!("Temperatura massima: {}°C", TEMPERATURA_MASSIMA);
println!("Max utenti: {}", NUMERO_MASSIMO_UTENTI);
println!("Configurazione: {}", CONFIGURAZIONE_GLOBALE);
}
Il compilatore Rust emette un warning se non si rispetta questa convenzione:
// warning: constant `maxValore` should have an upper case name
// const maxValore: i32 = 100; // Non idiomatico!
const MAX_VALORE: i32 = 100; // Corretto
Espressioni Costanti
Rust supporta un insieme crescente di operazioni nelle espressioni costanti. Con l’edizione 2024, molte più funzioni possono essere marcate come const fn e utilizzate nelle costanti:
const fn fattoriale(n: u64) -> u64 {
if n == 0 {
1
} else {
n * fattoriale(n - 1)
}
}
const FATT_10: u64 = fattoriale(10);
const fn massimo(a: i32, b: i32) -> i32 {
if a > b { a } else { b }
}
const VALORE_MAX: i32 = massimo(42, 17);
fn main() {
println!("10! = {}", FATT_10); // Stampa: 10! = 3628800
println!("Max = {}", VALORE_MAX); // Stampa: Max = 42
}
Le const fn sono funzioni che possono essere valutate a tempo di compilazione, permettendo di creare espressioni costanti complesse in modo leggibile e manutenibile.
Conclusione
Le costanti e le variabili statiche sono strumenti fondamentali per gestire valori fissi e dati globali in Rust. Le costanti con const sono ideali per valori noti a tempo di compilazione che vengono utilizzati frequentemente, mentre static è appropriato quando serve un indirizzo di memoria fisso o uno stato globale. La convenzione SCREAMING_SNAKE_CASE e l’obbligo dell’annotazione del tipo rendono il codice Rust esplicito e facilmente leggibile, contribuendo alla cultura di chiarezza e sicurezza che caratterizza il linguaggio.