Rust module crate package use pub

[Rust] 模組系統

到目前為止程式碼都擠在 src/main.rs,真實專案不會這樣寫。這篇把 Rust 怎麼切多檔案、怎麼控制可見性、怎麼引用第三方 crate 一次串完。掌握這些,下一篇再加上 trait,組織 Rust 程式的整套工具就齊了。

名詞先釐清

四個層次,從大到小:

概念說明
Workspace一組 packages,共用同一個 Cargo.locktarget/
Package一個 Cargo.toml 管理的單位,可包含一個 library crate + 多個 binary crate
Crate編譯單位,產出一個 library 或一個 binary
Module在 crate 內組織程式碼的單位,控制可見性與命名空間
Workspace
└── Package(Cargo.toml)
    ├── Crate(lib.rs,library)
    │   ├── Module(mod foo)
    │   └── Module(mod bar)
    └── Crate(main.rs,binary)

Crate root

每個 crate 有一個 root file:

  • Library crate:src/lib.rs
  • Binary crate:src/main.rs
  • 額外 binaries:src/bin/<name>.rs

cargo new 預設建立 binary crate(產生 src/main.rs)。 cargo new --lib name 建立 library crate(產生 src/lib.rs)。

mod:宣告模組

模組是 crate 內的命名空間。在 src/main.rssrc/lib.rsmod foo;,編譯器會去找:

  1. src/foo.rs(單一檔)
  2. src/foo/mod.rs(資料夾風格,舊式)
  3. src/foo.rssrc/foo/ 子資料夾(新式,2018 edition 之後)

Rust 2018 edition 起優先用 src/foo.rs(平行檔案)。src/foo/mod.rs 是舊式寫法——子模組多時目錄結構清楚,但檔名全叫 mod.rs,在編輯器分頁裡容易混淆,新程式碼建議避免。

範例專案結構:

src/
├── main.rs
├── garden.rs
└── garden/
    └── vegetables.rs

src/main.rs

mod garden;          // 載入 src/garden.rs

fn main() {
    let p = garden::vegetables::Asparagus {};
    println!("planted!");
}

src/garden.rs

pub mod vegetables;  // 載入 src/garden/vegetables.rs

src/garden/vegetables.rs

pub struct Asparagus {}

也可以直接 inline 寫在同一檔:

mod garden {
    pub mod vegetables {
        pub struct Asparagus {}
    }
}

pub:可見性

預設所有東西都是私有的——模組、函式、struct、struct 欄位、enum 變體通通是。

mod kitchen {
    pub fn cook() {       // pub:外面可以叫
        prepare();
    }

    fn prepare() { }      // 私有:只有 kitchen 內部能叫
}

fn main() {
    kitchen::cook();      // ✅
    // kitchen::prepare(); // ❌ private function
}

pub 還有更細的控制:

pub fn foo() {}              // 對所有人公開
pub(crate) fn bar() {}       // 整個 crate 內可見,但不對外
pub(super) fn baz() {}       // 父模組可見
pub(in crate::path) fn x() {} // 限定路徑可見

struct 與 enum 的 pub 差異

struct:把 struct 標 pub 不會自動把欄位變 pub,欄位要一個一個標。

mod food {
    pub struct Pizza {
        pub topping: String,    // 公開欄位
        size: u32,              // 私有,外面拿不到
    }

    impl Pizza {
        pub fn new(topping: String) -> Self {
            Pizza { topping, size: 12 }
        }
    }
}

fn main() {
    let p = food::Pizza::new(String::from("cheese"));
    println!("{}", p.topping);
    // println!("{}", p.size); // ❌
}

enum:標 pub所有變體自動 pub,因為大家用 enum 通常是要 match 所有 case。

設計理由:match 一個 enum 必須窮舉所有變體。如果某個變體對外隱藏,使用方永遠寫不出完整的 match——除非用 _ 兜底,但那就失去型別安全的好處了。所以 struct 欄位可以各自隱藏,enum 變體不行。

