透過 use 關鍵字引入路徑

要是每次都得寫出呼叫函式的路徑的話是很冗長、重複且不方便的。舉例來說範例 7-7 我們在考慮要使用絕對或相對路徑來呼叫 add_to_waitlist 函式時,每次想要呼叫 add_to_waitlist 我們都得指明 front_of_house 以及 hosting。幸運的是,我們有簡化過程的辦法:我們可以用 use 關鍵字建立路徑的捷徑,然後在作用域內透過更短的名稱來使用。

在範例 7-11 中,我們引入了 crate::front_of_house::hosting 模組進 eat_at_restaurant 函式的作用域中,所以我們要呼叫函式 add_to_waitlist 的話我們只需要指明 hosting::add_to_waitlist

檔案名稱: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();
}

範例 7-11:使用 use 將模組引入

使用 use 將路徑引入作用域就像是在檔案系統中產生符號連結一樣(symbolic link)。在 crate 源頭加上 use crate::front_of_house::hosting 後,hosting 在作用域內就是個有效的名稱了。使用 use 的路徑也會檢查隱私權,就像其他路徑一樣。

注意到 use 只會在它所位在的特定作用域內建立捷徑。範例 7-12 將 eat_at_restaurant 移入子模組 customer,這樣就會與 use 陳述式的作用域不同,所以其函式本體將無法編譯。

檔案名稱: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();
    }
}

範例 7-12:use 陳述式只適用於所在的作用域

編譯器錯誤顯示了該捷徑無法用在 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`

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` due to previous error; 1 warning emitted

你會發現還有另外一個警告說明 use 在它的作用域中並沒有被用到!要解決此問題的話,我們可以將 use 也移動到 customer 模組內,或是在customer 子模組透過 super::hosting 參考上層模組的捷徑。

建立慣用的 use 路徑

在範例 7-11 你可能會好奇為何我們指明 use crate::front_of_house::hosting 然後在 eat_at_restaurant 呼叫,而不是直接用 use 指明 add_to_waitlist 函式的整個路徑就好。像範例 7-13 這樣寫。

檔案名稱: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();
}

範例 7-13:使用 useadd_to_waitlist 函式引入作用域,但這較不符合習慣

雖然範例 7-11 與範例 7-13 都能完成相同的任務,但是範例 7-11 的做法比較符合習慣用法。使用 use 將函式的上層模組引入作用域,讓我們必須在呼叫函式時得指明對應模組。在呼叫函式時指定上層模組能清楚地知道該函式並非本地定義的,同時一樣能簡化路徑。範例 7-13 的程式碼會不清楚 add_to_waitlist 是在哪定義的。

另一方面,如果是要使用 use 引入結構體、列舉或其他項目的話,直接指明完整路徑反而是符合習慣的方式。範例 7-14 顯示了將標準函式庫的 HashMap 引入執行檔 crate 作用域的習慣用法。

檔案名稱:src/main.rs

use std::collections::HashMap;

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

範例 7-14:引入 HashMap 進作用域的習慣用法

此習慣沒什麼強硬的理由:就只是大家已經習慣這樣的用法來讀寫 Rust 的程式碼。

這樣的習慣有個例外,那就是如果我們將兩個相同名稱的項目使用 use 陳述式引入作用域時,因為 Rust 不會允許。範例 7-15 展示了如何引入兩個同名但屬於不同模組的 Result 型別進作用域中並使用的方法。

檔案名稱:src/lib.rs

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

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

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

範例 7-15:要將兩個同名的型別引入相同作用域的話,必須使用它們所屬的模組

如同你所見使用對應的模組可以分辨出是在使用哪個 Result 型別。如果我們直接指明 use std::fmt::Resultuse std::io::Result 的話,我們會在同一個作用域中擁有兩個 Result 型別,這樣一來 Rust 就無法知道我們想用的 Result 是哪一個。

使用 as 關鍵字提供新名稱

要在相同作用域中使用 use 引入兩個同名型別的話,還有另一個辦法。在路徑之後,我們可以用 as 指定一個該型別在本地的新名稱,或者說別名(alias)。範例 7-16 展示重寫了範例 7-15,將其中一個 Result 型別使用 as 重新命名。

檔案名稱:src/lib.rs

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

fn function1() -> Result {
    // --省略--
    Ok(())
}

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

範例 7-16:使用 as 將型別引入作用域的同時重新命名

在第二個 use 陳述式,我們選擇了將 std::io::Result 型別重新命名為 IoResult,這樣就不會和同樣引入作用域內 std::fmtResult 有所衝突。範例 7-15 與 範例 7-16 都屬於習慣用法,你可以選擇你比較喜歡的方式!

使用 pub use 重新匯出名稱

當我們使用 use 關鍵字將名稱引入作用域時,該有效名稱在新的作用域中是私有的。要是我們希望呼叫我們這段程式碼時,也可以使用這個名稱的話(就像該名稱是在此作用域內定義的),我們可以組合 pubuse。這樣的技巧稱之為重新匯出(re-exporting),因為我們將項目引入作用域,並同時公開給其他作用域參考。

範例 7-17 將範例 7-11 在源頭模組中原本的 use 改成 pub use

檔案名稱: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();
}

範例 7-17:使用 pub use 使名稱公開給任何程式的作用域中參考

在此之前,外部程式碼會需要透過 restaurant::front_of_house::hosting::add_to_waitlist() 這樣的路徑才能呼叫 add_to_waitlist。現在 pub use 從源頭模組重新匯出了 hosting 模組,外部程式碼現在可以使用 restaurant::hosting::add_to_waitlist() 這樣的路徑就好。

