2020年3月30日

rust07 Packages, Crates, and Modules

程式都會遇到 scope 的問題,在某一個時間點,compiler 知道些變數,允許呼叫哪些函數,變數引用是什麼?

rust 有 module system

  • packages: 是 cargo 的功能,可建立、測試、分享 crate
  • crates 是一個 module 的樹狀結構,有 library 或 binary project
  • modules 和 use: 允許你控制 scope 及 privacy of paths
  • paths: 對 struct, function, module 命名的方法

Packages and Crate

  • crate 是 binary 或是 library
  • crate root 一份描述如何建構 create 的文件
  • 一個 Package 是一個或多個 crates
  • Package 透過 Cargo.toml 用來描述建構 crates

  • package 可包含 0 或 1 個 library crate,不能超過。可包含多個 binary crates。至少要包含一個 crate (library 或 binary 都可以)

cargo new 就是在產生一個新的 package

$ cargo new my-project
     Created binary (application) `my-project` package

cargo package 預設就是在 Cargo.toml 同一層的目錄中,有包含 src 目錄,且裡面有 main.rs 時, cargo 就知道這個 package 帶有一個跟 package 同名的 binary crate,且 src/main.rs 就是 crate root。另外如果 src/lib.rs,其 package 帶有同名的 library crate,且 src/lib.rs 是 crate root。 crate root 檔案將會傳給 rust 用來建構 library 或是 binary。

package 可包含 0 或 1 個 library crate,不能超過。可包含多個 binary crates。至少要包含一個 crate (library 或 binary 都可以)

如果 package 同時包含 src/main.rs 及 src/lib.rs,就是帶有兩個 crate: 一個同名 library,一個同名 binary。package 可帶有多個 binary crate,要將其檔案放在 src/bin 目錄,每個檔案都是一個獨立的 binary crate。

Defining Modules to Control Scope and Privacy

module system

  • module: 組織 code 與控制路徑私有性的方法
  • paths: 命名 items 的方法
  • use: 將某個 path 帶入 scope
  • pub: 開放 items,讓 items 變成公有
  • as: 將 item 引入 scope 時,用來 rename
  • external packages
  • glob operator: 將 module 的所有內容引入 scope
  • 將不同 module 分割到獨立的檔案中

module 可組織程式碼,將 crate 分組,增加 readability 與 reuse 便利性。module 也可以控制 privacy of items,item 可以是 public (可被 outside code 使用),或是 private,只是 internal implementation。

以下是 restaurant 的 library crate 範例

在一個餐廳可分兩個部分:front of house 及 back of house。front of house 包含 seating customers, 收單的 servers,payment,製作飲料的 bartenders。back of house 包含chef,在廚房燒菜,洗碗機,及負責管理的 managers。

首先建立一個新的 library

cargo new --lib restaurant

修改 src/lib.rs

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

        fn seat_at_table() {}
    }

    mod serving {
        fn take_order() {}

        fn serve_order() {}

        fn take_payment() {}
    }
}

modules 可包含其他 items 的定義:structs, enums, constants, traits, functions

module 可將相關的定義放在一起。

src/main.rs, src/lib.rs 分別稱為 crate roots

以上就是產生了這樣的 module tree。類似 file system 的目錄樹,另一個類似的地方,是需要用 path 來找到適當的 module 或 item。

crate
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist
     │   └── seat_at_table
     └── serving
         ├── take_order
         ├── serve_order
         └── take_payment

Paths for Referring to an item in the Module Tree

rust 以 path 在module tree 中定位 item,如果要呼叫某個 function 就必須要知道他的 path

path 有兩種形式,兩種都有一或多個 identifiers,id 之間以 :: 區隔

  • absolute path: 從 crate root 開始,以 crate 開頭
  • relative path: 從目前的 module 開始,以 self, super 或目前的 module 的 id 開始
mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // Absolute path
    crate::front_of_house::hosting::add_to_waitlist();

    // Relative path
    front_of_house::hosting::add_to_waitlist();
}

在編譯時,會發生 error ,這是 privacy 的問題

error[E0603]: function `add_to_waitlist` is private
 --> src/lib.rs:9:37
  |
9 |     crate::front_of_house::hosting::add_to_waitlist();
  |                                     ^^^^^^^^^^^^^^^

error[E0603]: function `add_to_waitlist` is private
  --> src/lib.rs:12:30
   |
12 |     front_of_house::hosting::add_to_waitlist();
   |                              ^^^^^^^^^^^^^^^

rust 的所有 items: funcitons, methods, structs, enums, modules, constants 預設都是 private。在 parent module 的 items 不能使用 child modules 裡面的 private items。但 child modules 的 items 可以使用 ancestor modules 的 items。原因是 child modules 會 wrap & hide implementation details,但 child modules 可看到跟其定義相同的 context 的其他 items。

rust 會隱藏 inner implementation details,因此我們可知道如何在不影響 outer code 的狀況下,改變 inner code。可利用 pub keyword 讓 item 變成 public。

Exposing Paths with pub

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

// 因為 eat_at_restaurant 跟 front_of_house 是相同的 module,故可直接使用 front_of_house
pub fn eat_at_restaurant() {
    // Absolute path
    crate::front_of_house::hosting::add_to_waitlist();

    // Relative path
    front_of_house::hosting::add_to_waitlist();
}

