Rust 引用模塊項目的路徑

2023-03-22 15:09 更新
ch07-03-paths-for-referring-to-an-item-in-the-module-tree.md
commit 9c0fa2714859738ff73cbbb829592e4c037d7e46

來看一下 Rust 如何在模塊樹中找到一個項的位置,我們使用路徑的方式,就像在文件系統(tǒng)使用路徑一樣。如果我們想要調(diào)用一個函數(shù),我們需要知道它的路徑。

路徑有兩種形式:

  • 絕對路徑absolute path)從 crate 根開始,以 crate 名或者字面值 ?crate ?開頭。
  • 相對路徑relative path)從當前模塊開始,以 ?self?、?super ?或當前模塊的標識符開頭。

絕對路徑和相對路徑都后跟一個或多個由雙冒號(::)分割的標識符。

讓我們回到示例 7-1。我們?nèi)绾握{(diào)用 add_to_waitlist 函數(shù)?還是同樣的問題,add_to_waitlist 函數(shù)的路徑是什么?在示例 7-3 中,我們通過刪除一些模塊和函數(shù),稍微簡化了一下我們的代碼。我們在 crate 根定義了一個新函數(shù) eat_at_restaurant,并在其中展示調(diào)用 add_to_waitlist 函數(shù)的兩種方法。eat_at_restaurant 函數(shù)是我們 crate 庫的一個公共 API,所以我們使用 pub 關鍵字來標記它。在 “使用pub關鍵字暴露路徑” 一節(jié),我們將詳細介紹 pub。注意,這個例子無法編譯通過,我們稍后會解釋原因。

文件名: src/lib.rs

mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // 絕對路徑
    crate::front_of_house::hosting::add_to_waitlist();

    // 相對路徑
    front_of_house::hosting::add_to_waitlist();
}

示例 7-3: 使用絕對路徑和相對路徑來調(diào)用 add_to_waitlist 函數(shù)

第一種方式,我們在 eat_at_restaurant 中調(diào)用 add_to_waitlist 函數(shù),使用的是絕對路徑。add_to_waitlist 函數(shù)與 eat_at_restaurant 被定義在同一 crate 中,這意味著我們可以使用 crate 關鍵字為起始的絕對路徑。

在 crate 后面,我們持續(xù)地嵌入模塊,直到我們找到 add_to_waitlist。你可以想象出一個相同結構的文件系統(tǒng),我們通過指定路徑 /front_of_house/hosting/add_to_waitlist 來執(zhí)行 add_to_waitlist 程序。我們使用 crate 從 crate 根開始就類似于在 shell 中使用 / 從文件系統(tǒng)根開始。

第二種方式,我們在 eat_at_restaurant 中調(diào)用 add_to_waitlist,使用的是相對路徑。這個路徑以 front_of_house 為起始,這個模塊在模塊樹中,與 eat_at_restaurant 定義在同一層級。與之等價的文件系統(tǒng)路徑就是 front_of_house/hosting/add_to_waitlist。以名稱為起始,意味著該路徑是相對路徑。

選擇使用相對路徑還是絕對路徑,還是要取決于你的項目。取決于你是更傾向于將項的定義代碼與使用該項的代碼分開來移動,還是一起移動。舉一個例子,如果我們要將 front_of_house 模塊和 eat_at_restaurant 函數(shù)一起移動到一個名為 customer_experience 的模塊中,我們需要更新 add_to_waitlist 的絕對路徑,但是相對路徑還是可用的。然而,如果我們要將 eat_at_restaurant 函數(shù)單獨移到一個名為 dining 的模塊中,還是可以使用原本的絕對路徑來調(diào)用 add_to_waitlist,但是相對路徑必須要更新。我們更傾向于使用絕對路徑,因為把代碼定義和項調(diào)用各自獨立地移動是更常見的。

讓我們試著編譯一下示例 7-3,并查明為何不能編譯!示例 7-4 展示了這個錯誤。

$ cargo build
   Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: module `hosting` is private
 --> src/lib.rs:9:28
  |
