Il lifetime in Rust rappresenta il ciclo di vita del riferimento di un valore. Grazie a questa caratteristica, Rust è in grado di assicurarsi che la memoria funzioni in modo sicuro.
Nella maggior parte dei casi il lifetime viene gestito direttamente da Rust come mostra la guida relativa l’ownership e il borrowing. Ci sono, a volte, casi in cui è necessario esplicitare il lifetime perché Rust non può saperlo a priori e ce lo richiede.
Supponiamo di voler creare un programma o libreria di trigonometria, e vogliamo fare in modo che il programma valuti gli elementi noti di un triangolo e in base a questi richiami le funzioni corrette per risolvere il triangolo.
Per farlo, potremmo creare uno stato tramite un’enumerazione che valuti sia il numero dei lati e degli angoli ma anche se un angolo è opposto a uno dei lati. Per farlo usiamo una variabile booleana su una struct AngleTriangle.
Per indicare che tutti gli elementi dello stato hanno lo stesso lifetime inseriamo ‘a dopo il simbolo di riferimento &.
Spesso potremmo dover creare funzioni che gestiscono i lifetime dei parametri e del tipo di ritorno per indicare che quest’ultimo è valido fino a quando uno o più argomenti sono validi. Anche in questi casi bisogna rispettare le regole di proprietà e di riferimento di Rust.
fn name<'a>(arg: &'a str) -> &'a str {
//do something
arg
}
Esempio con i Lifetime in Rust
Supponiamo di voler creare un programma o libreria di trigonometria, e vogliamo fare in modo che il programma valuti gli elementi noti di un triangolo e in base a questi richiami le funzioni corrette per risolvere il triangolo.
Per farlo, potremmo creare uno stato tramite un’enumerazione che valuti sia il numero dei lati e degli angoli ma anche se un angolo è opposto a uno dei lati. Per farlo usiamo una variabile booleana su una struct AngleTriangle.
//module state
use crate::angle::AngleTriangle;
pub enum TriangleState<'a> {
OneSideTwoAngle {
side: &'a f64,
angle_opp: &'a AngleTriangle,
angle_2: &'a f64,
},
TwoSidesOneAngleBetween {
side_1: &'a f64,
side_2: &'a f64,
angle_between: &'a f64,
},
TwoSidesOneAngleOpp {
// Each side can have an opposite angle
side_1: &'a f64,
side_2: &'a f64,
angle_1: &'a Option<f64>,
angle_2: &'a Option<f64>,
},
ThreeSides {
side_1: &'a f64,
side_2: &'a f64,
side_3: &'a f64,
},
}
//module angle
pub struct AngleTriangle {
value: f64,
is_opp_side: bool,
}
impl AngleTriangle {
pub fn new(value: f64, is_opp_side: bool) -> Self {
Self { value, is_opp_side }
}
}
Per gestire lo stato creiamo una funzione che verifica gli elementi noti, richiama lo stato corretto e ritorna gli elementi mancanti.
pub fn use_triangle_state(state: TriangleState) -> Option<(Option<f64,Option<f64>,Option<f64>)> {
match state {
TriangleState::OneSideTwoAngle { side, angle_opp, angle_2 } => todo!(),
TriangleState::TwoSidesOneAngleBetween { side_1, side_2, angle_between } => todo!(),
TriangleState::TwoSidesOneAngleOpp { side_1, side_2, angle_1, angle_2 } => todo!(),
TriangleState::ThreeSides { side_1, side_2, side_3 } => todo!(),
}
}
‘static e puntatori intelligenti
E’ possibile indicare che un tipo di dato deve esistere per tutta la durata del programma utilizzando ‘static. Nell’esempio sotto usiamo una funzione che ci permette di convertire una stringa letterale in diversi tipi:
pub fn get_tuple<T: FromStr>(str_1: &'static str, str_2: &'static str) -> (T, T) {
let n1 = get_arg::<T>(&str_1); //Funzione che prende un singolo elemento e lo ritorna.
let n2 = get_arg::<T>(&str_2);
(n1,n2)
}
pub fn get_arg<T:FromStr>(str: &str) -> T {
loop {
println!("{}", str);
let mut buffer = String::new();
stdin()
.read_line(&mut buffer)
.expect("Errore nella lettura dell'input");
match buffer.trim().parse::<T>() {
Ok(element) => return element,
Err(_) => println!("Input non valido. Riprova!"),
}
}
}
fn main() {
let (n1,n2) = get_tuple::<i32>(
"Inserisci il primo numero",
"Inserisci il secondo numero"
);
println!("{},{}", n1, n2);
}
Un modo più semplice è quello di usare i puntatori intelligenti (Smart Pointers) per gestire i dati. In questo modo, il valore viene conservato nell’heap e soltanto il puntatore al valore viene salvato nello stack. Non dovremo quindi indicare i cicli di vita dato che il puntatore prende l’ownership dei dati.
Possiamo usare:
- Box<T> per avere soltanto la proprietà dei dati e usarli normalmente;
- Rc o Arc per condividere la proprietà di un dato ma senza modificarlo.
- Rc<RefCell<T> o Arc<Mutex<T> per condividere sia la proprietà che la modifica di un dato. RefCell e Mutex possono essere usati anche da soli ma non la proprietà può appartenere ad un’unica variabile.