Starting Relative Paths with super

super 類似 filesystem 的 .. 用途,代表 parent module。

backofhouse 跟 serve_order 在相同的 module

fn serve_order() {}

mod back_of_house {
    fn fix_incorrect_order() {
        cook_order();

        // super 就等同於 crate
        super::serve_order();
    }

    fn cook_order() {}
}

Making Structs and Enums Public

如果在 struct 定義前面加上 pub,可讓 struct 變成 public,但 struct 裡面的欄位還是 private。必須單獨每一個欄位分開設定 pub

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

    // 為 Breakfast 增加 public method: summer
    impl Breakfast {
        pub fn summer(toast: &str) -> Breakfast {
            Breakfast {
                toast: String::from(toast),
                seasonal_fruit: String::from("peaches"),
            }
        }
    }
}

pub fn eat_at_restaurant() {
    // Order a breakfast in the summer with Rye toast 訂早餐
    let mut meal = back_of_house::Breakfast::summer("Rye");

    // Change our mind about what bread we'd like 修改餐點
    meal.toast = String::from("Wheat");
    println!("I'd like {} toast please", meal.toast);

    // The next line won't compile if we uncomment it; we're not allowed
    // to see or modify the seasonal fruit that comes with the meal
    // 無法修改 seasonal_fruit,因為該欄位是 private
    // meal.seasonal_fruit = String::from("blueberries");
}

對於將 enum 設定為 public,只需要在定義的地方加上 pub,就會讓所有 variants 都是 public

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;
}

Bringing Paths info Scope with use

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

// 原本在使用 hosting,必須要透過 相對或絕對路徑
// 透過 use 宣告後,後面就可直接使用 hosting
use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}

use 要使用相對路徑有一點點不同,路徑必須要用 self 開頭。不過目前實測,如果不寫 self 也沒有問題。

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

// self 也可以不寫
//use self::front_of_house::hosting;
use front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}

Creating Idiomatic use Paths

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

// use 可以將 add_to_waitlist 引入,這樣後面的程式,就可以直接呼叫這個 fn
// 但這樣是不好的寫法
// 因為使用 add_to_waitlist 時,感覺 add_to_waitlist 是定義在這個 module 裡面
// 而上面 hosting::add_to_waitlist(); 這樣的呼叫方式
// 比較清楚知道,add_to_waitlist 是定義在其他地方
use crate::front_of_house::hosting::add_to_waitlist;

pub fn eat_at_restaurant() {
    add_to_waitlist();
    add_to_waitlist();
    add_to_waitlist();
}

當 use structs, enums, 其他 items 時,最好指定 full path。例如指定使用標準函式庫的 HashMap

use std::collections::HashMap;

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

唯一的例外是,在 scope 中,使用了兩個相同名稱的 items,這時必須要加上 parent module,用來區分 items

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

fn function1() -> fmt::Result {
}

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

Providing New Names with as

使用相同名稱的 items 有另一種解決方法,就是用 as 加上替代的 new local name

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

fn function1() -> Result {
}

fn function2() -> IoResult<()> {
}

Re-exporting Names with pub use

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

// 在 use 前面再加上 pub
// 就等於將 hosting 變成 public,外面的 code 也可以直接使用 hosting::add_to_waitlist()
pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}

Re-exporting 在程式碼的 internal structure 跟外面呼叫你的 code 的方法不同時,會很有用。例如餐廳,營運者很容易了解 "front of house" "back of house" 的意義。但客戶可能不懂這兩個名詞,這時候,可以透過 pub use 調整結構。

Using External Packages

先前專案有使用 rand 這個 external package,是在 Cargo.toml 裡面加上 rand

[dependencies]
rand = "0.5.5"

加上 rand 的定義後,等於將 rand 帶入這個 package 的 scope。透過 use 引用 Rng

use rand::Rng;
fn main() {
    let secret_number = rand::thread_rng().gen_range(1, 101);
}

Rust community 將許多 library packages 都放在 https://crates.io

標準函式庫 std 也是一個 crate。但因為 std 是在 rust language 裡面,所以不需要另外在 Cargo.toml 裡面填寫到 dependencies 中。

Using Nested Paths to Clean Up Large use Lists

如果同時要使用兩個以上的 module,但 std:: 會一直重複。

use std::cmp::Ordering;
use std::io;

改用這個方式

use std::{cmp::Ordering, io};

另一個例子

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

// 改為
use std::io::{self, Write};

The Glob Operator

如果想使用某個 path 的所有 public items,就用 *

use std::collections::*;

Separating Modules info Different Files

先前的範例都是將多個 module 定義在一個檔案中。可以將 module 分在不同檔案。

修改 src/lib.rs

// 定義在 src/front_of_house.rs
mod front_of_house;

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}

src/frontofhouse.rs

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

可再進一步分離 hosting

修改 src/frontofhouse.rs

pub mod hosting;

新增 src/frontofhouse/hosting.rs

pub fn add_to_waitlist() {}

References

The Rust Programming Language

中文版

中文版 2

沒有留言:

張貼留言