9 |     crate::front_of_house::hosting::add_to_waitlist();
  |                            ^^^^^^^ private module
  |
note: the module `hosting` is defined here
 --> src/lib.rs:2:5
  |
2 |     mod hosting {
  |     ^^^^^^^^^^^

error[E0603]: module `hosting` is private
  --> src/lib.rs:12:21
   |
12 |     front_of_house::hosting::add_to_waitlist();
   |                     ^^^^^^^ private module
   |
note: the module `hosting` is defined here
  --> src/lib.rs:2:5
   |
2  |     mod hosting {
   |     ^^^^^^^^^^^

For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` due to 2 previous errors

示例 7-4: 構建示例 7-3 出現(xiàn)的編譯器錯誤

錯誤信息說 hosting 模塊是私有的。換句話說,我們擁有 hosting 模塊和 add_to_waitlist 函數(shù)的的正確路徑,但是 Rust 不讓我們使用,因為它不能訪問私有片段。

模塊不僅對于你組織代碼很有用。他們還定義了 Rust 的 私有性邊界privacy boundary):這條界線不允許外部代碼了解、調(diào)用和依賴被封裝的實現(xiàn)細節(jié)。所以,如果你希望創(chuàng)建一個私有函數(shù)或結構體,你可以將其放入模塊。

Rust 中默認所有項(函數(shù)、方法、結構體、枚舉、模塊和常量)都是私有的。父模塊中的項不能使用子模塊中的私有項,但是子模塊中的項可以使用他們父模塊中的項。這是因為子模塊封裝并隱藏了他們的實現(xiàn)詳情,但是子模塊可以看到他們定義的上下文。繼續(xù)拿餐館作比喻,把私有性規(guī)則想象成餐館的后臺辦公室:餐館內(nèi)的事務對餐廳顧客來說是不可知的,但辦公室經(jīng)理可以洞悉其經(jīng)營的餐廳并在其中做任何事情。

Rust 選擇以這種方式來實現(xiàn)模塊系統(tǒng)功能,因此默認隱藏內(nèi)部實現(xiàn)細節(jié)。這樣一來,你就知道可以更改內(nèi)部代碼的哪些部分而不會破壞外部代碼。你還可以通過使用 pub 關鍵字來創(chuàng)建公共項,使子模塊的內(nèi)部部分暴露給上級模塊。

使用 pub 關鍵字暴露路徑

讓我們回頭看一下示例 7-4 的錯誤,它告訴我們 hosting 模塊是私有的。我們想讓父模塊中的 eat_at_restaurant 函數(shù)可以訪問子模塊中的 add_to_waitlist 函數(shù),因此我們使用 pub 關鍵字來標記 hosting 模塊,如示例 7-5 所示。

文件名: src/lib.rs

mod front_of_house {
    pub mod hosting {
        fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // 絕對路徑
    crate::front_of_house::hosting::add_to_waitlist();

    // 相對路徑
    front_of_house::hosting::add_to_waitlist();
}

示例 7-5: 使用 pub 關鍵字聲明 hosting 模塊使其可在 eat_at_restaurant 使用

不幸的是,示例 7-5 的代碼編譯仍然有錯誤,如示例 7-6 所示。

$ cargo build
   Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: function `add_to_waitlist` is private
 --> src/lib.rs:9:37
  |
9 |     crate::front_of_house::hosting::add_to_waitlist();
  |                                     ^^^^^^^^^^^^^^^ private function
  |
note: the function `add_to_waitlist` is defined here
 --> src/lib.rs:3:9
  |
3 |         fn add_to_waitlist() {}
  |         ^^^^^^^^^^^^^^^^^^^^

error[E0603]: function `add_to_waitlist` is private
  --> src/lib.rs:12:30
   |
12 |     front_of_house::hosting::add_to_waitlist();
   |                              ^^^^^^^^^^^^^^^ private function
   |
note: the function `add_to_waitlist` is defined here
  --> src/lib.rs:3:9
   |
3  |         fn add_to_waitlist() {}
   |         ^^^^^^^^^^^^^^^^^^^^

For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` due to 2 previous errors

示例 7-6: 構建示例 7-5 出現(xiàn)的編譯器錯誤

發(fā)生了什么?在 mod hosting 前添加了 pub 關鍵字,使其變成公有的。伴隨著這種變化,如果我們可以訪問 front_of_house,那我們也可以訪問 hosting。但是 hosting 的 內(nèi)容contents) 仍然是私有的;這表明使模塊公有并不使其內(nèi)容也是公有的。模塊上的 pub 關鍵字只允許其父模塊引用它。

示例 7-6 中的錯誤說,add_to_waitlist 函數(shù)是私有的。私有性規(guī)則不但應用于模塊,還應用于結構體、枚舉、函數(shù)和方法。

讓我們繼續(xù)將 pub 關鍵字放置在 add_to_waitlist 函數(shù)的定義之前,使其變成公有。如示例 7-7 所示。

文件名: src/lib.rs

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // 絕對路徑
    crate::front_of_house::hosting::add_to_waitlist();

    // 相對路徑
    front_of_house::hosting::add_to_waitlist();
}

