Home Rhyylen
Contatto
 
 
 
Rust Language
Capitolo 15
Struct

In questo capitolo affrontiamo un argomento molto importante in Rust per due motivi: primo, è un costrutto molto usato in Rust stesso, secondo permette all'utente di creare dei tipi, diciamo personalizzati, e quindi conferisce al programmatore una grande capacità descrittiva ed architetturale. Si tratta di un tipo particolare che è in grado di accogliere al suo interno elementi di tipo diverso. Le struct somigliano quindi in modo  alle ennuple, come appare dalla definizione di queste ultime. Tuttavia esiste una sostanziale differenza, ovvero, gli elementi all'interno di una struct hanno un nome per cui possono essere più facilmente reperiti e non è necessario riferirsi ad un ordinamento sequenziale per accedere ad essi il che evita situazioni in cui è possibile confondersi. Questo rende le struct molto più flessibili e comode nell'uso generico. E non è l'unico aspetto. Va inoltre sottolineato come anche le struct offrano astrazione a costo zero, se costruiamo tipi complessi non ci sono quindi overhead nascosti. In questo paragtrafo dovremo giocoforza anticipare alcuni argomenti che spiegheremo più avanti. Caso mai potrete tornarci sopra, intanto non scappano.

La keyword che definisce le struct è, banalmente struct e la conformazione standard, quella più comune, è la seguente:

struct Identificatore {
campo-1: tipo-1,
campo-2: tipo-2,
....
campo-n: tipo-n
}

