Importare Percorsi nello Scope con la parola chiave use

Dover scrivere i percorsi per chiamare le funzioni può sembrare scomodo e ripetitivo. Nel Listing 7-7, sia che scegliessimo il percorso assoluto o relativo per la funzione add_to_waitlist, ogni volta che volevamo chiamare add_to_waitlist dovevamo specificare anche front_of_house e hosting. Fortunatamente, c'è un modo per semplificare questo processo: possiamo creare un collegamento rapido a un percorso con la parola chiave use una volta, e poi usare il nome più breve ovunque nello scope.

Nel Listing 7-11, importiamo il modulo crate::front_of_house::hosting nello scope della funzione eat_at_restaurant così dobbiamo solo specificare hosting::add_to_waitlist per chiamare la funzione add_to_waitlist in eat_at_restaurant.

Nome del file: src/lib.rs

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
}

Listing 7-11: Importare un modulo nello scope con use

Aggiungere use e un percorso in uno scope è simile a creare un collegamento simbolico nel filesystem. Aggiungendo use crate::front_of_house::hosting nel crate root, hosting è ora un nome valido in quello scope, come se il modulo hosting fosse stato definito nel crate root. I percorsi importati nello scope con use controllano anche la privacy, come qualsiasi altro percorso.

Nota che use crea solo il collegamento rapido per lo specifico scope in cui si trova. Il Listing 7-12 sposta la funzione eat_at_restaurant in un nuovo modulo figlio chiamato customer, che è quindi un diverso scope rispetto alla dichiarazione use, quindi il Blocco della funzione non verrà compilato.

Nome del file: src/lib.rs

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use crate::front_of_house::hosting;

mod customer {
    pub fn eat_at_restaurant() {
        hosting::add_to_waitlist();
    }
}

Listing 7-12: Una dichiarazione use si applica solo nello scope in cui si trova

Il messaggio di errore del compilatore mostra che il collegamento rapido non si applica più all'interno del modulo customer:

$ cargo build
   Compiling restaurant v0.1.0 (file:///projects/restaurant)
warning: unused import: `crate::front_of_house::hosting`
 --> src/lib.rs:7:5
  |
7 | use crate::front_of_house::hosting;
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(unused_imports)]` on by default

error[E0433]: failed to resolve: use of undeclared crate or module `hosting`
  --> src/lib.rs:11:9
   |
11 |         hosting::add_to_waitlist();
   |         ^^^^^^^ use of undeclared crate or module `hosting`

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

Nota che c'è anche un avviso che l'use non è più utilizzato nel suo scope! Per risolvere questo problema, sposta l'use anche all'interno del modulo customer, o riferisci il collegamento rapido nel modulo genitore con super::hosting all'interno del modulo figlio customer.

Creare Percorsi use Idiomatici

Nel Listing 7-11, potresti esserti chiesto perché abbiamo specificato use crate::front_of_house::hosting e poi abbiamo chiamato hosting::add_to_waitlist in eat_at_restaurant, piuttosto che specificare il percorso use fino alla funzione add_to_waitlist per ottenere lo stesso risultato, come nel Listing 7-13.

Nome del file: src/lib.rs

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use crate::front_of_house::hosting::add_to_waitlist;

pub fn eat_at_restaurant() {
    add_to_waitlist();
}

Listing 7-13: Importare la funzione add_to_waitlist nello scope con use, che è non idiomatico

Sebbene entrambi i Listing 7-11 e 7-13 svolgano lo stesso compito, il Listing 7-11 è il modo idiomatico di importare una funzione nello scope con use. Importare 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 chiamiamo la funzione rende chiaro che la funzione non è definita localmente, riducendo al minimo la ripetizione del percorso completo. Il codice nel Listing 7-13 non è chiaro sulla definizione di add_to_waitlist.

D'altra parte, quando si importano structs, enums e altri elementi con use, è idiomatico specificare il percorso completo. Il Listing 7-14 mostra il modo idiomatico di importare lo struct HashMap della libreria standard nello scope di un crate binario.

Nome del file: src/main.rs

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    map.insert(1, 2);
}

Listing 7-14: Importare HashMap nello scope in modo idiomatico

Non ci sono forti motivi dietro questa convenzione: è solo la convenzione che è emersa, e la gente è abituata a leggere e scrivere codice Rust in questo modo.

L'eccezione a questa convenzione è quando portiamo nello scope due elementi con lo stesso nome usando dichiarazioni use, perché Rust non lo consente. Il Listing 7-15 mostra come importare nello scope due tipi Result con lo stesso nome ma moduli genitori differenti, e come riferirsi a essi.

Nome del file: src/lib.rs

use std::fmt;
use std::io;

fn function1() -> fmt::Result {
    // --snip--
    Ok(())
}

fn function2() -> io::Result<()> {
    // --snip--
    Ok(())
}

Listing 7-15: Importare nello stesso scope due tipi con lo stesso nome richiede l'uso dei loro moduli genitori.

Come puoi vedere, usare i moduli genitori distingue i due tipi Result. Se invece avessimo specificato use std::fmt::Result e use std::io::Result, avremmo due tipi Result nello stesso scope, e Rust non saprebbe quale intendevamo quando usiamo Result.

Fornire Nuovi Nomi con la parola chiave as

C'è un'altra soluzione al problema di importare nello stesso scope due tipi con lo stesso nome usando use: dopo il percorso, possiamo specificare as e un nuovo nome locale, o alias, per il tipo. Il Listing 7-16 mostra un altro modo di scrivere il codice nel Listing 7-15 rinominando uno dei due tipi Result con as.

Nome del file: src/lib.rs

use std::fmt::Result;
use std::io::Result as IoResult;

fn function1() -> Result {
    // --snip--
    Ok(())
}

fn function2() -> IoResult<()> {
    // --snip--
    Ok(())
}

Listing 7-16: Rinominare un tipo quando viene importato nello scope con la parola chiave as

Nella seconda dichiarazione use, abbiamo scelto il nuovo nome IoResult per il tipo std::io::Result, che non confliggerà con il Result da std::fmt che abbiamo già importato nello scope. Sia il Listing 7-15 che il Listing 7-16 sono considerati idiomatici, quindi la scelta sta a te!

Riesportare Nomi con pub use

Quando importiamo un nome nello scope con la parola chiave use, il nome disponibile nel nuovo scope è privato. Per permettere al codice che chiama il nostro codice di riferirsi a quel nome come se fosse stato definito nello scope di quel codice, possiamo combinare pub e use. Questa tecnica si chiama riesportazione perché stiamo importando un elemento nello scope ma lo rendiamo anche disponibile affinché altri possano importarlo nel loro scope.

Il Listing 7-17 mostra il codice nel Listing 7-11 con use nel modulo root cambiato in pub use.

Nome del file: src/lib.rs

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
}

Listing 7-17: Rendere un nome disponibile per qualsiasi codice da usare da un nuovo scope con pub use

Prima di questo cambiamento, il codice esterno avrebbe dovuto chiamare la funzione add_to_waitlist usando il percorso restaurant::front_of_house::hosting::add_to_waitlist(), che avrebbe anche richiesto che il modulo front_of_house fosse segnato come pub. Ora che questo pub use ha riesportato il modulo hosting dal modulo root, il codice esterno può usare il percorso restaurant::hosting::add_to_waitlist() invece.

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, le persone che gestiscono il ristorante pensano a "front of house" e "back of house". Ma i clienti che visitano un ristorante probabilmente non penseranno alle parti del ristorante in quei termini. Con pub use, possiamo scrivere il nostro codice con una struttura e esporne un’altra. Così facendo, la nostra libreria è ben organizzata per i programmatori che lavorano alla libreria e per i programmatori che la utilizzano. Vedremo un altro esempio di pub use e come influisce sulla documentazione del tuo crate nella sezione “Esportare una Comoda API Pubblica con pub use del Capitolo 14.

Usare Pacchetti Esterni

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

Nome del file: Cargo.toml

rand = "0.8.5"

Aggiungere rand come dipendenza in Cargo.toml dice a Cargo di scaricare il pacchetto rand e qualsiasi dipendenza da crates.io e rendere rand disponibile per il nostro progetto.

Poi, per importare le definizioni di rand nello scope del nostro pacchetto, abbiamo aggiunto una riga use iniziante con il nome del crate, rand, ed elencato gli elementi che volevamo importare nello scope. Ricorda che nella sezione “Generazione di un Numero Casuale” nel Capitolo 2, abbiamo importato il trait Rng nello scope e chiamato la funzione rand::thread_rng:

use std::io;
use rand::Rng;

fn main() {
    println!("Guess the number!");

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

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

I membri della comunità Rust hanno reso disponibili molti pacchetti su crates.io, e includerne uno nel tuo pacchetto comporta questi stessi passaggi: elencarli nel file Cargo.toml del tuo pacchetto e usare use per importare elementi dai loro crates nello scope.

Nota che la libreria standard std è anche un crate che è esterno al nostro pacchetto. Poiché la libreria standard è fornita con il linguaggio Rust, non abbiamo bisogno di cambiare Cargo.toml per includere std. Ma dobbiamo riferirci ad essa con use per importare elementi da lì nello scope del nostro pacchetto. Per esempio, con HashMap useremmo questa riga:

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

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

Usare Percorsi Annidati per Pulire Grandi Liste use

Se stiamo usando più elementi definiti nello stesso crate o nello stesso modulo, elencare ciascun elemento su una propria riga può occupare molto spazio verticale nei nostri file. Per esempio, queste due dichiarazioni use che avevamo nel gioco di indovinelli nel Listing 2-4 importano elementi da std nello scope:

Nome del file: src/main.rs

use rand::Rng;
// --snip--
use std::cmp::Ordering;
use std::io;
// --snip--

fn main() {
    println!("Guess the number!");

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

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}

Invece, possiamo usare percorsi annidati per importare gli stessi elementi nello scope in una linea. Possiamo fare ciò specificando la parte comune del percorso, seguita da due due punti, e poi parentesi graffe attorno a un elenco delle parti dei percorsi che differiscono, come mostrato nel Listing 7-18.

Nome del file: src/main.rs

use rand::Rng;
// --snip--
use std::{cmp::Ordering, io};
// --snip--

fn main() {
    println!("Guess the number!");

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

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    let guess: u32 = guess.trim().parse().expect("Please type a number!");

    println!("You guessed: {guess}");

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}

Listing 7-18: Specificare un percorso annidato per importare più elementi con lo stesso prefisso nello scope

In programmi più grandi, importare molti elementi nello scope dallo stesso crate o modulo usando percorsi annidati può ridurre il numero di dichiarazioni use separate necessarie di molto!

Possiamo usare un percorso annidato a qualsiasi livello di un percorso, il che è utile quando si combinano due dichiarazioni use che condividono un sotto-percorso. Per esempio, il Listing 7-19 mostra due dichiarazioni use: una che importa std::io nello scope e una che importa std::io::Write nello scope.

Nome del file: src/lib.rs

use std::io;
use std::io::Write;

Listing 7-19: Due dichiarazioni use dove uno è un sotto-percorso dell'altro

La parte comune di questi due percorsi è std::io, e questo è il percorso completo del primo. Per unire questi due percorsi in una sola dichiarazione use, possiamo usare self nel percorso annidato, come mostrato nel Listing 7-20.

Nome del file: src/lib.rs

use std::io::{self, Write};

Listing 7-20: Combinare i percorsi nel Listing 7-19 in una dichiarazione use

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

Il Glob Operator

Se vogliamo importare tutti gli elementi pubblici definiti in un percorso nello scope, possiamo specificare quel percorso seguito dall'operatore * glob:

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

Questa dichiarazione use importa tutti gli elementi pubblici definiti in std::collections nello scope corrente. Attenzione quando si utilizza l'operatore glob! Glob può rendere più difficile capire quali nomi sono nello scope e dove un nome usato nel tuo programma è stato definito.

L'operatore glob è spesso usato quando si testano per portare tutto sotto test nel modulo tests; ne parleremo nella sezione “Come Scrivere Test” nel Capitolo 11. L'operatore glob è anche a volte usato come parte del pattern prelude: vedi la documentazione della libreria standard per maggiori informazioni su quel pattern.