示例 7-7: 為 mod hosting 和 fn add_to_waitlist 添加 pub 關鍵字使他們可以在 eat_at_restaurant 函數(shù)中被調(diào)用

現(xiàn)在代碼可以編譯通過了!讓我們看看絕對路徑和相對路徑,并根據(jù)私有性規(guī)則,再檢查一下為什么增加 pub 關鍵字使得我們可以在 add_to_waitlist 中調(diào)用這些路徑。

在絕對路徑,我們從 crate,也就是 crate 根開始。然后 crate 根中定義了 front_of_house 模塊。front_of_house 模塊不是公有的,不過因為 eat_at_restaurant 函數(shù)與 front_of_house 定義于同一模塊中(即,eat_at_restaurant 和 front_of_house 是兄弟),我們可以從 eat_at_restaurant 中引用 front_of_house。接下來是使用 pub 標記的 hosting 模塊。我們可以訪問 hosting 的父模塊,所以可以訪問 hosting。最后,add_to_waitlist 函數(shù)被標記為 pub ,我們可以訪問其父模塊,所以這個函數(shù)調(diào)用是有效的!

在相對路徑,其邏輯與絕對路徑相同,除了第一步:不同于從 crate 根開始,路徑從 front_of_house 開始。front_of_house 模塊與 eat_at_restaurant 定義于同一模塊,所以從 eat_at_restaurant 中開始定義的該模塊相對路徑是有效的。接下來因為 hosting 和 add_to_waitlist 被標記為 pub,路徑其余的部分也是有效的,因此函數(shù)調(diào)用也是有效的!

使用 super 起始的相對路徑

我們還可以使用 super 開頭來構建從父模塊開始的相對路徑。這么做類似于文件系統(tǒng)中以 .. 開頭的語法。我們?yōu)槭裁匆@樣做呢?

考慮一下示例 7-8 中的代碼,它模擬了廚師更正了一個錯誤訂單,并親自將其提供給客戶的情況。fix_incorrect_order 函數(shù)通過指定的 super 起始的 serve_order 路徑,來調(diào)用 serve_order 函數(shù):

文件名: src/lib.rs

fn serve_order() {}

mod back_of_house {
    fn fix_incorrect_order() {
        cook_order();
        super::serve_order();
    }

    fn cook_order() {}
}

示例 7-8: 使用以 super 開頭的相對路徑從父目錄開始調(diào)用函數(shù)

fix_incorrect_order 函數(shù)在 back_of_house 模塊中,所以我們可以使用 super 進入 back_of_house 父模塊,也就是本例中的 crate 根。在這里,我們可以找到 serve_order。成功!我們認為 back_of_house 模塊和 serve_order 函數(shù)之間可能具有某種關聯(lián)關系,并且,如果我們要重新組織這個 crate 的模塊樹,需要一起移動它們。因此,我們使用 super,這樣一來,如果這些代碼被移動到了其他模塊,我們只需要更新很少的代碼。

