Bringing Paths into Scope with the use Keyword

Phải viết ra đường dẫn để gọi các hàm có thể cảm thấy không tiện lợi và lặp đi lặp lại. Trong Listing 7-7, dù chúng ta đã chọn đường dẫn tuyệt đối hay 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 front_of_househosting nữa. May mắn thay, có một cách để giảm bớt quá trình này: chúng ta có thể tạo một đường dẫn tắt 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 trong scope.

Trong Listing 7-11, chúng ta đưa module crate::front_of_house::hosting vào scope 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.

Filename: 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: Mang một module vào scope với use

Thêm use và đường dẫn trong một scope giống như 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 crate root, hosting là một tên hợp lệ trong scope đó, giống như module hosting đã được định nghĩa trong crate root. Đường dẫn được đưa vào scope với 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 đường dẫn tắt cho scope 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 customer, đó là một scope khác với câu lệnh use, vì vậy nội dung hàm sẽ không được biên dịch:

Filename: 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: Một câu lệnh use chỉ áp dụng trong scope nó đang ở

Lỗi biên dịch cho thấy đường dẫn tắt không còn áp dụng trong module 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

Lưu ý rằng còn có một cảnh báo rằng use không còn được sử dụng trong scope của nó! Để sửa vấn đề này, di chuyển use trong module customer nữa, hoặc tham chiếu đến đường dẫn tắt trong module cha với super::hosting trong module con customer.

Creating Idiomatic use Paths

Trong Listing 7-11, bạn có thể đã thắc mắc 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 đến hàm add_to_waitlist để đạt được kết quả giống như trong Listing 7-13.

Filename: 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: Đưa hàm add_to_waitlist vào scope với use, điều này không thường được dùng

Mặc dù cả Listing 7-11 và 7-13 đều đạt được cùng một nhiệm vụ, Listing 7-11 là cách hợp lệ để đưa một hàm vào scope với use. Đưa module cha của hàm vào scope 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 rằng hàm không được định nghĩa cục bộ trong lúc vẫn giảm thiểu sự lặp lại của đường dẫn đầy đủ. 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 struct, enum, và các mục khác với use, hợp lý là chỉ định đường dẫn đầy đủ. Listing 7-14 cho thấy cách hợp lý để đưa struct HashMap của thư viện chuẩn vào scope của một binary crate.

Filename: src/main.rs

use std::collections::HashMap;

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

Listing 7-14: Đưa HashMap vào scope một cách hợp lý

Không có lý do nào mạnh mẽ đằng sau quy tắc này: nó chỉ là quy ước đã xuất hiệ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 quy tắc này là nếu chúng ta đưa hai mục cùng tên vào scope với câu lệnh use, vì Rust không cho phép điều đó. Listing 7-15 cho thấy cách để đưa hai loại Result vào scope mà cùng tên nhưng module cha khác nhau và cách tham chiếu đến chúng.

Filename: src/lib.rs

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

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

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

Listing 7-15: Đưa hai loại cùng tên vào scope cùng một lúc yêu cầu sử dụng module cha của chúng.

Như bạn có thể thấy, sử dụng module cha để phân biệt hai loại Result. Nếu thay vì đó chúng ta chỉ định use std::fmt::Resultuse std::io::Result, chúng ta sẽ có hai loại Result trong cùng một scope và Rust sẽ không biết chúng ta muốn loại nào khi chúng ta sử dụng Result.


Providing New Names with the as Keyword

Có một cách khác để giải quyết vấn đề đưa hai loại cùng tên vào cùng một scope 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 bí danh, cho loại. Listing 7-16 cho thấy một cách khác để viết code trong Listing 7-15 bằng cách đổi tên một trong hai loại Result sử dụng as.

Filename: 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: Đổi tên một loại khi nó được đưa vào scope sử dụng từ khóa as

Trong câu lệnh use thứ hai, chúng ta đã chọn tên mới IoResult cho loại std::io::Result, nó sẽ không xung đột với Result từ std::fmt mà chúng ta cũng đã đưa vào scope. Listing 7-15 và Listing 7-16 được xem là tiêu chuẩn, vì vậy bạn có thể chọn một trong hai!

Re-exporting Names with pub use

Khi chúng ta dùng từ khóa use, tên mới được đưa vào chỉ hiện diện bên trong scope mà use được gọi. Để cho phép code bên ngoài có thể gọi đến tên đó chúng ta có thể kết hợp pubuse. Kỹ thuật này được gọi là export lại(re-exporting) vì chúng ta đang đưa một item vào scope nhưng cũng làm cho item đó có sẵn cho người khác để đưa vào scope của họ.

Listing 7-17 cho thấy code trong Listing 7-11 với use trong module gốc được đổi thành 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();
}

Listing 7-17: Làm cho một tên có sẵn cho bất kỳ code nào để sử dụng từ một scope mới với pub use

Trước khi thay đổi này diễn ra, code bên ngoài 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(). Bây giờ vì pub use đã export lại module hosting từ module gốc, code bên ngoài có thể sử dụng đường dẫn restaurant::hosting::add_to_waitlist() thay vì đường dẫn cũ.

