Wprowadzanie ścieżek do zasięgu za pomocą słowa kluczowego use
Konieczność wypisywania pełnych ścieżek do wywoływania funkcji może być uciążliwa i powtarzalna. W Listing 7-7, niezależnie od tego, czy wybraliśmy ścieżkę absolutną, czy względną do funkcji add_to_waitlist, za każdym razem, gdy chcieliśmy wywołać add_to_waitlist, musieliśmy również określać front_of_house i hosting. Na szczęście istnieje sposób na uproszczenie tego procesu: Możemy utworzyć skrót do ścieżki za pomocą słowa kluczowego use raz, a następnie używać krótszej nazwy wszędzie indziej w danym zasięgu.
W Listing 7-11 wprowadzamy moduł crate::front_of_house::hosting do zasięgu funkcji eat_at_restaurant, tak abyśmy musieli określać jedynie hosting::add_to_waitlist, aby wywołać funkcję add_to_waitlist w eat_at_restaurant.
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();
}
Dodanie use i ścieżki w zasięgu jest podobne do tworzenia dowiązania symbolicznego w systemie plików. Dodając use crate::front_of_house::hosting w katalogu głównym skrzynki, hosting jest teraz prawidłową nazwą w tym zasięgu, tak jakby moduł hosting został zdefiniowany w katalogu głównym skrzynki. Ścieżki wprowadzone do zasięgu za pomocą use również sprawdzają prywatność, tak jak każda inna ścieżka.
Zwróć uwagę, że use tworzy skrót tylko dla konkretnego zasięgu, w którym występuje use. Listing 7-12 przenosi funkcję eat_at_restaurant do nowego modułu potomnego o nazwie customer, który jest wówczas innym zasięgiem niż instrukcja use, więc ciało funkcji nie skompiluje się.
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();
}
}
Błąd kompilatora pokazuje, że skrót nie ma już zastosowania w module customer:
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0433]: failed to resolve: use of unresolved module or unlinked crate `hosting`
--> src/lib.rs:11:9
|
11 | hosting::add_to_waitlist();
| ^^^^^^^ use of unresolved module or unlinked crate `hosting`
|
= help: if you wanted to use a crate named `hosting`, use `cargo add hosting` to add it to your `Cargo.toml`
help: consider importing this module through its public re-export
|
10 + use crate::hosting;
|
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
For more information about this error, try `rustc --explain E0433`.
warning: `restaurant` (lib) generated 1 warning
error: could not compile `restaurant` (lib) due to 1 previous error; 1 warning emitted
Zauważ, że pojawiło się również ostrzeżenie, że use nie jest już używane w swoim zasięgu! Aby rozwiązać ten problem, przenieś use również do modułu customer lub odwołaj się do skrótu w module nadrzędnym za pomocą super::hosting w module potomnym customer.
Tworzenie idiomatycznych ścieżek use
W Listing 7-11 mogłeś się zastanawiać, dlaczego określiliśmy use crate::front_of_house::hosting, a następnie wywołaliśmy hosting::add_to_waitlist w eat_at_restaurant, zamiast określać ścieżkę use aż do funkcji add_to_waitlist, aby osiągnąć ten sam rezultat, jak w Listing 7-13.
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();
}
Chociaż zarówno Listing 7-11, jak i Listing 7-13 wykonują to samo zadanie, Listing 7-11 jest idiomatycznym sposobem wprowadzania funkcji do zasięgu za pomocą use. Wprowadzenie modułu nadrzędnego funkcji do zasięgu za pomocą use oznacza, że musimy określić moduł nadrzędny podczas wywoływania funkcji. Określanie modułu nadrzędnego podczas wywoływania funkcji jasno informuje, że funkcja nie jest zdefiniowana lokalnie, jednocześnie minimalizując powtarzanie pełnej ścieżki. Kod w Listing 7-13 niejasno określa, gdzie zdefiniowano add_to_waitlist.
Z drugiej strony, podczas wprowadzania struktur, wyliczeń i innych elementów za pomocą use, idiomatyczne jest określanie pełnej ścieżki. Listing 7-14 pokazuje idiomatyczny sposób wprowadzania struktury HashMap ze standardowej biblioteki do zasięgu skrzynki binarnej.
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert(1, 2);
}
Nie ma mocnego powodu, dla którego ten idiom: to po prostu konwencja, która się wykształciła, a ludzie przyzwyczaili się do czytania i pisania kodu w Rust w ten sposób.
Wyjątkiem od tego idiomu jest sytuacja, gdy wprowadzamy dwa elementy o tej samej nazwie do zasięgu za pomocą instrukcji use, ponieważ Rust na to nie pozwala. Listing 7-15 pokazuje, jak wprowadzić dwa typy Result do zasięgu, które mają tę samą nazwę, ale różne moduły nadrzędne, oraz jak się do nich odwoływać.
use std::fmt;
use std::io;
fn function1() -> fmt::Result {
// --snip--
Ok(())
}
fn function2() -> io::Result<()> {
// --snip--
Ok(())
}
Jak widać, użycie modułów nadrzędnych odróżnia dwa typy Result. Gdybyśmy zamiast tego określili use std::fmt::Result i use std::io::Result, mielibyśmy dwa typy Result w tym samym zasięgu, a Rust nie wiedziałby, o który nam chodzi, gdybyśmy użyli Result.
Nadawanie nowych nazw za pomocą słowa kluczowego as
Istnieje inne rozwiązanie problemu wprowadzania dwóch typów o tej samej nazwie do tego samego zasięgu za pomocą use: Po ścieżce możemy określić as i nową lokalną nazwę, czyli alias, dla typu. Listing 7-16 pokazuje inny sposób napisania kodu z Listing 7-15 poprzez zmianę nazwy jednego z dwóch typów Result za pomocą as.
use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {
// --snip--
Ok(())
}
fn function2() -> IoResult<()> {
// --snip--
Ok(())
}
W drugiej instrukcji use wybraliśmy nową nazwę IoResult dla typu std::io::Result, która nie będzie kolidować z Result z std::fmt, który również wprowadziliśmy do zasięgu. Listing 7-15 i Listing 7-16 są uważane za idiomatyczne, więc wybór należy do Ciebie!
Ponowne eksportowanie nazw za pomocą pub use
Kiedy wprowadzamy nazwę do zasięgu za pomocą słowa kluczowego use, nazwa jest prywatna dla zasięgu, do którego ją zaimportowaliśmy. Aby umożliwić kodowi spoza tego zasięgu odwoływanie się do tej nazwy tak, jakby została zdefiniowana w tym zasięgu, możemy połączyć pub i use. Ta technika nazywa się re-eksportowaniem, ponieważ wprowadzamy element do zasięgu, ale także udostępniamy ten element innym, aby mogli go wprowadzić do swojego zasięgu.
Listing 7-17 pokazuje kod z Listing 7-11, w którym use w module głównym zostało zmienione na pub use.
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();
}
Przed tą zmianą, kod zewnętrzny musiałby wywoływać funkcję add_to_waitlist używając ścieżki restaurant::front_of_house::hosting::add_to_waitlist(), co wymagałoby również, aby moduł front_of_house był oznaczony jako pub. Teraz, gdy pub use ponownie wyeksportowało moduł hosting z modułu głównego, kod zewnętrzny może zamiast tego używać ścieżki restaurant::hosting::add_to_waitlist().
Ponowny eksport jest przydatny, gdy wewnętrzna struktura kodu różni się od sposobu, w jaki programiści wywołujący kod myśleliby o domenie. Na przykład, w tej metaforze restauracji, osoby prowadzące restaurację myślą o „front of house” i „back of house”. Ale klienci odwiedzający restaurację prawdopodobnie nie będą myśleć o częściach restauracji w tych kategoriach. Dzięki pub use możemy pisać kod o jednej strukturze, ale udostępniać inną strukturę. Dzięki temu nasza biblioteka jest dobrze zorganizowana dla programistów pracujących nad biblioteką i programistów wywołujących bibliotekę. Inny przykład pub use i jego wpływu na dokumentację skrzynki omówimy w rozdziale 14 w sekcji „Eksportowanie wygodnego publicznego API”.
Używanie pakietów zewnętrznych
W Rozdziale 2 programowaliśmy projekt gry zgadywanek, który używał zewnętrznego pakietu rand do generowania liczb losowych. Aby użyć rand w naszym projekcie, dodaliśmy następującą linię do Cargo.toml:
rand = "0.8.5"
Dodanie rand jako zależności w Cargo.toml informuje Cargo, aby pobrał pakiet rand i wszystkie jego zależności z crates.io i udostępnił rand naszemu projektowi.
Następnie, aby wprowadzić definicje rand do zasięgu naszego pakietu, dodaliśmy linię use zaczynającą się od nazwy skrzynki, rand, i wymieniliśmy elementy, które chcieliśmy wprowadzić do zasięgu. Przypomnij sobie, że w sekcji „Generowanie liczby losowej” w Rozdziale 2 wprowadziliśmy cechę Rng do zasięgu i wywołaliśmy funkcję 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}");
}
Członkowie społeczności Rust udostępnili wiele pakietów na crates.io, a włączenie któregokolwiek z nich do Twojego pakietu obejmuje te same kroki: wymienienie ich w pliku Cargo.toml Twojego pakietu i użycie use do wprowadzenia elementów z ich skrzynek do zasięgu.
Zauważ, że standardowa biblioteka std jest również skrzynką zewnętrzną dla naszego pakietu. Ponieważ standardowa biblioteka jest dostarczana z językiem Rust, nie musimy zmieniać Cargo.toml, aby uwzględnić std. Musimy jednak odwołać się do niej za pomocą use, aby wprowadzić z niej elementy do zasięgu naszego pakietu. Na przykład, w przypadku HashMap użylibyśmy tej linii:
#![allow(unused)]
fn main() {
use std::collections::HashMap;
}
Jest to ścieżka absolutna zaczynająca się od std, nazwy skrzynki standardowej biblioteki.
Używanie zagnieżdżonych ścieżek do porządkowania list use
Jeśli używamy wielu elementów zdefiniowanych w tej samej skrzynce lub tym samym module, wymienianie każdego elementu w osobnej linii może zajmować dużo miejsca w naszych plikach. Na przykład, te dwie instrukcje use z gry zgadywanek w Listing 2-4 wprowadzają elementy z std do zasięgu:
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!"),
}
}
Zamiast tego możemy użyć zagnieżdżonych ścieżek, aby wprowadzić te same elementy do zasięgu w jednej linii. Robimy to, określając wspólną część ścieżki, po której następują dwa dwukropki, a następnie nawiasy klamrowe wokół listy części ścieżek, które się różnią, jak pokazano w Listing 7-18.
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!"),
}
}
W większych programach wprowadzanie wielu elementów do zasięgu z tej samej skrzynki lub modułu za pomocą zagnieżdżonych ścieżek może znacznie zmniejszyć liczbę oddzielnych instrukcji use!
Możemy używać zagnieżdżonych ścieżek na dowolnym poziomie ścieżki, co jest przydatne przy łączeniu dwóch instrukcji use, które współdzielą podścieżkę. Na przykład, Listing 7-19 pokazuje dwie instrukcje use: jedna, która wprowadza std::io do zasięgu, i druga, która wprowadza std::io::Write do zasięgu.
use std::io;
use std::io::Write;
Wspólną częścią tych dwóch ścieżek jest std::io, i to jest kompletna pierwsza ścieżka. Aby połączyć te dwie ścieżki w jedną instrukcję use, możemy użyć self w zagnieżdżonej ścieżce, jak pokazano w Listing 7-20.
use std::io::{self, Write};
Ta linia wprowadza std::io i std::io::Write do zasięgu.
Importowanie elementów za pomocą operatora glob
Jeśli chcemy wprowadzić wszystkie publiczne elementy zdefiniowane w ścieżce do zasięgu, możemy określić tę ścieżkę, po której następuje operator glob *:
#![allow(unused)]
fn main() {
use std::collections::*;
}
Ta instrukcja use wprowadza wszystkie publiczne elementy zdefiniowane w std::collections do bieżącego zasięgu. Zachowaj ostrożność podczas używania operatora glob! Glob może utrudnić określenie, jakie nazwy są w zasięgu i gdzie nazwa użyta w programie została zdefiniowana. Dodatkowo, jeśli zależność zmieni swoje definicje, to co zostało zaimportowane, również się zmieni, co może prowadzić do błędów kompilatora podczas aktualizacji zależności, jeśli zależność doda definicję o tej samej nazwie co Twoja definicja w tym samym zasięgu, na przykład.
Operator glob jest często używany podczas testowania, aby wprowadzić wszystko, co jest testowane, do modułu tests; omówimy to w sekcji „Jak pisać testy” w Rozdziale 11. Operator glob jest również czasami używany jako część wzorca preludium: zobacz dokumentację standardowej biblioteki, aby uzyskać więcej informacji na temat tego wzorca.