創(chuàng)建公有的結構體和枚舉

我們還可以使用 pub 來設計公有的結構體和枚舉,不過有一些額外的細節(jié)需要注意。如果我們在一個結構體定義的前面使用了 pub ,這個結構體會變成公有的,但是這個結構體的字段仍然是私有的。我們可以根據(jù)情況決定每個字段是否公有。在示例 7-9 中,我們定義了一個公有結構體 back_of_house:Breakfast,其中有一個公有字段 toast 和私有字段 seasonal_fruit。這個例子模擬的情況是,在一家餐館中,顧客可以選擇隨餐附贈的面包類型,但是廚師會根據(jù)季節(jié)和庫存情況來決定隨餐搭配的水果。餐館可用的水果變化是很快的,所以顧客不能選擇水果,甚至無法看到他們將會得到什么水果。

文件名: src/lib.rs

mod back_of_house {
    pub struct Breakfast {
        pub toast: String,
        seasonal_fruit: String,
    }

    impl Breakfast {
        pub fn summer(toast: &str) -> Breakfast {
            Breakfast {
                toast: String::from(toast),
                seasonal_fruit: String::from("peaches"),
            }
        }
    }
}

pub fn eat_at_restaurant() {
    // 在夏天訂購一個黑麥土司作為早餐
    let mut meal = back_of_house::Breakfast::summer("Rye");
    // 改變主意更換想要面包的類型
    meal.toast = String::from("Wheat");
    println!("I'd like {} toast please", meal.toast);

    // 如果取消下一行的注釋代碼不能編譯;
    // 不允許查看或修改早餐附帶的季節(jié)水果
    // meal.seasonal_fruit = String::from("blueberries");
}

示例 7-9: 帶有公有和私有字段的結構體

因為 back_of_house::Breakfast 結構體的 toast 字段是公有的,所以我們可以在 eat_at_restaurant 中使用點號來隨意的讀寫 toast 字段。注意,我們不能在 eat_at_restaurant 中使用 seasonal_fruit 字段,因為 seasonal_fruit 是私有的。嘗試去除那一行修改 seasonal_fruit 字段值的代碼的注釋,看看你會得到什么錯誤!

還請注意一點,因為 back_of_house::Breakfast 具有私有字段,所以這個結構體需要提供一個公共的關聯(lián)函數(shù)來構造 Breakfast 的實例(這里我們命名為 summer)。如果 Breakfast 沒有這樣的函數(shù),我們將無法在 eat_at_restaurant 中創(chuàng)建 Breakfast 實例,因為我們不能在 eat_at_restaurant 中設置私有字段 seasonal_fruit 的值。

與之相反,如果我們將枚舉設為公有,則它的所有成員都將變?yōu)楣?。我們只需要?nbsp;enum 關鍵字前面加上 pub,就像示例 7-10 展示的那樣。

文件名: src/lib.rs

mod back_of_house {
    pub enum Appetizer {
        Soup,
        Salad,
    }
}

pub fn eat_at_restaurant() {
    let order1 = back_of_house::Appetizer::Soup;
    let order2 = back_of_house::Appetizer::Salad;
}

示例 7-10: 設計公有枚舉,使其所有成員公有

因為我們創(chuàng)建了名為 Appetizer 的公有枚舉,所以我們可以在 eat_at_restaurant 中使用 Soup 和 Salad 成員。如果枚舉成員不是公有的,那么枚舉會顯得用處不大;給枚舉的所有成員挨個添加 pub 是很令人惱火的,因此枚舉成員默認就是公有的。結構體通常使用時,不必將它們的字段公有化,因此結構體遵循常規(guī),內(nèi)容全部是私有的,除非使用 pub 關鍵字。

還有一種使用 pub 的場景我們還沒有涉及到,那就是我們最后要講的模塊功能:use 關鍵字。我們將先單獨介紹 use,然后展示如何結合使用 pub 和 use


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號