Re-exporting rất hữu ích khi cấu trúc bên trong code của bạn khác với cách lập trình viên gọi code của bạn sẽ nghĩ về domain. Ví dụ, trong thí dụ nhà hàng ở trên, người điều hành nhà hàng nghĩ về “front of house” và “back of house”. Nhưng khách hàng đến nhà hàng có thể không nghĩ về các phần của nhà hàng theo cách đó. Với pub use, chúng ta có thể viết code của mình với một cấu trúc nhưng sẽ cho thấy một cấu trúc khác. Việc làm như vậy giúp code của chúng ta được tổ chức tốt cho cả các lập trình viên làm việc trên code của chúng ta và các lập trình viên gọi code của chúng ta. 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 của crate của bạn trong phần “Exporting a Convenient Public API with pub use của Chapter 14.

Using External Packages

Trong chương 2, chúng ta đã viết một project đoán số sử dụng một package bên ngoài gọi là rand để lấy ra các số ngẫu nhiên. Để sử dụng rand trong project của chúng ta, chúng ta đã thêm dòng này vào Cargo.toml:

Filename: Cargo.toml

rand = "0.8.5"

Thêm rand làm một dependency trong Cargo.toml cho biết Cargo sẽ tải về package rand và bất kỳ dependencies nào từ crates.io và cho phép rand có sẵn để dùng trong project của chúng ta.

Sau đó, để cho phép rand có thể được sử dụng trong scope của package, chúng ta đã thêm một dòng use bắt đầu với tên của crate, rand, và liệt kê những item mà chúng ta muốn cho phép sử dụng trong scope. Nhớ lại trong phần “Generating a Random Number” của chương 2, chúng ta đã import trait Rng vào scope 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}");
}

Thành viên của cộng đồng Rust đã làm rất nhiều package có sẵn tại crates.io, và để sử dụng bất kỳ package nào trong package của bạn, bạn chỉ cần làm những bước tương tự như trên: liệt kê chúng trong file Cargo.toml của package và sử dụng use để import các item từ package đó vào scope.

Lưu ý rằng thư viện chuẩn std cũng là một crate bên ngoài package của chúng ta. Vì thư viện chuẩn được cung cấp cùng 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 phải tham chiếu đến nó với use để import các item từ đó vào scope của package 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 với std, tên của crate thư viện chuẩn.

Using Nested Paths to Clean Up Large use Lists

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 sẽ chiếm rất nhiều không gian dọc trong file của chúng ta. Ví dụ, hai dòng use này chúng ta có trong game đoán số ở Listing 2-4 import các item từ std vào scope:

Filename: 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!"),
    }
}

Thay vì đó, chúng ta có thể sử dụng đường dẫn lồng nhau để import các item cùng vào scope 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 bởi hai dấu hai chấm, và sau đó là dấu ngoặc nhọn xung quanh một danh sách các phần của đường dẫn khác nhau, như trong Listing 7-18.

Filename: 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: Khai báo một đường dẫn lồng nhau để import nhiều item cùng với cùng một tiền tố vào scope

Trong các chương trình lớn hơn, việc import nhiều item từ cùng một crate hoặc module sử dụng đường dẫn lồng nhau có thể giảm số lượng các dòng use riêng biệt rất nhiều.

Chúng ta có thể sử dụng một đường dẫn lồng nhau ở bất kỳ mức độ nào trong một đường dẫn, điều này rất hữu ích khi kết hợp hai dòng use chia sẻ một đường dẫn con. Ví dụ, Listing 7-19 cho thấy hai dòng use: một trong đó import std::io vào scope và một trong đó import std::io::Write vào scope.

Filename: src/lib.rs

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

Listing 7-19: Hai dòng use trong đó một là một đường dẫn con của đường dẫn khác

Phần chung của hai đường dẫn này là std::io, và đó là đường dẫn đầu tiên hoàn chỉnh. Để kết hợp hai đường dẫn này thành một dòng use, chúng ta có thể sử dụng self trong đường dẫn lồng nhau, như trong Listing 7-20.

Filename: src/lib.rs

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

Listing 7-20: Kết hợp đường dẫn trong Listing 7-19 thành một dòng use

Dòng này import std::iostd::io::Write vào scope.

The Glob Operator

Nếu chúng ta muốn import tất cả các mục công khai được định nghĩa trong một đường dẫn vào scope, 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::*;
}

Dòng use này import tất cả các mục công khai được định nghĩa trong std::collections vào scope hiện tại. Hãy cẩn thận khi sử dụng toán tử glob! Toán tử glob có thể làm cho việc biết được tên nào đang trong scope và nơi một tên được sử dụng trong chương trình của bạn được định nghĩa trở nên khó khăn hơn.

Toán tử glob thường được sử dụng khi kiểm thử để import tất cả mọi thứ dưới kiểm thử vào module tests; chúng ta sẽ nói về điều đó trong phần “How to Write Tests” trong Chương 11. Toán tử glob cũng được sử dụng đôi khi là một phần của pattern prelude: xem documentation của thư viện chuẩn để biết thêm thông tin về pattern đó.