mod color {
    pub enum Light {
        Red,
        Yellow,
        Green,
    }
}

路徑:絕對 vs 相對

// 假設這是 src/lib.rs

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

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

    // 相對路徑:從目前模組開始
    front_of_house::hosting::add_to_waitlist();
}

super = 上一層模組,self = 目前模組:

mod parent {
    fn helper() {}

    mod child {
        fn use_helper() {
            super::helper();      // 找上一層的 helper
        }
    }
}

use:把長路徑帶到當前 scope

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();   // 直接寫 hosting:: 即可
}

慣例:函式的 use 到模組那層就停(use ...::hosting),呼叫時帶模組名(hosting::add_to_waitlist),讀程式時才看得出函式來源。

例外:型別、struct、enum、trait 慣例 use 到本身:

use std::collections::HashMap;
use std::io::Read;

let m = HashMap::new();

as:避免衝突

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

fn f1() -> Result { Ok(()) }
fn f2() -> IoResult<()> { Ok(()) }

pub use:re-export

把內部模組的東西轉手公開到外層:

mod inner {
    pub fn helper() {}
}

pub use inner::helper;
// 外面可以直接呼叫 crate::helper(),不用知道它住在 inner

很多 library 用這招把 API 集中在 lib.rs 暴露出來。

多重 import

use std::io::{self, Read, Write};
// 等同:
// use std::io;
// use std::io::Read;
// use std::io::Write;

use std::collections::*;   // glob,會把 collections 下所有 pub 的東西帶進來

* 慎用,會污染命名空間,通常只在 prelude / 測試模組使用。

外部依賴:使用第三方 crate

Cargo.toml

[dependencies]
serde = { version = "1", features = ["derive"] }
rand = "0.8"

或用指令:

cargo add serde --features derive
cargo add rand

程式裡:

use rand::Rng;

fn main() {
    let n: u8 = rand::thread_rng().gen_range(1..=100);
    println!("{n}");
}

std 與 prelude

std 是標準庫。最常用的那批東西(OptionResultVecStringprintln!BoxSomeNoneOkErr…)放在 prelude,自動 import 到每個檔案,不用 use

prelude 之外的就要手動 use:

use std::collections::HashMap;
use std::fs::File;
use std::io::{Read, Write};

一個合理的 library crate 結構

my_lib/
├── Cargo.toml
└── src/
    ├── lib.rs              // 對外 API(用 pub use 集中暴露)
    ├── error.rs            // 錯誤型別
    ├── client.rs           // HTTP client
    ├── client/
    │   ├── auth.rs
    │   └── retry.rs
    └── models/
        ├── mod.rs          // 或 src/models.rs
        ├── user.rs
        └── post.rs

src/lib.rs

mod error;
mod client;
mod models;

// 對外暴露的 API 集中在這
pub use error::{Error, Result};
pub use client::Client;
pub use models::{User, Post};

呼叫方:

use my_lib::{Client, User, Result};

binary + library 混合 package

Cargo.toml 裡 package 名是 my_app

src/
├── lib.rs        // library crate(package 同名 my_app)
├── main.rs       // binary crate
└── bin/
    └── tool.rs   // 額外 binary:my_app-tool

src/main.rs 用 library 的東西:

use my_app::{Client, Config};   // 要用 package 名而不是 crate

fn main() {
    let c = Client::new();
    // ...
}

跑時:

cargo run                     # 跑 src/main.rs
cargo run --bin tool          # 跑 src/bin/tool.rs

常見錯誤訊息對照

訊息意思
function ... is privatepub
unresolved import ...use 路徑寫錯 / 模組沒宣告
cannot find ... in this scope沒 use 進來 / 不在這個模組可見
module ... does not existmod foo; 但找不到 src/foo.rs

模組系統打通後,最後一篇 trait 把「行為」這個維度補進來——也就是 Rust 怎麼做多型、抽象與泛型約束。

Latest Updates

  • 2026.06.11 Content updated