Home Rhyylen
Contatto
 
 
 
Rust Language
Capitolo 30
Implementare con impl

Tra le varie possibilità offerte da questo potente linguaggio ne esiste una che mi ha colpito positivamente ed ' rappresentata dalla keyword
impl, e sono certo che anche voi la troverete utile e molto usata nei vari progetti che affronterete. Il suo uso è molto versatile e sostanzialmente riguarda due ambiti:

**  implementazione di trait - che per ora non ci interessa
**  implementazioni cosiddette inerenti o forse meglio, implementazioni di tipo, che, in pratica, aggiungono funzionalità ad un tipo senza che questo debba implementare altri trait. Si tratta quindi di una potente possibilità di personalizzazione.


Il secondo aspetto quindi è quello che ci interessa in questo paragrafo. Questa possibilità come vedremo ci aiuta anche ad avvicinarci al mondo della programmazione a oggetti tipica di altri linguaggi. Rust, come abbiamo detto nell'introduzione, non è un vero e proprio linguaggio object oriented, ma ha alcune caratteristiche che ritroviamo anche in quel popolare paradigma. Un esempio caratteristico che chiarirà il concetto per chi già conosce cosa sia la programmazione a oggetti, coinvolge le struct, partner ideali di impl. Eccolo:

  Esempio 30.1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct Punto {
    x: i32,
    y: i32,
}
impl Punto {
    fn distanza_origine(&self) -> f64 {
        ((self.x.pow(2) + self.y.pow(2)) as f64).sqrt()
    }

    fn is_origine(&self) -> bool {
        self.x == 0 && self.y == 0
    }
}

fn main() {
    let p01 = Punto {x: 3, y: 4};
    println!("{}", p01.distanza_origine());
    println!("{}", p01.is_origine());
}

Inizialmente definiamo una normale struttura con due coordinate. Alla riga 5 ecco che entra in funzione la nostra implementazione del tipo struct appena creato. Ad esso aggiungiamo nuove funzionalità (proprio come se fosse un oggetto) che ci permettono di cooperare in maniera più ampia con il nostro Punto. La sintassi come la sua costruzione complessiva è abbastanza semplice tutto sommato:

***  impl deve essere seguito dal nome della struttura stessa così da creare un legame con essa; la sintassi base più completa può essere descritta come segue:

impl<T> NomeTipo<T> {
metodi associati al tipo
}

***
  alla riga 6 troviamo &self che è il solito riferimento immutabile all'istanza corrente. Tale istanza viene creata alla riga 16


l'output è ovvio:

5
false

Il discorso è del tutto analogo anche per altri elementi, vediamo ad esempio con un enumerativo con un classico esempio:

  Esempio 30.2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum Stato {
    Attivo,
    Inattivo,
    Sospeso,
}
impl Stato {
    fn descriz(&self) -> &str {
        match *self {
            Stato::Attivo => "Attivo",
            Stato::Inattivo => "Inattivo",
            Stato::Sospeso => "Sospeso",
        }
   }
}

fn main() {
    let mio_stato = Stato::Attivo;
    println!("Lo stato è: {}", mio_stato.descriz());
}

Si noti l'uso alla riga 8 dell'operatore di dereferenziazione che serve per accedere ai valori interni di Stato. In alternativa (scomoda) potremmo riscrivere la funzione alla riga 7 come segue:

fn descriz(&self) -> &str {
    match self {
    &Stato::Attivo => "Attivo",
    &Stato::Inattivo => "Inattivo",
    &Stato::Sospeso => "Sospeso",
    }
}

 
Un altro esempio interessante può essere il seguente:


  Esempio 30.3
1
2
3
4
5
6
7
8
9
10
11
12
struct Metri(f32);
impl Metri {
    fn aggiungi(&mut self, altri: Metri) {
        self.0 += altri.0;
    }
}

fn main() {
    let mut distanza = Metri(100.0);
    distanza.aggiungi(Metri(50.0));
    println!("La distanza è {} metri", distanza.0);
}

la cui analisi lascio come (semplice) esercizio.
E' interessante notare che, come peraltro è normale, possiamo anche usare il tipo generico T nelle implementazioni:

  Esempio 30.4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Coppia<T> {
    primo: T,
    secondo: T,
}
impl<T> Coppia<T> {
    fn nuovo(primo: T, secondo: T) -> Self {
        Self { primo, secondo }
    }
}
fn main() {
    let coppia1 = Coppia::nuovo(1, 2);
    println!("Coppia: ({}, {})", coppia1.primo, coppia1.secondo);
    let coppia2 = Coppia::nuovo('a', 'b');
    println!("Coppia: ({}, {})", coppia2.primo, coppia2.secondo);
}

che accetta interi oppure caratteri o anche altro.

Un'altra considerazione la merita la clausola
where, già incontrata. che viene utilizzata per specificare che un tipo generico deve implementare determinati trait, ad esempio.

impl<T> Tipo<T>
where
T: std::fmt::Display,

Come detto
impl risulta importante, direi fondamentale, quando si parla dei trait, ma questa trattazione sarà oggetto di altri capitoli.