Đưa Đường dẫn vào Phạm vi với từ khóa use
Việc phải viết ra các đường dẫn để gọi hàm có thể cảm thấy bất tiện và lặp đi
lặp lại. Trong Listing 7-7, cho dù chúng ta chọn đường dẫn tuyệt đối hoặc tương
đối đến hàm add_to_waitlist
, mỗi khi chúng ta muốn gọi add_to_waitlist
chúng
ta phải chỉ định cả front_of_house
và hosting
. May mắn thay, có một cách để
đơn giản hóa quá trình này: chúng ta có thể tạo một lối tắt đến đường dẫn với từ
khóa use
một lần, và sau đó sử dụng tên ngắn hơn ở mọi nơi khác trong phạm vi.
Trong Listing 7-11, chúng ta đưa module crate::front_of_house::hosting
vào
phạm vi của hàm eat_at_restaurant
để chúng ta chỉ cần chỉ định
hosting::add_to_waitlist
để gọi hàm add_to_waitlist
trong
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();
}
Việc thêm use
và một đường dẫn trong một phạm vi tương tự như việc tạo một
liên kết tượng trưng trong hệ thống tệp. Bằng cách thêm
use crate::front_of_house::hosting
trong gốc crate, hosting
bây giờ là một
tên hợp lệ trong phạm vi đó, giống như thể module hosting
đã được định nghĩa
trong gốc crate. Đường dẫn được đưa vào phạm vi bằng use
cũng kiểm tra quyền
riêng tư, giống như bất kỳ đường dẫn nào khác.
Lưu ý rằng use
chỉ tạo lối tắt cho phạm vi cụ thể mà use
xảy ra. Listing
7-12 di chuyển hàm eat_at_restaurant
vào một module con mới có tên là
customer
, sau đó là một phạm vi khác với câu lệnh use
, vì vậy phần thân hàm
sẽ không biên dịch được.
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();
}
}
Lỗi trình biên dịch cho thấy lối tắt không còn áp dụng trong module customer
:
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
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`
|
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
Lưu ý cũng có một cảnh báo rằng use
không còn được sử dụng trong phạm vi của
nó! Để khắc phục vấn đề này, hãy di chuyển use
vào module customer
, hoặc
tham chiếu lối tắt trong module cha với super::hosting
trong module con
customer
.
Tạo Đường dẫn use
Thuần phong
Trong Listing 7-11, bạn có thể đã tự hỏi tại sao chúng ta chỉ định
use crate::front_of_house::hosting
và sau đó gọi hosting::add_to_waitlist
trong eat_at_restaurant
, thay vì chỉ định đường dẫn use
hoàn toàn đến hàm
add_to_waitlist
để đạt được kết quả tương tự, như trong 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();
}
Mặc dù cả Listing 7-11 và Listing 7-13 đều thực hiện cùng một nhiệm vụ, Listing
7-11 là cách thuần phong để đưa một hàm vào phạm vi với use
. Việc đưa module
cha của hàm vào phạm vi với use
có nghĩa là chúng ta phải chỉ định module cha
khi gọi hàm. Chỉ định module cha khi gọi hàm làm rõ rằng hàm không được định
nghĩa cục bộ trong khi vẫn giảm thiểu việc lặp lại đường dẫn đầy đủ. Đoạn mã
trong Listing 7-13 không rõ ràng về nơi add_to_waitlist
được định nghĩa.
Mặt khác, khi đưa vào structs, enums, và các item khác với use
, việc chỉ định
đường dẫn đầy đủ là thuần phong. Listing 7-14 thể hiện cách thuần phong để đưa
struct HashMap
của thư viện chuẩn vào phạm vi của một binary crate.
use std::collections::HashMap; fn main() { let mut map = HashMap::new(); map.insert(1, 2); }
Không có lý do mạnh mẽ đằng sau cách viết này: đó chỉ là quy ước đã nổi lên, và mọi người đã quen với việc đọc và viết mã Rust theo cách này.
Ngoại lệ cho cách viết này là nếu chúng ta đưa hai item có cùng tên vào phạm vi
với các câu lệnh use
, vì Rust không cho phép điều đó. Listing 7-15 thể hiện
cách đưa hai kiểu Result
vào phạm vi có cùng tên nhưng có module cha khác
nhau, và cách tham chiếu đến chúng.
use std::fmt;
use std::io;
fn function1() -> fmt::Result {
// --snip--
Ok(())
}
fn function2() -> io::Result<()> {
// --snip--
Ok(())
}
Như bạn có thể thấy, việc sử dụng các module cha phân biệt hai kiểu Result
.
Nếu thay vào đó chúng ta chỉ định use std::fmt::Result
và
use std::io::Result
, chúng ta sẽ có hai kiểu Result
trong cùng một phạm vi,
và Rust sẽ không biết chúng ta muốn nói đến kiểu nào khi chúng ta sử dụng
Result
.
Cung cấp Tên Mới với từ khóa as
Có một giải pháp khác cho vấn đề đưa hai kiểu có cùng tên vào cùng một phạm vi
với use
: sau đường dẫn, chúng ta có thể chỉ định as
và một tên cục bộ mới,
hoặc alias (biệt danh), cho kiểu đó. Listing 7-16 thể hiện một cách khác để
viết mã trong Listing 7-15 bằng cách đổi tên một trong hai kiểu Result
sử dụng
as
.
use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {
// --snip--
Ok(())
}
fn function2() -> IoResult<()> {
// --snip--
Ok(())
}
Trong câu lệnh use
thứ hai, chúng ta đã chọn tên mới IoResult
cho kiểu
std::io::Result
, điều này sẽ không xung đột với Result
từ std::fmt
mà
chúng ta cũng đã đưa vào phạm vi. Listing 7-15 và Listing 7-16 đều được coi là
thuần phong, vì vậy sự lựa chọn là ở bạn!
Tái xuất Tên với pub use
Khi chúng ta đưa một tên vào phạm vi với từ khóa use
, tên đó là riêng tư đối
với phạm vi mà chúng ta đã nhập nó vào. Để cho phép mã bên ngoài phạm vi đó tham
chiếu đến tên đó như thể nó đã được định nghĩa trong phạm vi đó, chúng ta có thể
kết hợp pub
và use
. Kỹ thuật này được gọi là re-exporting (tái xuất) vì
chúng ta đưa một item vào phạm vi nhưng cũng làm cho item đó có sẵn cho người
khác đưa vào phạm vi của họ.
Listing 7-17 thể hiện mã trong Listing 7-11 với use
trong module gốc được thay
đổi thành 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();
}
Trước thay đổi này, mã bên ngoài sẽ phải gọi hàm add_to_waitlist
bằng cách sử
dụng đường dẫn restaurant::front_of_house::hosting::add_to_waitlist()
, điều
này cũng đòi hỏi module front_of_house
phải được đánh dấu là pub
. Bây giờ
pub use
này đã tái xuất module hosting
từ module gốc, mã bên ngoài có thể sử
dụng đường dẫn restaurant::hosting::add_to_waitlist()
thay thế.
Tái xuất rất hữu ích khi cấu trúc nội bộ của mã của bạn khác với cách các lập
trình viên gọi mã của bạn sẽ nghĩ về miền. Ví dụ, trong phép ẩn dụ nhà hàng này,
những người điều hành nhà hàng nghĩ về "front of house" và "back of house."
Nhưng khách hàng ghé thăm một nhà hàng có lẽ sẽ không nghĩ về các phần của nhà
hàng theo những thuật ngữ đó. Với pub use
, chúng ta có thể viết mã của mình
với một cấu trúc nhưng hiển thị một cấu trúc khác. Làm như vậy giúp thư viện của
chúng ta được tổ chức tốt cho các lập trình viên làm việc trên thư viện và các
lập trình viên gọi thư viện. Chúng ta sẽ xem một ví dụ khác về pub use
và cách
nó ảnh hưởng đến tài liệu crate của bạn trong "Xuất một Public API Tiện lợi với
pub use
" trong Chương 14.
Sử dụng Các Gói Bên ngoài
Trong Chương 2, chúng ta đã lập trình một dự án trò chơi đoán số sử dụng một gói
bên ngoài có tên là rand
để có được số ngẫu nhiên. Để sử dụng rand
trong dự
án của chúng ta, chúng ta đã thêm dòng này vào Cargo.toml:
rand = "0.8.5"
Thêm rand
như một phụ thuộc trong Cargo.toml cho Cargo biết phải tải xuống
gói rand
và bất kỳ phụ thuộc nào từ crates.io và làm cho
rand
có sẵn cho dự án của chúng ta.
Sau đó, để đưa các định nghĩa rand
vào phạm vi của gói của chúng ta, chúng ta
đã thêm một dòng use
bắt đầu bằng tên của crate, rand
, và liệt kê các item
chúng ta muốn đưa vào phạm vi. Hãy nhớ lại rằng trong "Tạo một Số Ngẫu
nhiên" trong Chương 2, chúng ta đã đưa trait Rng
vào
phạm vi và gọi hàm 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}");
}
Các thành viên của cộng đồng Rust đã làm cho nhiều gói có sẵn tại
crates.io, và việc kéo bất kỳ gói nào vào gói của bạn đều
liên quan đến các bước tương tự: liệt kê chúng trong tệp Cargo.toml của gói
của bạn và sử dụng use
để đưa các item từ các crate đó vào phạm vi.
Lưu ý rằng thư viện chuẩn std
cũng là một crate bên ngoài đối với gói của
chúng ta. Bởi vì thư viện chuẩn được đi kèm với ngôn ngữ Rust, chúng ta không
cần phải thay đổi Cargo.toml để bao gồm std
. Nhưng chúng ta cần tham chiếu
đến nó với use
để đưa các item từ đó vào phạm vi của gói của chúng ta. Ví dụ,
với HashMap
chúng ta sẽ sử dụng dòng này:
#![allow(unused)] fn main() { use std::collections::HashMap; }
Đây là một đường dẫn tuyệt đối bắt đầu bằng std
, tên của crate thư viện chuẩn.
Sử dụng Đường dẫn Lồng nhau để Làm sạch Danh sách use
Lớn
Nếu chúng ta sử dụng nhiều item được định nghĩa trong cùng một crate hoặc cùng
một module, việc liệt kê mỗi item trên một dòng riêng có thể chiếm nhiều không
gian dọc trong các tệp của chúng ta. Ví dụ, hai câu lệnh use
này chúng ta đã
có trong trò chơi đoán số trong Listing 2-4 đưa các item từ std
vào phạm vi:
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!"),
}
}
Thay vào đó, chúng ta có thể sử dụng đường dẫn lồng nhau để đưa các item tương tự vào phạm vi trong một dòng. Chúng ta làm điều này bằng cách chỉ định phần chung của đường dẫn, theo sau là hai dấu hai chấm, và sau đó là dấu ngoặc nhọn quanh danh sách các phần của đường dẫn khác nhau, như được hiển thị trong 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!"),
}
}
Trong các chương trình lớn hơn, việc đưa nhiều item vào phạm vi từ cùng một
crate hoặc module sử dụng đường dẫn lồng nhau có thể giảm đáng kể số lượng câu
lệnh use
riêng biệt cần thiết!
Chúng ta có thể sử dụng đường dẫn lồng nhau ở bất kỳ cấp độ nào trong một đường
dẫn, điều này rất hữu ích khi kết hợp hai câu lệnh use
có chung một đường dẫn
con. Ví dụ, Listing 7-19 thể hiện hai câu lệnh use
: một câu lệnh đưa std::io
vào phạm vi và một câu lệnh đưa std::io::Write
vào phạm vi.
use std::io;
use std::io::Write;
Phần chung của hai đường dẫn này là std::io
, và đó là toàn bộ đường dẫn đầu
tiên. Để hợp nhất hai đường dẫn này thành một câu lệnh use
, chúng ta có thể sử
dụng self
trong đường dẫn lồng nhau, như được hiển thị trong Listing 7-20.
use std::io::{self, Write};
Dòng này đưa std::io
và std::io::Write
vào phạm vi.
Toán tử Glob
Nếu chúng ta muốn đưa tất cả các item công khai được định nghĩa trong một
đường dẫn vào phạm vi, chúng ta có thể chỉ định đường dẫn đó theo sau bởi toán
tử glob *
:
#![allow(unused)] fn main() { use std::collections::*; }
Câu lệnh use
này đưa tất cả các item công khai được định nghĩa trong
std::collections
vào phạm vi hiện tại. Hãy cẩn thận khi sử dụng toán tử glob!
Glob có thể làm cho nó khó hơn để biết những tên nào đang trong phạm vi và tên
được sử dụng trong chương trình của bạn được định nghĩa ở đâu. Ngoài ra, nếu phụ
thuộc thay đổi các định nghĩa của nó, những gì bạn đã import cũng thay đổi theo,
điều này có thể dẫn đến lỗi trình biên dịch khi bạn nâng cấp phụ thuộc nếu phụ
thuộc đó thêm một định nghĩa có cùng tên với một định nghĩa của bạn trong cùng
phạm vi, chẳng hạn.
Toán tử glob thường được sử dụng khi kiểm thử để đưa mọi thứ đang được kiểm thử
vào module tests
; chúng ta sẽ nói về điều đó trong "Cách Viết Kiểm
thử" trong Chương 11. Toán tử glob cũng đôi khi
được sử dụng như một phần của mẫu prelude: xem
tài liệu thư viện chuẩn
để biết thêm thông tin về mẫu đó.