si noti che i singoli elementi sono separati da una virgola (eventualmente l'ultima, prima della graffa di chiusura, può essere omessa) e l'identificatore della struct, come da linee guida, inizia con una maiuscola, o meglio segue la consueta convenzione CamelCase, il compilatore emette un warning se non trova l'iniziale maiuscola. Ora vediamo un classico esempio:

struct Persona {
nome: String,
cognome: String
eta: u8
}

In questo modo abbiamo definito un nostro modello di persona del tutto generico, insomma il nostro "tipo persona". Come tutti i tipi resta sulla carta fintanto che non effettuiamo una istanziazione. In pratica dobbiamo creare una variabile che abbia per tipo la struttura che abbiamo creato per utilizzarla nel nostro programma.
 
  Esempio 15.1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Persona {    
nome: String,    
cognome: String,    
eta: u8
}

fn main() {    
let p01 = Persona {        
nome: String::from("Mario"),        
cognome: String::from("Rossi"),        
eta: 50,    
};    
print!("{} {} {}",p01.nome, p01.cognome, p01.eta)
}

Alla riga 8 troviamo la creazione di una istanza della nostra struct Persona e nelle successive l'inizializzazione dei campi. L'accesso ai campi stessi è possibile grazie all'operatore . (punto). la cosiddetta dot notation. Affinchè i campi inizializzati possano essere successivamente variati è necessario che l'istanza sia dichiarata mut. La riga 8 pertanto diventerà

let mut p01 = Persona {

e questo permette di scrivere successivamente alla riga 12 istruzioni come:

p01.nome = "Luigi".to_string();

Non è possibile invece dichiarare solo alcuni campi come mutabili. L'istanza mutabile lo è in tutti i suoi campi oppure non lo è.

Il nostro linguaggio permette alcuni sistemi per limitare la necessità di digitazione quando si lavora sulle struct. Vediamo un esempio:

  Esempio 15.2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct Persona {
nome: String,
cognome: String,
anni: i32
}

fn main(){
    let p1 = Persona {
        nome: String::from("Mario"),
        cognome: String::from("Rossi"),
        anni: 25
    };
    let p2 = Persona {
        nome: p1.nome,
        cognome: p1.cognome,
        anni: 28
    };
    let p3 = Persona {
        nome: String::from("Giuseppe"),
       ..p2
    };
    println!("p3: {} {} {}", p3.nome, p3.cognome, p3.anni);
}

con l'ouput:

p3: Giuseppe Rossi 28

Questo perchè:

- alla riga 8 definiamo la variabile p1 di tipo definito dalla struct Persona
- alla riga 13 definiamo p2 che importa gli elementi nome e cognome di p1 e aggiunge il campo anni modificato rispetto a p1
- alla riga 18 definiamo p3 che modifica solo il campo nome e importa gli altri elementi da p2  tramite l'istruzione alla riga 20.

In casi complessi questo può risparmiare un po' di battute sulla tastiera. Alla riga 20 abbiamo introdotto l'operatore .. che in questo caso significa qualcosa come "tutto il resto è uguale ai corrispondenti campi di p2".
Bisogna fare attenzione in Rust, i suoi meccansimi di sicurezza sono sempre all'erta. Per cui ad esempio:

struct Test {    
    numero: i32,
}
fn main()
{    
    let t01 = Test {        
    numero: 8    
    };    
    println!("{}", t01.numero);    
    let t02 = t01;    
    println!("{}", t02.numero);    
    println!("{}", t01.numero);
}

Questo codice non compila e il messaggio lo potrete già intuire:

error[E0382]: borrow of moved value: `t01`
--> r209.rs:12:20
|
6 | let t01 = Test {
| --- move occurs because `t01` has type `Test`, which does not implement the `Copy` trait
...
10 | let t02 = t01;
| --- value moved here
11 | println!("{}", t02.numero);
12 | println!("{}", t01.numero);
| ^^^^^^^^^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0382`.

Il tipo da noi creato non supporta Copy (e nemmeno Clone, se volete provare). per cui la copia di una struct è un po' più complicata. Le regole di borrowing ed ownership seguono i singoli campi. Fornisco un esempio per risolvere (al netto di qualche ulteriore problemino spiegato di seguito) il problema ma torneremo sull'argomento in apposita sezione

  Esempio 15.3
1
2
3
4
5
6
7
8
9
10
11
12
13
#[derive(Copy, Clone)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let mut original = Point { x: 5, y: 10 };
    let copied = original; // Qui avviene la copia
    original.x = 9;
    println!("{}", original.x);
    println!("{}", copied.x);
}

e come output abbiamo:

9
5

a dimostrazione che si tratta di due copie indipendenti. La direttiva alla riga 1 serve a indicare al compilatore di implementare automaticamente i trait Copy e Clone per la struttura Point. Questi trait permettono di gestire la clonazione e la copia degli oggetti in modo sicuro ed efficiente. Inoltre come avrete intuito già da questo esempio, la copia di struct dipende anche dalla natura dei dati interni e dalla loro facoltà di implementare oppure no il trait Copy. Nell'esempio abbiamo gli interi che lo implementano. Ma se così non fosse bisogna usare Clone. Se ad esempio uno dei due campi x o y fosse stato una stringa l'istruzione di copia non avrebbe funzionato e per far girare correttamente il programma avremmo dovuto eliminare il richiamo al Trait copy alla prima riga e riscrivere la linea 9 come segue:

let copied = original.clone(); 

La vicinanza tra tuple e struct è certificato, se vogliamo, dall'esistenza di un costrutto molto simile ad entrambe tanto che vengono definite struct tuple o tuple struct. Queste sono caratterizzate ancora dalla keyword struct seguita da un identificatore ma i campi non hanno nomi espliciti ma solo un tipo a caratterizzarne la natura. Quindi sono assegnabili a variabili in modo più "leggero". Il formato è semplice:

struct identificatore (tipo-1, tipo-2, ..., tipo-n)

Vediamo  l'esempio (un classico, a dire il vero):

  Esempio 15.4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Definizione di una struct-tuple `Point`
struct Point(i32, i32);

fn main() {
// Creazione di un'istanza di `Point`
let origin = Point(0, 0); // `origin` è una struct-tuple con due valori interi.

// Accesso ai campi della struct-tuple
println!("Origine in ({}, {})", origin.0, origin.1);

// possiamo anche destrutturare una struct-tuple in una dichiarazione `let`
let Point(x, y) = origin; // Destrutturazione
println!("Coordinate: x = {}, y = {}", x, y);

}

Noterete che anche in questo l'accesso ai campi avviene come per le ennuple con l'operatore . (punto). Un altro classico esempio, (un tormentone sull'argomento, adire il vero), è il seguente, presento solo il codice che potrete facilmente comprendere da soli.

struct Color(i32, i32, i32);
fn main() {
let nero = Color(0, 0, 0);
// Accesso ai valori tramite l'operatore `.` e l'indice del campo
println!("Il nero ha i seguenti valori RGB({}, {}, {})", nero.0, nero.1, nero.2);
}

Di norma questo costrutto viene usato quando i campi all'interno non sono particolarmente numerosi, diciamo 4 o 5 ma, in pratica, non saranno molte le occasioni in cui lo incontrerete. Rispetto alle ennuple, che tutto sommato dei meri aggregatori, sono forse costrutti un po' più pesanti ma sono più chiari semanticamente e possono godere di implementazioni di trait e metodi.

Una terza forma di struct è quella vuota, caratterizzata dalla sola keyword struct seguita dall'identificatore:

struct Zero;

che può essere istanziata nel solito modo, ovviamente:

let zero = Zero;

Vi sono alcuni casi in questa particolare forma viene utile, quando dobbiamo rappresentare un elemento senza dati per esempio.

Prima di chiudere vediamo qualche caso di utilizzo della struct forse un po' meno comune ma molto utile.
Usare il pattern matching  (vedere paragrafo) sulle struct, esempio classico:

  Esempio 15.5
1
2
3
4
5
6
7
8
9
10
11
12
struct Point {
    x: i32,
    y: i32,
}
fn main() {
    let point = Point { x: 5, y: 10 };
    match point {
        Point { x: 0, y } => println!("Sul asse y: {}", y),
        Point { x, y: 0 } => println!("Sul asse x: {}", x),
        Point { x, y } => println!("Fuori dagli assi: ({}, {})", x, y),
    }
}

Un utile metodo di confronto. In pratica il punto creato alla riga 6 viene poi comparato con delle strutture create al volo nei vari rami del pattern.

Campi potenzialmente nulli nella struct

Quando abbiamo a che fare con una struct con campi che potrebbero essere assenti, è comune utilizzare il tipo Option per quei campi. Questo permette di esprimere in modo esplicito che un campo potrebbe contenere o meno un valore, evitando il rischio di null pointer presenti in altri linguaggi.

  Esempio 15.6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Person {
    name: String,
    age: Option<u8>, // Potrebbe non avere un'età specificata
}
fn main() {
    let gio = Person { name: "Gio".to_string(), age: Some(30) };
    let pio = Person { name: "Pio".to_string(), age: None }; // Nessuna età

    if let Some(age) = gio.age {
        println!("Gio ha {} anni", age);
    }
    match pio.age {
    Some(age) => println!("Pio ha {} anni", age),
    None => println!("Età di Pio sconosciuta"),
   }
}

Come si vede, non abbiamo specificato nessuna età per Pio e il programma risce a gestire tranquillamente questa situazione, l'output infatti è:

Gio ha 30 anni
Età di Pio sconosciuta
 
Destrutturazione

Così come per le ennuple possiamo effettuare inizializzazioni multiple così possiamo fare anche con le struct. Il seguente esempio crea due variabili x2 ed y2 a partite dai valori memorizzati in una struct. Si tratta di una estensione al caso standard di quanto visto nell’esempio 15.4:

  Esempio 15.7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct P {
    x1: i32,
    y1: f32,
    }
    fn main() {
        let p = P {
            x1: 1,
            y1: 2.1
        };
        let P {
            x1: x2,
            y1: y2
        } = p;
        println!("{}, {}", x2 * 2, y2);
    }

Le righe cruciali sono la 11 e la 12 nelle quali creaimo le nuove variabili. La riga 13 riprende i valori dell'istanza che vengono riversati nelle due nuove variabili che ovviamente ereditano il tipo dei campi originali.

Un capitolo importante, anche qui come per ennuple, è la convivenza tra struct e funzioni. Per saperne di più dovrete attendere l'apposito paragrafo. Torneremo poi con uno specifico paragrafo che ci pemetterà di ampliare ancora le struct che, come vedremo, si avvicineranno molto al concetto di "oggetti" tipico di altri linguaggi.