Rust 引用模塊項(xiàng)目的路徑

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

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

路徑有兩種形式:

  • 絕對(duì)路徑absolute path)從 crate 根開(kāi)始,以 crate 名或者字面值 ?crate ?開(kāi)頭。
  • 相對(duì)路徑relative path)從當(dāng)前模塊開(kāi)始,以 ?self?、?super ?或當(dāng)前模塊的標(biāo)識(shí)符開(kāi)頭。

絕對(duì)路徑和相對(duì)路徑都后跟一個(gè)或多個(gè)由雙冒號(hào)(::)分割的標(biāo)識(shí)符。

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

文件名: src/lib.rs

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

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

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

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

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

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

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

選擇使用相對(duì)路徑還是絕對(duì)路徑,還是要取決于你的項(xiàng)目。取決于你是更傾向于將項(xiàng)的定義代碼與使用該項(xiàng)的代碼分開(kāi)來(lái)移動(dòng),還是一起移動(dòng)。舉一個(gè)例子,如果我們要將 front_of_house 模塊和 eat_at_restaurant 函數(shù)一起移動(dòng)到一個(gè)名為 customer_experience 的模塊中,我們需要更新 add_to_waitlist 的絕對(duì)路徑,但是相對(duì)路徑還是可用的。然而,如果我們要將 eat_at_restaurant 函數(shù)單獨(dú)移到一個(gè)名為 dining 的模塊中,還是可以使用原本的絕對(duì)路徑來(lái)調(diào)用 add_to_waitlist,但是相對(duì)路徑必須要更新。我們更傾向于使用絕對(duì)路徑,因?yàn)榘汛a定義和項(xiàng)調(diào)用各自獨(dú)立地移動(dòng)是更常見(jiàn)的。

讓我們?cè)囍幾g一下示例 7-3,并查明為何不能編譯!示例 7-4 展示了這個(gè)錯(cuò)誤。

$ 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: 構(gòu)建示例 7-3 出現(xiàn)的編譯器錯(cuò)誤

錯(cuò)誤信息說(shuō) hosting 模塊是私有的。換句話(huà)說(shuō),我們擁有 hosting 模塊和 add_to_waitlist 函數(shù)的的正確路徑,但是 Rust 不讓我們使用,因?yàn)樗荒茉L(fǎng)問(wèn)私有片段。

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

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

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

使用 pub 關(guān)鍵字暴露路徑

讓我們回頭看一下示例 7-4 的錯(cuò)誤,它告訴我們 hosting 模塊是私有的。我們想讓父模塊中的 eat_at_restaurant 函數(shù)可以訪(fǎng)問(wèn)子模塊中的 add_to_waitlist 函數(shù),因此我們使用 pub 關(guān)鍵字來(lái)標(biāo)記 hosting 模塊,如示例 7-5 所示。

文件名: src/lib.rs

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

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

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

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

不幸的是,示例 7-5 的代碼編譯仍然有錯(cuò)誤,如示例 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: 構(gòu)建示例 7-5 出現(xiàn)的編譯器錯(cuò)誤

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

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

讓我們繼續(xù)將 pub 關(guān)鍵字放置在 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() {
    // 絕對(duì)路徑
    crate::front_of_house::hosting::add_to_waitlist();

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

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

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

在絕對(duì)路徑,我們從 crate,也就是 crate 根開(kāi)始。然后 crate 根中定義了 front_of_house 模塊。front_of_house 模塊不是公有的,不過(guò)因?yàn)?nbsp;eat_at_restaurant 函數(shù)與 front_of_house 定義于同一模塊中(即,eat_at_restaurant 和 front_of_house 是兄弟),我們可以從 eat_at_restaurant 中引用 front_of_house。接下來(lái)是使用 pub 標(biāo)記的 hosting 模塊。我們可以訪(fǎng)問(wèn) hosting 的父模塊,所以可以訪(fǎng)問(wèn) hosting。最后,add_to_waitlist 函數(shù)被標(biāo)記為 pub ,我們可以訪(fǎng)問(wèn)其父模塊,所以這個(gè)函數(shù)調(diào)用是有效的!

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

使用 super 起始的相對(duì)路徑

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

考慮一下示例 7-8 中的代碼,它模擬了廚師更正了一個(gè)錯(cuò)誤訂單,并親自將其提供給客戶(hù)的情況。fix_incorrect_order 函數(shù)通過(guò)指定的 super 起始的 serve_order 路徑,來(lái)調(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 開(kāi)頭的相對(duì)路徑從父目錄開(kāi)始調(diào)用函數(shù)

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

創(chuàng)建公有的結(jié)構(gòu)體和枚舉

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

文件名: 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() {
    // 在夏天訂購(gòu)一個(gè)黑麥土司作為早餐
    let mut meal = back_of_house::Breakfast::summer("Rye");
    // 改變主意更換想要面包的類(lèi)型
    meal.toast = String::from("Wheat");
    println!("I'd like {} toast please", meal.toast);

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

示例 7-9: 帶有公有和私有字段的結(jié)構(gòu)體

因?yàn)?nbsp;back_of_house::Breakfast 結(jié)構(gòu)體的 toast 字段是公有的,所以我們可以在 eat_at_restaurant 中使用點(diǎn)號(hào)來(lái)隨意的讀寫(xiě) toast 字段。注意,我們不能在 eat_at_restaurant 中使用 seasonal_fruit 字段,因?yàn)?nbsp;seasonal_fruit 是私有的。嘗試去除那一行修改 seasonal_fruit 字段值的代碼的注釋?zhuān)纯茨銜?huì)得到什么錯(cuò)誤!

還請(qǐng)注意一點(diǎn),因?yàn)?nbsp;back_of_house::Breakfast 具有私有字段,所以這個(gè)結(jié)構(gòu)體需要提供一個(gè)公共的關(guān)聯(lián)函數(shù)來(lái)構(gòu)造 Breakfast 的實(shí)例(這里我們命名為 summer)。如果 Breakfast 沒(méi)有這樣的函數(shù),我們將無(wú)法在 eat_at_restaurant 中創(chuàng)建 Breakfast 實(shí)例,因?yàn)槲覀儾荒茉?nbsp;eat_at_restaurant 中設(shè)置私有字段 seasonal_fruit 的值。

與之相反,如果我們將枚舉設(shè)為公有,則它的所有成員都將變?yōu)楣?。我們只需要?nbsp;enum 關(guān)鍵字前面加上 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: 設(shè)計(jì)公有枚舉,使其所有成員公有

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

還有一種使用 pub 的場(chǎng)景我們還沒(méi)有涉及到,那就是我們最后要講的模塊功能:use 關(guān)鍵字。我們將先單獨(dú)介紹 use,然后展示如何結(jié)合使用 pub 和 use


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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)