Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Percorsi per Fare Riferimento a un Elemento nell’Albero dei Moduli

Per mostrare a Rust dove trovare un elemento in un albero dei moduli, utilizziamo un path (percorso) nello stesso modo in cui usiamo un path quando navighiamo in un filesystem. Per chiamare una funzione, dobbiamo conoscere il suo path.

Un path può assumere due forme:

  • Un path assoluto è il percorso completo che inizia dalla radice del crate; per il codice di un crate esterno, il percorso assoluto inizia con il nome del crate, e per il codice del crate corrente, inizia con il letterale crate.
  • Un path relativo inizia dal modulo corrente e utilizza self, super o un identificatore nel modulo corrente.

Sia i path assoluti che relativi sono seguiti da uno o più identificatori separati da doppi due punti (::).

Tornando al Listato 7-1, supponiamo di voler chiamare la funzione aggiungi_in_lista. Questo è lo stesso che chiedere: qual è il path della funzione aggiungi_in_lista? Il Listato 7-3 contiene il Listato 7-1 con alcuni dei moduli e delle funzioni rimossi.

Mostreremo due modi per chiamare la funzione aggiungi_in_lista da una nuova funzione, mangiare_al_ristorante, definita nella radice del crate. Questi path sono corretti, ma c’è un altro problema che impedirà a questo esempio di compilare così com’è. Spiegheremo perché tra poco.

La funzione mangiare_al_ristorante fa parte dell’API pubblica del nostro crate libreria, quindi la contrassegniamo con la parola chiave pub. Nella sezione “Esporre Path con la Parola Chiave pub, entreremo nei dettagli di pub.

File: src/lib.rs
mod sala {
    mod accoglienza {
        fn aggiungi_in_lista() {}
    }
}

pub fn mangiare_al_ristorante() {
    // Path assoluta
    crate::sala::accoglienza::aggiungi_in_lista();

    // Path relativa
    sala::accoglienza::aggiungi_in_lista();
}
Listato 7-3: Chiamare la funzione aggiungi_in_lista utilizzando path assoluti e relativi

La prima volta che chiamiamo la funzione aggiungi_in_lista in mangiare_al_ristorante, utilizziamo un path assoluto. La funzione aggiungi_in_lista è definita nello stesso crate di mangiare_al_ristorante, il che significa che possiamo usare la parola chiave crate per iniziare un path assoluto. Includiamo quindi ciascuno dei moduli successivi fino a raggiungere aggiungi_in_lista. Puoi immaginare un filesystem con la stessa struttura: specificheremmo il path /sala/accoglienza/aggiungi_in_lista per eseguire il programma aggiungi_in_lista; utilizzare il nome del crate per partire dalla radice del crate è come usare / per partire dalla radice del filesystem nel tuo terminale.

La seconda volta che chiamiamo aggiungi_in_lista in mangiare_al_ristorante, utilizziamo un path relativo. Il path inizia con sala, il nome del modulo definito allo stesso livello dell’albero dei moduli di mangiare_al_ristorante. Qui l’equivalente nel filesystem sarebbe utilizzare il path sala/accoglienza/aggiungi_in_lista. Iniziare con un nome di modulo significa che il path è relativo.

Scegliere se utilizzare un path relativo o assoluto è una decisione che prenderai in base al tuo progetto, e dipende da se è più probabile che tu sposti il codice di definizione dell’elemento separatamente o insieme al codice che utilizza l’elemento. Ad esempio, se spostassimo il modulo sala e la funzione mangiare_al_ristorante in un modulo chiamato gestione_cliente, dovremmo aggiornare il path assoluto per aggiungi_in_lista, ma il path relativo rimarrebbe valido. Tuttavia, se spostassimo la funzione mangiare_al_ristorante separatamente in un modulo chiamato cena, il path assoluto per la chiamata a aggiungi_in_lista rimarrebbe lo stesso, ma il path relativo dovrebbe essere aggiornato. La nostra preferenza in generale è specificare path assoluti perché è più probabile che vogliamo spostare le definizioni di codice e le chiamate agli elementi in modo indipendente l’una dall’altra.

