Le funzioni sono ottime, ma se si vuole chiamarne un po' su alcuni dati, può diventare scomodo. Si consideri questo codice:
fn main() { baz(bar(foo(x))); }baz(bar(foo(x)));
Normalmente leggiamo questo codice da sinistra a destra, e quindi diciamo ‘baz di bar di foo di x’. Ma questo non è l'ordine con cui le funzioni verrebbero chiamate; l'ordine di chiamata è invece il contrario: ‘applica a x prima foo, poi bar, e poi baz’. Non sarebbe carino se potessimo scrivere il seguente codice?
fn main() { x.foo().bar().baz(); }x.foo().bar().baz();
Fortunatamente, come si potrebbe immaginare, si può! Rust fornisce
la capacità di usare questa ‘sintassi di chiamata di metodo’ tramite
la parola-chiave impl
.
Ecco come funziona:
struct Cerchio { x: f64, y: f64, raggio: f64, } impl Cerchio { fn area(&self) -> f64 { std::f64::consts::PI * (self.raggio * self.raggio) } } fn main() { let c = Cerchio { x: 0.0, y: 0.0, raggio: 2.0 }; println!("{}", c.area()); }struct Cerchio { x: f64, y: f64, raggio: f64, } impl Cerchio { fn area(&self) -> f64 { std::f64::consts::PI * (self.raggio * self.raggio) } } fn main() { let c = Cerchio { x: 0.0, y: 0.0, raggio: 2.0 }; println!("{}", c.area()); }
Questo stamperà 12.566371
.
Abbiamo definito una struct
che rappresenta un cerchio. Poi abbiamo scritto
un blocco impl
, e al suo interno abbiamo definito un metodo, area
.
I metodi prendono un primo argomento speciale, di cui ci sono tre varianti:
self
, &self
, e &mut self
. Si può pensare a questo primo argomento come
se fosse il foo
in foo.bar()
. Le tre varianti corrispondono ai tre tipi
di cose che foo
potrebbe essere: self
se è un valore sullo stack,
&self
se è un riferimento, e &mut self
se è un riferimento mutabile.
Siccome abbiamo preso l'argomento &self
da area
, possiamo usarlo
come qualunque altro argomento. Siccome sappiamo che tale argomento è di tipo
Cerchio
, possiamo accedere al suo membro raggio
come faremmo con qualunque
altra struct
.
Di regola dovremmo usare &self
, dato che dovremmo preferire prendere
a prestito rispetto a prendere il possesso, e pure dovremmo preferire
prendere un riferimenti immutabili rispetto a qulli mutabili. Ecco un esempio
di tutte e tre le varianti:
struct Cerchio { x: f64, y: f64, raggio: f64, } impl Cerchio { fn riferimento(&self) { println!("presa di sé per riferimento!"); } fn riferimento_mutabile(&mut self) { println!("presa di sé per riferimento mutabile!"); } fn prendi_possesso(self) { println!("presa di possesso di sé!"); } }
Si possono usare tanti blocchi impl
quanti se ne vuole. L'esempio precedente
poteva anche essere scritto così:
struct Cerchio { x: f64, y: f64, raggio: f64, } impl Cerchio { fn riferimento(&self) { println!("presa di sé per riferimento!"); } } impl Cerchio { fn riferimento_mutabile(&mut self) { println!("presa di sé per riferimento mutabile!"); } } impl Cerchio { fn prendi_possesso(self) { println!("presa di possesso di sé!"); } }
Perciò, adesso sappiamo come chiamare un metodo, come foo.bar()
. E che dire
del nostro esempio originale, x.foo().bar().baz()
? Questo è chiamato
‘concatenamento di metodi’. Vediamo un esempio:
struct Cerchio { x: f64, y: f64, raggio: f64, } impl Cerchio { fn area(&self) -> f64 { std::f64::consts::PI * (self.raggio * self.raggio) } fn cresci(&self, incremento: f64) -> Cerchio { Cerchio { x: self.x, y: self.y, raggio: self.raggio + incremento } } } fn main() { let c = Cerchio { x: 0.0, y: 0.0, raggio: 2.0 }; println!("{}", c.area()); let d = c.cresci(2.0).area(); println!("{}", d); }
Verifica il tipo del valore restituito:
fn main() { struct Cerchio; impl Cerchio { fn grow(&self, increment: f64) -> Cerchio { Cerchio } } }fn grow(&self, increment: f64) -> Cerchio {
Diciamo che stiamo restituendo un Cerchio
. Con questo metodo, possiamo
far crescere un nuovo Cerchio
a qualunque dimensione.
Si possono anche definire funzioni associate che non prendono un argomento
self
. Ecco un pattern molto comune nel codice Rust:
struct Cerchio { x: f64, y: f64, raggio: f64, } impl Cerchio { fn new(x: f64, y: f64, raggio: f64) -> Cerchio { Cerchio { x: x, y: y, raggio: raggio, } } } fn main() { let c = Cerchio::new(0.0, 0.0, 2.0); }
Questa ‘funzione associata’ ci costruisce un nuovo Cerchio
. Si noti che
le funzioni associate vengono chiamate usando la sintassi Struct::function()
,
invece che con la sintassi ref.method()
. In alcuni altri linguaggi,
le funzioni associate sono chiamate ‘funzioni membro statiche’
o ‘metodi statici’ o ‘metodi di classe’.
Diciamo che vogliamo che i nostri utenti possano creare delle istanze
di Cerchio
, ma permetteremo loro di impostare solamente le proprietà
a cui sono interessati. Se non specificati, gli attributi x
e y
varranno
0.0
, e l'attributo raggio
varrà 1.0
. Rust non ha il sovraccaricamento
dei metodi, né gli argomenti con nome, né un numero variabile di argomenti.
Invece si impiega il pattern del costruttore. Si presenta così:
struct Cerchio { x: f64, y: f64, raggio: f64, } impl Cerchio { fn area(&self) -> f64 { std::f64::consts::PI * (self.raggio * self.raggio) } } struct CostruttoreDiCerchi { x: f64, y: f64, raggio: f64, } impl CostruttoreDiCerchi { fn new() -> CostruttoreDiCerchi { CostruttoreDiCerchi { x: 0.0, y: 0.0, raggio: 1.0, } } fn x(&mut self, coordinata: f64) -> &mut CostruttoreDiCerchi { self.x = coordinata; self } fn y(&mut self, coordinata: f64) -> &mut CostruttoreDiCerchi { self.y = coordinata; self } fn raggio(&mut self, raggio: f64) -> &mut CostruttoreDiCerchi { self.raggio = raggio; self } fn finalizza(&self) -> Cerchio { Cerchio { x: self.x, y: self.y, raggio: self.raggio } } } fn main() { let c = CostruttoreDiCerchi::new() .x(1.0) .y(2.0) .raggio(2.0) .finalizza(); println!("area: {}", c.area()); println!("x: {}", c.x); println!("y: {}", c.y); }
Ciò che abbiamo fatto qui è creare un'altra struct
, CostruttoreDiCerchi
.
Su di essa abbiamo definito i nostri metodi di costruttore. Abbiamo anche
definito il nostro metodo area()
su Cerchio
. Inoltre abbiamo creato
un altro metodo su CostruttoreDiCerchi
: finalizza()
. Questo metodo crea
il nostro Cerchio
finale dal costruttore. Adesso, abbiamo usato il sistema
dei tipi per imporre le nostre intenzioni: possiamo usare i metodi su
CostruttoreDiCerchi
per vincolare la costruzione di istanze di Cerchio
in qualunque modo desideriamo.