Metodi e Blocchi impl in Rust
In Rust, i metodi sono funzioni associate a un tipo specifico, definite all’interno di blocchi impl. A differenza di altri linguaggi orientati agli oggetti, Rust separa la definizione dei dati (struct) dalla definizione del comportamento (impl), mantenendo il codice modulare e chiaro.
Definire Metodi con impl
Un blocco impl collega metodi a una struct (o enum, o trait). Il primo parametro di un metodo e sempre una variante di self, che rappresenta l’istanza su cui il metodo viene chiamato.
#[derive(Debug)]
struct Rettangolo {
larghezza: f64,
altezza: f64,
}
impl Rettangolo {
fn area(&self) -> f64 {
self.larghezza * self.altezza
}
fn perimetro(&self) -> f64 {
2.0 * (self.larghezza + self.altezza)
}
}
fn main() {
let rett = Rettangolo {
larghezza: 10.0,
altezza: 5.0,
};
println!("Area: {}", rett.area());
println!("Perimetro: {}", rett.perimetro());
}
self, &self e &mut self
La scelta tra le diverse forme di self determina come il metodo interagisce con l’istanza:
&self- prende un riferimento immutabile (il piu comune)&mut self- prende un riferimento mutabile, puo modificare l’istanzaself- prende l’ownership, consuma l’istanza
struct Contatore {
valore: i32,
}
impl Contatore {
// &self: legge senza modificare
fn valore(&self) -> i32 {
self.valore
}
// &mut self: modifica l'istanza
fn incrementa(&mut self) {
self.valore += 1;
}
// self: consuma l'istanza e la trasforma
fn azzera(self) -> Contatore {
Contatore { valore: 0 }
}
}
fn main() {
let mut c = Contatore { valore: 0 };
c.incrementa();
c.incrementa();
c.incrementa();
println!("Valore: {}", c.valore()); // 3
let c = c.azzera(); // c originale consumato, nuova istanza creata
println!("Dopo azzeramento: {}", c.valore()); // 0
}
Funzioni Associate (Costruttori)
Le funzioni associate sono funzioni definite nel blocco impl che non prendono self come primo parametro. Si chiamano usando la sintassi Tipo::funzione(). Il pattern piu comune e il costruttore new.
struct Cerchio {
raggio: f64,
centro: (f64, f64),
}
impl Cerchio {
// Costruttore classico
fn new(raggio: f64, x: f64, y: f64) -> Self {
Self {
raggio,
centro: (x, y),
}
}
// Costruttore alternativo
fn unitario() -> Self {
Self {
raggio: 1.0,
centro: (0.0, 0.0),
}
}
fn area(&self) -> f64 {
std::f64::consts::PI * self.raggio * self.raggio
}
fn circonferenza(&self) -> f64 {
2.0 * std::f64::consts::PI * self.raggio
}
}
fn main() {
let c1 = Cerchio::new(5.0, 0.0, 0.0);
let c2 = Cerchio::unitario();
println!("Cerchio 1 - Area: {:.2}", c1.area());
println!("Cerchio 2 - Circonferenza: {:.2}", c2.circonferenza());
}
Self (con la S maiuscola) e un alias per il tipo su cui e implementato il blocco impl, rendendo il codice piu manutenibile.
Blocchi impl Multipli
E possibile avere piu blocchi impl per lo stesso tipo. Questo e utile per organizzare il codice o per implementare metodi condizionati da trait bounds.
struct Punto {
x: f64,
y: f64,
}
// Primo blocco: costruttori
impl Punto {
fn new(x: f64, y: f64) -> Self {
Self { x, y }
}
fn origine() -> Self {
Self { x: 0.0, y: 0.0 }
}
}
// Secondo blocco: operazioni geometriche
impl Punto {
fn distanza_da(&self, altro: &Punto) -> f64 {
((self.x - altro.x).powi(2) + (self.y - altro.y).powi(2)).sqrt()
}
fn punto_medio(&self, altro: &Punto) -> Punto {
Punto {
x: (self.x + altro.x) / 2.0,
y: (self.y + altro.y) / 2.0,
}
}
}
fn main() {
let p1 = Punto::new(3.0, 4.0);
let p2 = Punto::origine();
println!("Distanza dall'origine: {:.2}", p1.distanza_da(&p2));
let medio = p1.punto_medio(&p2);
println!("Punto medio: ({:.1}, {:.1})", medio.x, medio.y);
}
Method Chaining (Restituire Self)
Il method chaining e un pattern in cui ogni metodo restituisce Self (o &mut Self), permettendo di concatenare piu chiamate in una singola espressione.
#[derive(Debug)]
struct QueryBuilder {
tabella: String,
condizioni: Vec<String>,
limite: Option<usize>,
ordinamento: Option<String>,
}
impl QueryBuilder {
fn new(tabella: &str) -> Self {
Self {
tabella: tabella.to_string(),
condizioni: Vec::new(),
limite: None,
ordinamento: None,
}
}
fn dove(mut self, condizione: &str) -> Self {
self.condizioni.push(condizione.to_string());
self
}
fn limite(mut self, n: usize) -> Self {
self.limite = Some(n);
self
}
fn ordina_per(mut self, campo: &str) -> Self {
self.ordinamento = Some(campo.to_string());
self
}
fn costruisci(&self) -> String {
let mut query = format!("SELECT * FROM {}", self.tabella);
if !self.condizioni.is_empty() {
query.push_str(&format!(" WHERE {}", self.condizioni.join(" AND ")));
}
if let Some(ref campo) = self.ordinamento {
query.push_str(&format!(" ORDER BY {}", campo));
}
if let Some(limite) = self.limite {
query.push_str(&format!(" LIMIT {}", limite));
}
query
}
}
fn main() {
let query = QueryBuilder::new("utenti")
.dove("eta > 18")
.dove("attivo = true")
.ordina_per("nome")
.limite(10)
.costruisci();
println!("{}", query);
// SELECT * FROM utenti WHERE eta > 18 AND attivo = true ORDER BY nome LIMIT 10
}
Conclusione
I blocchi impl sono il cuore della programmazione orientata ai tipi in Rust. Permettono di associare comportamenti alle struct in modo chiaro e organizzato. La distinzione tra &self, &mut self e self rende esplicito il livello di accesso ai dati, le funzioni associate offrono un modo naturale per definire costruttori, e il method chaining rende il codice espressivo e fluido. Combinati con i trait, i blocchi impl formano la base del polimorfismo in Rust.