Proviamo a compilare il Listato 7-3 e scopriamo perché non si compila ancora! Gli errori che otteniamo sono mostrati nel Listato 7-4.

$ cargo build
   Compiling ristorante v0.1.0 (file:///progetti/ristorante)
error[E0603]: module `accoglienza` is private
 --> src/lib.rs:9:18
  |
9 |     crate::sala::accoglienza::aggiungi_in_lista();
  |                  ^^^^^^^^^^^  ----------------- function `aggiungi_in_lista` is not publicly re-exported
  |                  |
  |                  private module
  |
note: the module `accoglienza` is defined here
 --> src/lib.rs:2:5
  |
2 |     mod accoglienza {
  |     ^^^^^^^^^^^^^^^

error[E0603]: module `accoglienza` is private
  --> src/lib.rs:12:11
   |
12 |     sala::accoglienza::aggiungi_in_lista();
   |           ^^^^^^^^^^^  ----------------- function `aggiungi_in_lista` is not publicly re-exported
   |           |
   |           private module
   |
note: the module `accoglienza` is defined here
  --> src/lib.rs:2:5
   |
 2 |     mod accoglienza {
   |     ^^^^^^^^^^^^^^^

For more information about this error, try `rustc --explain E0603`.
error: could not compile `ristorante` (lib) due to 2 previous errors
Listato 7-4: Errori del compilatore durante la costruzione del codice nel Listato 7-3

I messaggi di errore dicono che il modulo accoglienza è privato. In altre parole, abbiamo i path corretti per il modulo accoglienza e la funzione aggiungi_in_lista, ma Rust non ci permette di usarli perché non ha accesso alle sezioni private. In Rust, tutti gli elementi (funzioni, metodi, struct, enum, moduli e costanti) sono privati rispetto ai moduli genitore come impostazione predefinita. Se desideri rendere un elemento come una funzione o una struct privata, lo metti in un modulo.

Gli elementi in un modulo genitore non possono utilizzare gli elementi privati all’interno dei moduli figli, ma gli elementi nei moduli figli possono utilizzare gli elementi nei loro moduli antenati. Questo perché i moduli figli incapsulano e nascondono i loro dettagli di implementazione, ma i moduli figli possono vedere il contesto in cui sono definiti. Per continuare con la nostra metafora, pensa alle regole di privacy come se fossero l’ufficio posteriore di un ristorante: ciò che accade lì è privato e nascosto per i clienti del ristorante, ma i manager dell’ufficio possono vedere e fare tutto nel ristorante che gestiscono.

Rust ha scelto che far funzionare il sistema dei moduli in questo modo, nascondendo i dettagli di implementazione interni come impostazione predefinita. In questo modo, sai quali parti del codice interno puoi modificare senza compromettere il codice esterno. Tuttavia, Rust ti offre la possibilità di esporre le parti interne del codice dei moduli figli ai moduli antenati utilizzando la parola chiave pub per rendere pubblico un elemento.

Esporre Path con la Parola Chiave pub

Torniamo all’errore nel Listato 7-4 che ci diceva che il modulo accoglienza è privato. Vogliamo che la funzione mangiare_al_ristorante nel modulo genitore abbia accesso alla funzione aggiungi_in_lista nel modulo figlio, quindi contrassegniamo il modulo accoglienza con la parola chiave pub, come mostrato nel Listato 7-5.

File: src/lib.rs
mod sala {
    pub mod accoglienza {
        fn aggiungi_in_lista() {}
    }
}

// --taglio--
pub fn mangiare_al_ristorante() {
    // Path assoluta
    crate::sala::accoglienza::aggiungi_in_lista();

    // Path relativa
    sala::accoglienza::aggiungi_in_lista();
}
Listato 7-5: Dichiarare il modulo accoglienza come pub per usarlo da mangiare_al_ristorante

Sfortunatamente, il codice nel Listato 7-5 genera ancora errori del compilatore, come mostrato nel Listato 7-6.

$ cargo build
   Compiling ristorante v0.1.0 (file:///progetti/ristorante)
error[E0603]: function `aggiungi_in_lista` is private
  --> src/lib.rs:12:31
   |
12 |     crate::sala::accoglienza::aggiungi_in_lista();
   |                               ^^^^^^^^^^^^^^^^^ private function
   |
note: the function `aggiungi_in_lista` is defined here
  --> src/lib.rs:4:9
   |
 4 |         fn aggiungi_in_lista() {}
   |         ^^^^^^^^^^^^^^^^^^^^^^

error[E0603]: function `aggiungi_in_lista` is private
  --> src/lib.rs:15:24
   |
15 |     sala::accoglienza::aggiungi_in_lista();
   |                        ^^^^^^^^^^^^^^^^^ private function
   |
note: the function `aggiungi_in_lista` is defined here
  --> src/lib.rs:4:9
   |
 4 |         fn aggiungi_in_lista() {}
   |         ^^^^^^^^^^^^^^^^^^^^^^

For more information about this error, try `rustc --explain E0603`.
error: could not compile `ristorante` (lib) due to 2 previous errors
Listato 7-6: Errori del compilatore durante la costruzione del codice nel Listato 7-5

Cosa è successo? Aggiungere la parola chiave pub davanti a mod accoglienza rende il modulo pubblico. Con questa modifica, se possiamo accedere a sala, possiamo accedere a accoglienza. Ma i contenuti di accoglienza sono ancora privati; rendere pubblico il modulo non rende pubblici i suoi contenuti. La parola chiave pub su un modulo consente solo al codice nei suoi moduli genitore di fare riferimento ad esso, non di accedere al suo codice interno. Poiché i moduli sono contenitori, non possiamo fare molto semplicemente rendendo pubblico il modulo; dobbiamo andare oltre e scegliere di rendere pubblici uno o più degli elementi all’interno del modulo.

Gli errori nel Listato 7-6 dicono che la funzione aggiungi_in_lista è privata. Le regole di privacy si applicano a struct, enum, funzioni e metodi, così come ai moduli.

Facciamo in modo che anche la funzione aggiungi_in_lista sia pubblica aggiungendo la parola chiave pub prima della sua definizione, come nel Listato 7-7.

File: src/lib.rs
mod sala {
    pub mod accoglienza {
        pub fn aggiungi_in_lista() {}
    }
}

// --taglio--
pub fn mangiare_al_ristorante() {
    // Path assoluta
    crate::sala::accoglienza::aggiungi_in_lista();

    // Path relativa
    sala::accoglienza::aggiungi_in_lista();
}
Listato 7-7: Aggiungere la parola chiave pub a mod accoglienza e fn aggiungi_in_lista ci consente di chiamare la funzione da mangiare_al_ristorante

Ora il codice si compilerà! Per capire perché aggiungere la parola chiave pub ci consente di utilizzare questi path in mangiare_al_ristorante rispetto alle regole di privacy, diamo un’occhiata ai path assoluti e relativi.

Nel path assoluto, iniziamo con crate, la radice dell’albero dei moduli del nostro crate. Il modulo sala è definito nella radice del crate. Anche se sala non è pubblico, poiché la funzione mangiare_al_ristorante è definita nello stesso modulo di sala (cioè, mangiare_al_ristorante e sala sono fratelli), possiamo fare riferimento a sala da mangiare_al_ristorante. Successivamente, c’è il modulo accoglienza contrassegnato con pub. Possiamo accedere al modulo genitore di accoglienza, quindi possiamo accedere a accoglienza. Infine, la funzione aggiungi_in_lista è contrassegnata con pub e possiamo accedere al suo modulo genitore, quindi questa chiamata di funzione funziona!

Nel path relativo, la logica è la stessa del path assoluto, tranne per il primo passaggio: invece di partire dalla radice del crate, il path inizia da sala. Il modulo sala è definito all’interno dello stesso modulo di mangiare_al_ristorante, quindi il path relativo che inizia dal modulo in cui è definita mangiare_al_ristorante funziona. Poi, poiché accoglienza e aggiungi_in_lista sono contrassegnati con pub, il resto del path funziona, e questa chiamata di funzione è valida!

Se prevedi di condividere il tuo crate libreria affinché altri progetti possano utilizzare il tuo codice, la tua API pubblica è il tuo contratto con gli utenti del tuo crate che determina come possono interagire con il tuo codice. Ci sono molte considerazioni relative alla gestione delle modifiche alla tua API pubblica per facilitare la dipendenza delle persone dal tuo crate. Queste considerazioni vanno oltre l’ambito di questo libro; se sei interessato a questo argomento, consulta Le Linee Guida per l’API di Rust.

Buone Pratiche per Pacchetti con un Binario e una Libreria

Abbiamo menzionato che un pacchetto può contenere sia una radice di crate binario src/main.rs che una radice di crate libreria src/lib.rs, e entrambi i crate avranno il nome del pacchetto come impostazione predefinita. Tipicamente, i pacchetti che contengono sia una libreria che un crate binario avranno nel crate binario il codice strettamente necessario ad avviare un eseguibile che chiama il codice definito nel crate libreria. Questo consente ad altri progetti di beneficiare della maggior parte delle funzionalità che il pacchetto fornisce, poiché il codice del crate libreria può essere condiviso.

L’albero dei moduli dovrebbe essere definito in src/lib.rs. Quindi, qualsiasi elemento pubblico può essere utilizzato nel crate binario facendo iniziare i path con il nome del pacchetto. Il crate binario diventa un utilizzatore del crate libreria proprio come un crate completamente esterno utilizzerebbe il crate libreria: può utilizzare solo l’API pubblica. Questo ti aiuta a progettare una buona API; non solo sei l’autore, ma sei anche un cliente!

Nel Capitolo 12, dimostreremo questa pratica organizzativa con un programma da riga di comando che conterrà sia un crate binario che un crate libreria.

Iniziare Path Relative con super

Possiamo costruire path relative che iniziano nel modulo genitore, piuttosto che nel modulo corrente o nella radice del crate, utilizzando super all’inizio del path. Questo è simile a iniziare un path del filesystem con la sintassi .. che significa andare nella directory genitore. Utilizzare super ci consente di fare riferimento a un elemento che sappiamo essere nel modulo genitore, il che può rendere più facile riorganizzare l’albero dei moduli quando il modulo è strettamente correlato al genitore, ma il genitore potrebbe essere spostato altrove nell’albero dei moduli in futuro.

Considera il codice nel Listato 7-8 che modella la situazione in cui un cuoco corregge un ordine errato e lo porta personalmente al cliente. La funzione correzione_ordine definita nel modulo cucine chiama la funzione servi_ordine definita nel modulo genitore specificando il path per servi_ordine, iniziando con super.

File: src/lib.rs
fn servi_ordine() {}

mod cucine {
    fn correzione_ordine() {
        cucina_ordine();
        super::servi_ordine();
    }

    fn cucina_ordine() {}
}
Listato 7-8: Chiamare una funzione utilizzando un path relativo che inizia con super

La funzione correzione_ordine si trova nel modulo cucine, quindi possiamo usare super per andare al modulo genitore di cucine, che in questo caso è crate, la radice. Da lì, cerchiamo servi_ordine e lo troviamo. Successo! Pensiamo che il modulo cucine e la funzione servi_ordine siano probabilmente destinati a rimanere nella stessa relazione l’uno con l’altro e verranno spostati insieme se decidiamo di riorganizzare l’albero dei moduli del crate. Pertanto abbiamo usato super in modo da avere meno posti da aggiornare nel codice in futuro se questo codice viene spostato in un modulo diverso.

Rendere Pubbliche Struct e Enum

Possiamo anche utilizzare pub per designare struct ed enum come pubblici, ma ci sono alcuni dettagli aggiuntivi nell’uso di pub con struct ed enum. Se utilizziamo pub prima di una definizione di struct, rendiamo la struct pubblica, ma i campi della struct rimarranno privati (come per i moduli). Possiamo rendere pubblici o meno ciascun campo caso per caso. Nel Listato 7-9, abbiamo definito una struct pubblica cucine::Colazione con un campo pubblico toast ma un campo privato frutta_di_stagione. Questo modella il caso in un ristorante in cui il cliente può scegliere il tipo di pane che accompagna un pasto, ma il cuoco decide quale frutta accompagna il pasto in base a ciò che è di stagione e disponibile. La frutta disponibile cambia rapidamente, quindi i clienti non possono scegliere la frutta o vedere quale frutta riceveranno.

File: src/lib.rs
mod cucine {
    pub struct Colazione {
        pub toast: String,
        frutta_di_stagione: String,
    }

    impl Colazione {
        pub fn estate(toast: &str) -> Colazione {
            Colazione {
                toast: String::from(toast),
                frutta_di_stagione: String::from("pesche"),
            }
        }
    }
}

pub fn mangiare_al_ristorante() {
    // Ordina una colazione in estate con pane tostato di segale.
    let mut pasto = cucine::Colazione::estate("segale");
    // Cambiare idea sul pane che vorremmo.
    pasto.toast = String::from("integrale");
    println!("Vorrei un toast {}, grazie.", pasto.toast);

    // La riga successiva non verrà compilata se la de-commentiamo; non
    // ci è permesso vedere o modificare frutta che accompagna il pasto.
    // pasto.frutta_di_stagione = String::from("mirtilli");
}
Listato 7-9: Una struct con alcuni campi pubblici e alcuni campi privati

Poiché il campo toast nella struct cucine::Colazione è pubblico, in mangiare_al_ristorante possiamo scrivere e leggere il campo toast utilizzando la notazione a punto. Nota che non possiamo utilizzare il campo frutta_di_stagione in mangiare_al_ristorante, perché frutta_di_stagione è privato. Prova a de-commentare la riga che modifica il valore del campo frutta_di_stagione per vedere quale errore ottieni!

Inoltre, nota che poiché cucine::Colazione ha un campo privato, la struct deve fornire una funzione associata pubblica che costruisce un’istanza di Colazione (qui l’abbiamo chiamata estate). Se Colazione non avesse una funzione del genere, non potremmo creare un’istanza di Colazione in mangiare_al_ristorante perché non potremmo impostare il valore del campo privato frutta_di_stagione in mangiare_al_ristorante.

Al contrario, se rendiamo un’enum pubblico, tutte le sue varianti diventano pubbliche. Abbiamo bisogno solo di pub prima della parola chiave enum, come mostrato nel Listato 7-10.

File: src/lib.rs
mod cucine {
    pub enum Antipasti {
        Zuppa,
        Insalata,
    }
}

pub fn mangiare_al_ristorante() {
    let ordine1 = cucine::Antipasti::Zuppa;
    let ordine2 = cucine::Antipasti::Insalata;
}
Listato 7-10: Designare un’enum come pubblico rende pubbliche tutte le sue varianti

Poiché abbiamo reso pubblico l’enum Antipasti, possiamo utilizzare le varianti Zuppa e Insalata in mangiare_al_ristorante.

Le enum non sono molto utili a meno che le loro varianti non siano pubbliche; sarebbe fastidioso dover annotare tutte le varianti delle enum con pub una ad una, quindi la norma per le varianti delle enum è essere pubbliche. Le struct sono spesso utili senza che i loro campi siano pubblici, quindi i campi delle struct seguono la regola generale che tutto è privato come impostazione predefinita, a meno che non sia annotato con pub.

C’è un’ultima situazione che coinvolge pub che non abbiamo trattato, ed è la nostra ultima caratteristica del sistema dei moduli: la parola chiave use. Tratteremo use da solo prima, e poi mostreremo come combinare pub e use.