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

Portare i Percorsi in Scope con la Parola Chiave use

Dover scrivere i percorsi (path) per chiamare le funzioni può risultare scomodo e ripetitivo. Nel Listato 7-7, sia che scegliessimo il path assoluto o relativo per la funzione aggiungi_in_lista, ogni volta che volevamo chiamare aggiungi_in_lista dovevamo specificare anche sala e accoglienza. Fortunatamente esiste un modo per semplificare questo processo: possiamo creare un collegamento rapido a un path con la parola chiave use una volta, e poi usare il nome più corto nel resto dello scope.

Nel Listato 7-11, portiamo il modulo crate::sala::accoglienza nello scope della funzione mangiare_al_ristorante così da dover specificare solo accoglienza::aggiungi_in_lista per chiamare la funzione aggiungi_in_lista in mangiare_al_ristorante.

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

use crate::sala::accoglienza;

pub fn mangiare_al_ristorante() {
    accoglienza::aggiungi_in_lista();
}
Listato 7-11: Portare un modulo nello scope con use

Aggiungere use e un path in uno scope è simile a creare un collegamento simbolico nel filesystem. Aggiungendo use crate::sala::accoglienza nella radice (root) del crate, accoglienza è ora un nome valido in quello scope, come se il modulo accoglienza fosse stato definito nella radice del crate. I path portati nello scope con use rispettano anche la privacy, come qualsiasi altro path.

Nota che use crea il collegamento rapido solo per lo scope particolare in cui use è dichiarato. Il Listato 7-12 sposta la funzione mangiare_al_ristorante in un nuovo modulo figlio chiamato cliente, che ora è in uno scope diverso rispetto alla dichiarazione use, quindi il corpo della funzione non si compilerà.

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

use crate::sala::accoglienza;

mod cliente {
    pub fn mangiare_al_ristorante() {
        accoglienza::aggiungi_in_lista();
    }
}
Listato 7-12: Una dichiarazione use si applica solo allo scope in cui si trova

L’errore del compilatore mostra che il collegamento non si applica più all’interno del modulo cliente:

$ cargo build
   Compiling ristorante v0.1.0 (file://progetti/ristorante)
error[E0433]: failed to resolve: use of undeclared crate or module `accoglienza`
  --> src/lib.rs:11:9
   |
11 |         accoglienza::aggiungi_in_lista();
   |         ^^^^^^^^^^^ use of undeclared crate or module `accoglienza`
   |
help: consider importing this module through its public re-export
   |
10 +     use crate::accoglienza;
   |

warning: unused import: `crate::sala::accoglienza`
 --> src/lib.rs:7:5
  |
7 | use crate::sala::accoglienza;
  |     ^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(unused_imports)]` on by default

For more information about this error, try `rustc --explain E0433`.
warning: `ristorante` (lib) generated 1 warning
error: could not compile `ristorante` (lib) due to 1 previous error; 1 warning emitted

Nota che c’è l’avviso che il use non è più usato nel suo scope! Per risolvere questo problema, sposta il use anche all’interno del modulo cliente, o riferisciti al collegamento dal modulo genitore con super::accoglienza all’interno del modulo figlio cliente.

Creare Percorsi use Idiomatici

Nel Listato 7-11, forse ti sarai chiesto perché abbiamo specificato use crate::sala::accoglienza e poi chiamato accoglienza::aggiungi_in_lista in mangiare_al_ristorante, invece di specificare il path use fino alla funzione aggiungi_in_lista per ottenere lo stesso risultato, come nel Listato 7-13.

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

use crate::sala::accoglienza::aggiungi_in_lista;

pub fn mangiare_al_ristorante() {
    aggiungi_in_lista();
}
Listato 7-13: Portare la funzione aggiungi_in_lista nello scope con use, che non è idiomatico

Sebbene sia il Listato 7-11 sia il Listato 7-13 compiano lo stesso compito, il Listato 7-11 è il modo idiomatico di portare una funzione nello scope con use. Portare il modulo genitore della funzione nello scope con use significa che dobbiamo specificare il modulo genitore quando chiamiamo la funzione. Specificare il modulo genitore quando si chiama la funzione rende chiaro che la funzione non è definita localmente riducendo comunque la ripetizione del path completo. Il codice in Listato 7-13 non chiarisce dove sia definita aggiungi_in_lista.

D’altra parte, quando portiamo struct, enum e altri elementi con use, è idiomatico specificare il path completo. Il Listato 7-14 mostra il modo idiomatico per portare, ad esempio, HashMap della libreria standard nello scope di un crate binario.

File: src/main.rs
use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    map.insert(1, 2);
}
Listato 7-14: Portare HashMap nello scope in modo idiomatico

Non c’è una ragione forte dietro questo uso: è semplicemente la convenzione che è emersa, e le persone si sono abituate a leggere e scrivere codice Rust in questo modo.

L’eccezione a questa idioma è se stiamo portando due elementi con lo stesso nome nello scope con use, perché Rust non lo permette. Il Listato 7-15 mostra come portare due Result nello scope che hanno lo stesso nome ma moduli genitore diversi, e come riferirsi a essi.

File: src/lib.rs
use std::fmt;
use std::io;

fn funzione1() -> fmt::Result {
    // --taglio--
    Ok(())
}

fn funzione2() -> io::Result<()> {
    // --taglio--
    Ok(())
}
Listato 7-15: Portare due type con lo stesso nome nello stesso scope richiede l’uso dei loro moduli genitore

Come si vede, usare i moduli genitore distingue i due type Result. Se invece avessimo specificato use std::fmt::Result e use std::io::Result, avremmo due type Result nello stesso scope, e Rust non saprebbe quale intendessimo quando avremmo usato Result.

Fornire Nuovi Nomi con la Parola Chiave as

C’è un’altra soluzione al problema di portare due type con lo stesso nome nello stesso scope con use: dopo il path, possiamo specificare as e un nuovo nome locale, o alias, per il type. Il Listato 7-16 mostra un altro modo di scrivere il codice del Listato 7-15 rinominando uno dei due type Result con as.

File: src/lib.rs
use std::fmt::Result;
use std::io::Result as IoResult;

fn funzione1() -> Result {
    // --taglio--
    Ok(())
}

fn funzione2() -> IoResult<()> {
    // --taglio--
    Ok(())
}
Listato 7-16: Rinominare un type quando viene portato nello scope con la parola chiave as

Nella seconda dichiarazione use, abbiamo scelto il nuovo alias IoResult per il type std::io::Result, che non entrerà in conflitto con il Result di std::fmt che abbiamo anch’esso portato nello scope. Sia il Listato 7-15 che il Listato 7-16 sono considerati idiomatici, quindi la scelta spetta a te!

Riesportare Nomi con pub use

Quando portiamo un nome nello scope con la parola chiave use, il nome è privato allo scope in cui lo abbiamo importato. Per consentire al codice esterno a quello scope di riferirsi a quel nome come se fosse stato definito in quello scope, possiamo combinare pub e use. Questa tecnica si chiama riesportare (re-exporting) perché portiamo un elemento nello scope ma lo rendiamo anche disponibile affinché altri possano portarlo nel loro scope.

Il Listato 7-17 mostra il codice del Listato 7-11 con use nella radice del modulo cambiato in pub use.

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

pub use crate::sala::accoglienza;

pub fn mangiare_al_ristorante() {
    accoglienza::aggiungi_in_lista();
}
Listato 7-17: Rendere un nome disponibile a qualsiasi codice da un nuovo scope con pub use

Prima di questa modifica, il codice esterno avrebbe dovuto chiamare la funzione aggiungi_in_lista usando il path ristorante::sala::accoglienza::aggiungi_in_lista(), che avrebbe inoltre richiesto che il modulo sala fosse marcato come pub. Ora che questo pub use ha riesportato il modulo accoglienza dalla radice del modulo, il codice esterno può invece usare il path ristorante::accoglienza::aggiungi_in_lista().

La riesportazione è utile quando la struttura interna del tuo codice è diversa da come i programmatori che chiamano il tuo codice penserebbero al dominio. Per esempio, in questa metafora del ristorante, chi gestisce il ristorante pensa in termini di “sala” e “cucine”. Ma i clienti che visitano un ristorante probabilmente non penseranno alle parti del ristorante in questi termini. Con pub use, possiamo scrivere il nostro codice con una struttura e però esporre una struttura diversa. Ciò rende la nostra libreria ben organizzata sia per i programmatori che lavorano sulla libreria sia per i programmatori che la usano. Vedremo un altro esempio di pub use e di come influisce sulla documentazione del crate in “Esportare un API Pubblica Efficace” nel Capitolo 14.

Usare Pacchetti Esterni

Nel Capitolo 2, abbiamo programmato un progetto del gioco degli indovinelli che usava un pacchetto esterno chiamato rand per ottenere numeri casuali. Per usare rand nel nostro progetto, abbiamo aggiunto questa riga a Cargo.toml:

File: Cargo.toml
rand = "0.8.5"

Aggiungere rand come dipendenza in Cargo.toml dice a Cargo di scaricare il pacchetto rand e le sue dipendenze da crates.io e di rendere rand disponibile al nostro progetto.

Poi, per portare le definizioni di rand nello scope del nostro pacchetto, abbiamo aggiunto una riga use che cominciava con il nome del crate, rand, e elencava gli elementi che volevamo portare nello scope. Ricorda che in “Generare un Numero Casuale” nel Capitolo 2, abbiamo portato il trait Rng nello scope e chiamato la funzione rand::thread_rng:

use std::io;

use rand::Rng;

fn main() {
    println!("Indovina il numero!");

    let numero_segreto = rand::thread_rng().gen_range(1..=100);

    println!("Il numero segreto è: {numero_segreto}");

    println!("Inserisci la tua ipotesi.");

    let mut ipotesi = String::new();

    io::stdin()
        .read_line(&mut ipotesi)
        .expect("Errore di lettura");

    println!("Hai ipotizzato: {ipotesi}");
}

Membri della community Rust hanno reso molti pacchetti disponibili su crates.io, e includere uno di essi nel tuo pacchetto implica gli stessi passi: elencarlo nel file Cargo.toml del tuo pacchetto e usare use per portare elementi dai loro crate nello scope.

Nota che la libreria standard std è anch’essa un crate esterno al nostro pacchetto. Poiché la libreria standard è distribuita con il linguaggio Rust, non dobbiamo modificare Cargo.toml per includere std. Ma dobbiamo comunque riferirci ad essa con use per portare elementi da lì nello scope del nostro pacchetto. Per esempio, per HashMap useremmo questa riga:

#![allow(unused)]
fn main() {
use std::collections::HashMap;
}

Questo è un path assoluto che inizia con std, il nome del crate della libreria standard.

Usare Percorsi Nidificati per Accorpare Elenchi di use

Se usiamo più elementi definiti nello stesso crate o nello stesso modulo, elencare ogni elemento su una sua riga può occupare molto spazio verticale nei file. Per esempio, queste due dichiarazioni use che avevamo nel gioco degli indovinelli nel Listato 2-4 portano elementi da std nello scope:

File: src/main.rs
use rand::Rng;
// --taglio--
use std::cmp::Ordering;
use std::io;
// --taglio--

fn main() {
    println!("Indovina il numero!");

    let numero_segreto = rand::thread_rng().gen_range(1..=100);

    println!("Il numero segreto è: {numero_segreto}");

    println!("Inserisci la tua ipotesi.");

    let mut ipotesi = String::new();

    io::stdin()
        .read_line(&mut ipotesi)
        .expect("Errore di lettura");

    println!("Hai ipotizzato: {ipotesi}");

    match ipotesi.cmp(&numero_segreto) {
        Ordering::Less => println!("Troppo piccolo!"),
        Ordering::Greater => println!("Troppo grande!"),
        Ordering::Equal => println!("Hai indovinato!"),
    }
}

Invece, possiamo usare path nidificati per portare gli stessi elementi nello scope in una sola riga. Lo facciamo specificando la parte comune del path, seguita da due due punti, e poi parentesi graffe intorno a una lista delle parti che differiscono dei path, come mostrato nel Listato 7-18.

File: src/main.rs
use rand::Rng;
// --taglio--
use std::{cmp::Ordering, io};
// --taglio--

fn main() {
    println!("Indovina il numero!");

    let numero_segreto = rand::thread_rng().gen_range(1..=100);

    println!("Il numero segreto è: {numero_segreto}");

    println!("Inserisci la tua ipotesi.");

    let mut ipotesi = String::new();

    io::stdin()
        .read_line(&mut ipotesi)
        .expect("Errore di lettura");

    let ipotesi: u32 = ipotesi.trim().parse().expect("Inserisci un numero!");

    println!("Hai ipotizzato: {ipotesi}");

    match ipotesi.cmp(&numero_segreto) {
        Ordering::Less => println!("Troppo piccolo!"),
        Ordering::Greater => println!("Troppo grande!"),
        Ordering::Equal => println!("Hai indovinato!"),
    }
}
Listato 7-18: Specificare un path nidificato per portare più elementi con lo stesso prefisso nello scope

Nei programmi più grandi, portare molti elementi nello scope dallo stesso crate o modulo usando path nidificati può ridurre di molto il numero di dichiarazioni use separate necessarie!

Possiamo usare un path nidificato a qualsiasi livello in un path, il che è utile quando si combinano due dichiarazioni use che condividono una parte di path. Per esempio, il Listato 7-19 mostra due dichiarazioni use: una che porta std::io nello scope e una che porta std::io::Write nello scope.

File: src/lib.rs
use std::io;
use std::io::Write;
Listato 7-19: Due dichiarazioni use che condividono parte della path

La parte comune di questi due path è std::io, ed è il path completo della prima path. Per unire questi due path in una sola dichiarazione use, possiamo usare self nel path nidificato, come mostrato nel Listato 7-20.

File: src/lib.rs
use std::io::{self, Write};
Listato 7-20: Combinare i path nel Listato 7-19 in una sola dichiarazione use

Questa riga porta std::io e std::io::Write nello scope.

Importare Elementi con l’Operatore Glob

Se vogliamo portare tutti gli elementi pubblici definiti in un path nello scope, possiamo specificare quel path seguito dall’operatore glob *:

#![allow(unused)]
fn main() {
use std::collections::*;
}

Questa dichiarazione use porta tutti gli elementi pubblici definiti in std::collections nello scope corrente. Stai attento quando usi l’operatore glob! Il glob può rendere più difficile capire quali nomi sono nello scope e da dove proviene un nome usato nel tuo programma. Inoltre, se la dipendenza cambia le sue definizioni, ciò che hai importato cambia a sua volta, il che può portare a errori del compilatore quando aggiorni la dipendenza se la dipendenza aggiunge una definizione con lo stesso nome di una tua definizione nello stesso scope, per esempio.

L’operatore glob viene spesso usato durante i test per portare tutto ciò che è sotto test nel modulo tests; parleremo di questo in “Come Scrivere dei Test” nel Capitolo 11. L’operatore glob è anche a volte usato come parte del pattern prelude: vedi la documentazione della libreria standard per ulteriori informazioni su quel pattern.