當程式碼的內部結構與使用程式的開發者對於該領域所想像的結構不同時,重新匯出會很有用。我們再次用餐廳做比喻的話就像是,經營餐廳的人可能會想像餐廳是由「前台」與「後台」所組成,但光顧的顧客可能不會用這些術語來描繪餐廳的每個部分。使用 pub use 的話,我們可以用某種架構寫出程式碼,再以不同的架構對外公開。這樣讓我們的的函式庫可以完整的組織起來,且對開發函式庫的開發者與使用函式庫的開發者都提供友善的架構。我們會在第十四章的「透過 pub use 匯出理想的公開 API」段落再看看另一個 pub use 的範例並了解它會如何影響 crate 的技術文件。

使用外部套件

在第二章我們寫了一支猜謎遊戲專案時,有用到一個外部套件叫做 rand 來取得隨機數字。要在專案內使用 rand 的話,我們會在 Cargo.toml 加上此行:

檔案名稱:Cargo.toml

rand = "0.8.5"

Cargo.toml 新增 rand 作為依賴函式庫會告訴 Cargo 要從 crates.io 下載 rand 以及其他相關的依賴,讓我們的專案可以使用 rand

接下來要將 rand 的定義引入我們套件的作用域的話,我們加上一行 use 後面接著 crate 的名稱 rand 然後列出我們想要引入作用域的項目。回想一下在第二章「產生隨機數字」的段落,我們將 Rng 特徵引入作用域中,並呼叫函式 rand::thread_rng

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

fn main() {
    println!("請猜測一個數字!");

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

    println!("祕密數字為:{secret_number}");

    println!("請輸入你的猜測數字。");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("讀取該行失敗");

    println!("你的猜測數字:{guess}");
}

Rust 社群成員在 crates.io 發佈了不少套件可供使用,要將這些套件引入到你的套件的步驟是一樣的。在你的套件的 Cargo.toml 檔案列出它們,然後使用 use 將這些 crate 內的項目引入作用域中。

請注意到標準函式庫 std 對於我們的套件來說也是一個外部 crate。由於標準函式庫會跟著 Rust 語言發佈,所以我們不需要更改 Cargo.toml 來包含 std。但是我們仍然需使用 use 來將它的項目引入我們套件的作用域中。舉例來說,要使用 HashMap 我們可以這樣寫:

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

這是個用標準函式庫的 crate 名稱 std 起頭的絕對路徑。

使用巢狀路徑來清理大量的 use 行數

如果我們要使用在相同 crate 或是相同模組內定義的數個項目,針對每個項目都單獨寫一行的話,會佔據我們檔案內很多空間。舉例來說,範例 2-4 中的猜謎遊戲我們用了這兩個 use 陳述式來引入作用域中:

檔案名稱:src/main.rs

use rand::Rng;
// --省略--
use std::cmp::Ordering;
use std::io;
// --省略--

fn main() {
    println!("請猜測一個數字!");

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

    println!("祕密數字為:{secret_number}");

    println!("請輸入你的猜測數字。");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("讀取行數失敗");

    println!("你的猜測數字:{guess}");

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("太小了!"),
        Ordering::Greater => println!("太大了!"),
        Ordering::Equal => println!("獲勝!"),
    }
}

我們可以改使用巢狀路徑(nested paths)來只用一行就能將數個項目引入作用域中。我們先指明相同路徑的部分,加上雙冒號,然後在大括號內列出各自不同的路徑部分,如範例 7-18 所示。

檔案名稱:src/main.rs

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

fn main() {
    println!("請猜測一個數字!");

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

    println!("祕密數字為:{secret_number}");

    println!("請輸入你的猜測數字。");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("讀取行數失敗");

    let guess: u32 = guess.trim().parse().expect("請輸入一個數字!");

    println!("你的猜測數字:{guess}");

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("太小了!"),
        Ordering::Greater => println!("太大了!"),
        Ordering::Equal => println!("獲勝!"),
    }
}

範例 7-18:使用巢狀路徑引入有部分相同前綴的數個路徑至作用域中

在較大的程式中,使用巢狀路徑將相同 crate 或相同模組中的許多項目引入作用域,可以大量減少 use 陳述式的數量!

我們可以在路徑中的任何部分使用巢狀路徑,這在組合兩個享有相同子路徑的 use 陳述式時非常有用。舉例來說,範例 7-19 顯示了兩個 use 陳述式:一個將 std::io 引入作用域,另一個將 std::io::Write 引入作用域。

檔案名稱:src/lib.rs

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

範例 7-19:兩個 use 陳述式且其中一個是另一個的子路徑

這兩個路徑的相同部分是 std::io,這也是整個第一個路徑。要將這兩個路徑合為一個 use 陳述式的話,我們可以在巢狀路徑使用 self,如範例 7-20 所示。

檔案名稱:src/lib.rs

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

範例 7-20:組合範例 7-19 的路徑為一個 use 陳述式

此行就會將 std::iostd::io::Write 引入作用域。

全域運算子

如果我們想要將在一個路徑中所定義的所有公開項目引入作用域的話,我們可以在指明路徑之後加上全域(glob)運算子 *

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

use 陳述式會將 std::collections 定義的所有公開項目都引入作用域中。不過請小心使用全域運算子!它容易讓我們無法分辨作用域內的名稱,以及程式中使用的名稱是從哪定義來的。

全域運算子很常用在 tests 模組下,將所有東西引入測試中。我們會在第十一章的「如何寫測試」段落來討論。全域運算子也常拿來用在 prelude 模式中,你可以查閱標準函式庫的技術文件來瞭解此模式的更多資訊。