Włączanie Ścieżek do Zasięgu za Pomocą Słowa Kluczowego use
Konieczność ciągłego wypisywania ścieżek, by wywołać funkcję może być uciążliwa.
Na listingu 7-7, niezależnie od tego, czy wybraliśmy bezwzględną czy względną ścieżkę do funkcji add_to_waitlist
, za każdym razem, wywołując ją, musieliśmy napisać także front_of_house
i hosting
.
Na szczęście istnieje sposób na uproszczenie tego procesu: możemy raz utworzyć skrót do ścieżki za pomocą słowa kluczowego use
i używać go wielokrotnie w obrębie zasięgu.
Na listingu 7-11 włączamy moduł crate::front_of_house::hosting
w zasięg funkcji eat_at_restaurant
, więc musimy podać jedynie hosting::add_to_waitlist
, aby wywołać funkcję add_to_waitlist
z eat_at_restaurant
.
Plik: 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();
}
Dodanie do zasięgu use
i ścieżki jest podobne do tworzenia dowiązania symbolicznego w systemie plików.
Dodanie use crate::front_of_house::hosting
w korzeniu skrzyni sprawia, że hosting
staje się poprawną nazwą w tym zasięgu, tak jakby moduł hosting
był zdefiniowany w korzeniu skrzyni.
Ścieżki wprowadzone w zasięg za pomocą use
podlegają takim samym zasadą prywatność, jak wszystkie inne ścieżki.
Warto podkreślić, że use
tworzy skrót tylko w zasięgu, w którym występuje.
Na listingu 7-12 przeniesiono funkcję eat_at_restaurant
do nowego modułu podrzędnego o nazwie customer
, który tworzy zasięg odrębny od tego, w którym użyto use
. Dlatego ciało funkcji nie skompiluje się:
Plik: 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();
}
}
Błąd kompilatora pokazuje, że skrót nie ma zastosowania w obrębie modułu 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
Proszę zauważyć, że pojawiło się również ostrzeżenie, że use
nie jest używany w swoim zasięgu!
Aby rozwiązać ten problem, należy przenieść use
do modułu customer
, lub z modułu customer
odwołać się do skrótu w module nadrzędnym za pomocą super::hosting
.
Tworzenie Idiomatycznych Ścieżek use
Patrząc na listing 7-11, można się zastanawiać, dlaczego zdefiniowaliśmy use crate::front_of_house::hosting
, a następnie w eat_at_restaurant
wywołaliśmy hosting::add_to_waitlist
, zamiast od razu podać w use
całą ścieżkę do add_to_waitlist
, tak jak na listingu 7-13.
Plik: 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();
}
Działanie kodu na obu listingach, 7-11 i 7-13, jest takie samo.
Jednakże tylko listing 7-11 pokazuje idiomatyczne wykorzystanie use
do włączenia funkcji w zasięg.
Włączenie do zasięgu jej modułu nadrzędnego sprawia, że wywołując tę funkcję musimy podać nazwę jej modułu.
To zaś jasno mówi, iż funkcja ta nie jest zdefiniowana lokalnie, a jednocześnie ogranicza konieczność podawania pełnej ścieżki.
Dla odmiany kod na listingu 7-13 jest niejasny co do miejsca, w którym zdefiniowano add_to_waitlist
.
Z drugiej strony, gdy za pomocą use
wskazujemy struktury, enumy i inne elementy, idiomatycznie jest podać pełną ścieżkę.
Listing 7-14 pokazuje idiomatyczny sposób włączania w zasięg skrzyni binarnej struktury HashMap
z biblioteki standardowej.
Plik: src/main.rs
use std::collections::HashMap; fn main() { let mut map = HashMap::new(); map.insert(1, 2); }
Za tym idiomem nie stoi żaden mocny argument: jest to po prostu przyjęta konwencja, zaś ludzie przyzwyczaili się do czytania i pisania zgodnego z nią kodu.
Jednakże nie możemy podążyć za tą konwencją, gdy za pomocą use
chcemy wprowadzić w zasięg dwa elementy o tej samej nazwie.
Rust nam na to nie pozwoli.
Listing 7-15 pokazuje, jak włączyć w zasięg i odwoływać się do dwóch typów Result
, które mają tę samą nazwę, ale pochodzą z różnych modułów.
Plik: src/lib.rs
use std::fmt;
use std::io;
fn function1() -> fmt::Result {
// --snip--
Ok(())
}
fn function2() -> io::Result<()> {
// --snip--
Ok(())
}
Jak widać, używanie modułów nadrzędnych pozwala rozróżnić dwa typy Result
.
Gdybyśmy zamiast tego napisali use std::fmt::Result
i use std::io::Result
, mielibyśmy w tym samym zasięgu dwa różne typy Result
i Rust nie wiedziałby, który z nich mamy na myśli, gdy piszemy Result
.
Nadawanie Nowych Nazw Za Pomocą Słowa Kluczowego as
Istnieje też inne rozwiązanie problemu wprowadzania dwóch typów o tej samej nazwie w ten sam zasięg za pomocą use
: po ścieżce możemy podać as
i nową nazwę lokalną, alias dla typu. Listing 7-16 pokazuje kod równoważny temu z listingu 7-15, ale wykorzystujący zmianę nazwy jednego z dwóch typów Result
przy użyciu as
.
Plik: src/lib.rs
use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {
// --snip--
Ok(())
}
fn function2() -> IoResult<()> {
// --snip--
Ok(())
}
W drugiej deklaracji use
typowi std::io::Result
nadajemy nową nazwę IoResult
, niekolidującą z nazwą Result
z std::fmt
, którą również włączamy w ten sam zasięg.
Kod pokazany na obu listingach, 7-15 i 7-16, uważany jest za idiomatyczny.
Więc w takim przypadku wybór zależy jedynie od naszych osobistych preferencji!
Re-eksportowanie Nazw Za Pomocą pub use
Kiedy za pomocą słowa kluczowego use
włączamy nazwę w zasięg, to w nowym zasięgu jest ona prywatna.
Aby kodowi wywołującemu nasz kod umożliwić odwołanie się do tej nazwy tak, jakby była zdefiniowana w jego zasięgu, możemy połączyć pub
i use
.
Technika ta nazywana jest reeksportem, ponieważ wprowadzamy element w zasięg, ale również udostępniamy ten element innym, by mogli go włączyć w swój zasięg.
Listing 7-17 pokazuje kod z listingu 7-11 z zmienionym use
w module głównym na pub use
.
Filename: 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();
}
Przed zmianą, zewnętrzny kod, by wywołać funkcję add_to_waitlist
, musiałby użyć ścieżki
restaurant::front_of_house::hosting::add_to_waitlist()
.
Po zmianie, gdy pub use
reeksportował moduł hosting
z modułu głównego, zewnętrzny kod może w zamian użyć ścieżki restaurant::hosting::add_to_waitlist()
.
Reeksportowanie jest przydatne, gdy wewnętrzna struktura twojego kodu różni się od tego, jak wywołujący go programiści postrzegają jego domenę.
Na przykład w naszej metaforze restauracyjnej, ludzie prowadzący restaurację dzielą ją na „front of house“ i „back of house“.
Ale klienci odwiedzający restaurację prawdopodobnie nie będą myśleć o częściach restauracji w ten sam sposób.
Dzięki pub use
, możemy napisać nasz kod korzystając z innej struktury, od tej, którą ujawnimy.
Czynimy to, by nasza biblioteka była dobrze zorganizowana zarówno dla programistów pracujących nad nią, jak i tych ją wywołujących.
Przyjrzymy się innemu przykładowi pub use
i temu, jak wpływa on na dokumentację skrzyni w sekcji „Eksportowanie Wygodnego Publicznego API Za Pomocą pub use
“ rozdziału 14.
Używanie Pakietów Zewnętrznych
W rozdziale 2 zaprogramowaliśmy grę w zgadywanie, która wykorzystywała zewnętrzny pakiet o nazwie rand
do uzyskiwania liczb losowych. Aby użyć rand
w naszym projekcie, dodaliśmy następującą linię do Cargo.toml:
Plik: Cargo.toml
rand = "0.8.5"
Dodanie rand
jako zależności w Cargo.toml powoduje, że Cargo pobiera pakiet rand
wraz ze wszystkimi jego zależnościami z crates.io i udostępnia rand
naszemu projektowi.
Następnie, aby włączyć definicje z rand
w zasięg naszego pakietu, dodaliśmy linię use
ze ścieżką rozpoczynającą się od nazwy skrzyni, czyli rand
, i wymieniliśmy elementy, które chcemy włączyć w zasięg.
Przypomnijmy, że w sekcji „Generowanie Losowej Liczby“ rozdziału 2, włączyliśmy w zasięg cechę Rng
i wywołaliśmy funkcję rand::thread_rng
:
use std::io;
use rand::Rng;
fn main() {
println!("Zgadnij liczbę!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("Sekretna liczba to: {secret_number}");
println!("Podaj swoją liczbę:");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Błąd wczytania linii");
println!("Wybrana przez ciebie liczba: {guess}");
}
Członkowie społeczności Rusta udostępnili na stronie crates.io wiele pakietów, a użycie dowolnego z nich we własnym pakiecie wymaga wykonania tych samych kroków: dodania go do pliku Cargo.toml i włączenia w zasięg jego wybranych elementów za pomocą use
.
Proszę zauważyć, że standardowa biblioteka std
również jest skrzynią, która jest zewnętrzna względem naszego pakietu.
Ponieważ standardowa biblioteka jest dostarczana z językiem Rust, nie musimy dodawać std
do Cargo.toml.
Musimy jednak odwoływać się do niej za pomocą use
, aby wprowadzić jej elementy w zasięg naszego pakietu.
Na przykład, możemy skorzystać z HashMap
za pomocą następującej linii:
#![allow(unused)] fn main() { use std::collections::HashMap; }
Jest to ścieżka bezwzględna rozpoczynająca się od std
, czyli nazwy skrzyni biblioteki standardowej.
Porządkowania Długich List use
za Pomocą Zagnieżdżonych Ścieżek
Gdy używamy wielu elementów zdefiniowanych w tej samej skrzyni lub w tym samym module, wymienienie każdego elementu w osobnej linii pochłania sporo miejsca. Na przykład, te dwie deklaracje use
, które mieliśmy w grze zgadywance na listingu 2-4, włączają w zasięg elementy z std
:
Plik: 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!"),
}
}
Zamiast nich, możemy użyć zagnieżdżonych ścieżek, aby włączyć te same elementy w jednym wierszu. Robimy to, podając wspólną część ścieżki, po której następują dwa dwukropki, a następnie nawiasy klamrowe obejmujące listę różniących się fragmentów ścieżek, co pokazano na listingu 7-18.
Plik: 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!"),
}
}
W większych programach, wprowadzenie wielu elementów tej samej skrzyni lub modułu w zasięg przy użyciu zagnieżdżonych ścieżek może znacznie zmniejszyć liczbę napisanych linii use
!
Ścieżki można zagnieżdżać na dowolnym poziomie, co jest przydatne przy łączeniu dwóch deklaracji use
dzielących podścieżkę.
Na przykład, listing 7-19 pokazuje dwie instrukcje use
: pierwsza włącza w zasięg std::io
, zaś druga std::io::Write
.
Plik: src/lib.rs
use std::io;
use std::io::Write;
Częścią wspólną tych dwóch ścieżek jest std::io
, co daje całą pierwszą ścieżkę.
Aby zawrzeć te dwie ścieżki w jednej deklaracji use
, można użyć self
w zagnieżdżonej ścieżce, tak jak pokazano na listingu 7-20.
Plik: src/lib.rs
use std::io::{self, Write};
Ta linia włącza std::io
i std::io::Write
w zasięg.
Operator Glob
Jeśli chcemy włączyć w zasięg wszystkie elementy publiczne zdefiniowane w ścieżce, możemy podać tę ścieżkę, a za nią *
zwaną operatorem glob:
#![allow(unused)] fn main() { use std::collections::*; }
Ta deklaracja use
wprowadza do bieżącego zasięgu wszystkie publiczne elementy zdefiniowane w std::collections
.
Operatora glob należy używać z rozwagą!
Glob może utrudnić ustalenie, które nazwy są w zasięgu i gdzie zostały zdefiniowane.
Operator globy jest często używany podczas testowania, aby wprowadzić wszystko, co jest testowane, do modułu tests
;
o czym będziemy mówić w sekcji "How to Write Tests" rozdziału 11.
Operator glob jest też czasami używany jako część wzorca prelude, opisanego w dokumentacji biblioteki standardowej.