[Rust] Rust 模組系統 & 可見性全攻略:pub / use / self / super 一篇搞懂

[Rust] Rust 模組系統 & 可見性全攻略:pub / use / self / super 一篇搞懂
Photo by Nick Fewings / Unsplash
你有沒有過這種窘境:
「我明明寫了 Spoon struct,為什麼編譯器死都找不到?」
結果一翻檔案,發現它被塞在 kitchen::utils::nested::deeply::more_nested
模組 是程式碼的收納箱,可見性 則是箱子上的密碼鎖。今天就帶你拆解鎖頭機關,學會優雅地開關抽屜,讓該看的看得到、不該看的乖乖待箱子裡

為什麼 mod 像樂高?

如果你把專案想成一座樂高城市,模組是分區街道,可見性就是那些「員工專用」「一般通行」的門禁卡。
要是把每棟大樓全敞開,大人小孩都能跑進機房——維修人員會高興嗎?

小規模專案時,你或許不在意,但一旦城市擴張,就必須:

  1. 封裝維修室:維護者只修自己的範圍
  2. 降低耦合:改動一棟樓不會讓整個街區停電
  3. 清晰導覽:新來的觀光客(新人開發者)不會迷路

理解 Rust 的模組系統

Rust 的預設行為:先上鎖再說

在 Rust,函式、struct、enum...天生都是私有的,只能在定義它們的模組以及其子模組裡存取

mod utils {
    struct Spoon; // 外人看不到
}
預設私有 就像銀行保險箱,除非你主動給鑰匙,別人連箱門都推不開

pub:打開房門,歡迎光臨!

pub 就是公開開關。給項目貼上 pub,就像在門口掛上一個牌子:"歡迎參觀",只要是看得到這扇門的人,都能進來使用

pub struct Spoon;
  • 能看到模組 ≠ 全世界:若 utils 本身沒有用 pub mod utils; 暴露出去,上層照樣看不到 Spoon
  • pub 只是開門,還得帶路:其他模組仍需 use crate::utils::Spoon; 把湯匙端進自己的廚房

use:把路徑搬進當前作用域,少打一堆 ::

如果你每次想喝湯,都得打完整指令 crate::kitchen::utensils::Spoon,是不是像點一杯手搖還要背出他在菜單哪一排
use 就像路口指示牌,幫你把長路徑縮短成好記的名字

基本語法

use crate::kitchen::utensils::Spoon;

fn main() {
    let _s = Spoon;
}
  • use 只改名字,不複製程式碼:編譯器仍然知道 Spoon 住哪,只是你呼叫時可以偷懶
  • 相對 vs 絕對use crate::… 從mod root開始,use super::… / self::… 從目前樓層找

取別名 (alias)

use std::io::Result as IoResult;
  • 解決命名衝突、或讓回傳型別語意更清晰

分組/展開一次搞定

use std::{fs, io::{self, Read}, path::Path};
  • {} 把同一路徑的成員打包
  • self 代表整個模組本身,和路徑混用不必重複寫

常見雷區

  • 「我在 mod a 裡宣告了 pub struct Foomod b 直接用得到吧?」→ 不行,因為 mod b 必須先 use crate::a::Foo;
  • 「我每個東西都加 pub 總不會有問題吧」→ code base越來越大你就知道問題在哪,亂加只會讓 API 混亂還埋下衝突炸彈

pub(crate):模組內通行證,向外鎖上鎖

你在 my_crate 裡寫了一個 資料庫連線池,只想讓 同一個專案webworker 模組共用,卻又不想被其他 crate 直接呼叫。該怎麼辦?
👉 答案就是 pub(crate) : 給整個 (crate)一張共用門禁卡,鄰居想進來?免談

語法速查

// src/db.rs
pub(crate) struct ConnectionPool;
  • pub(crate) = 只有當前 crate 能看到
  • 在當前 crate 外的世界,編譯器視它為私有
// ── my_crate ───────────────────────────
// src/lib.rs
mod db;
mod web;

// src/db.rs
pub(crate) struct ConnectionPool;

// src/web.rs
use crate::db::ConnectionPool; // OK,同一個 crate

// ── other_crate ────────────────────────
// src/main.rs
use my_crate::db::ConnectionPool; // 編譯錯誤:`ConnectionPool` is private

常見踩雷 & 建議

  1. 二進位 (binary) 專案沒差?
    Binary 預設不可被外部 use,但若日後抽成 library,pub(crate) 早就幫你把界線劃好
  2. 測試模組需要存取?
    測試 (#[cfg(test)]) 編譯在同一個 crate,能直接用 pub(crate) 內容,不必再加 #[cfg(test)] pub
  3. 文件生成 (cargo doc)
    pub(crate) 會出現在 private 文件頁面 (cargo doc --document-private-items),預設外部使用者看不到,文件乾淨不雜

當你心想這工具大家都用得到,但外人不該拿到——九成九就是 pub(crate)

你該選哪個?

場景建議可見性
公開函式庫 APIpub
內部工具函式、多模組共用結構體pub(crate)
測試 helper、benchmark 工具cfg(test) + pub(crate)

self::super:::讓模組路徑不再迷路的 GPS 指南

腦內建構一張地圖self 是你現在站的位置,super 則是樓上一層的樓梯口。搞懂方位感,之後看到 30 行 use 也不會頭暈
關鍵字指向常見用途
self::目前模組避免與外部同名項目混淆‑ 匯入巢狀私有項目
super::上一層父模組子模組呼叫父模組函式或型別、減少 public 暴露

典型場景

self:: 保持語意明確

mod parser {
    pub struct Parser;
    impl Parser {
        pub fn parse() {}
    }

    // 建立別名時刻意加 self:: ,讀者秒懂來源
    pub use self::Parser as DefaultParser;
}

為什麼不用相對路徑就好?
假如上層也有 Parser,明寫 self:: 可避免 IDE 自動補錯路徑、日後重構也少踩雷

super:: 拿父模組工具,但不外露

mod service {
    fn secret_algorithm() { /* 私有 */ }

    pub mod api {
        pub fn run() {
            // 借用父模組私有邏輯,不必把它 pub 出去
            super::secret_algorithm();
        }
    }
}

secret_algorithm 仍維持私有,API 與實作分離,如果是測試模組可直接 super:: 呼叫驗證

易犯錯誤 & 編譯器提示

mod a {
    pub mod b {
        pub fn hello() {}
    }
}

use super::a::b::hello; // error: no `a` in `super`
在 crate root 以外亂加 super:: 會指向不存在的父模組,編譯器馬上打臉。遇到這類錯誤,先確認「我現在身在何處」

情境測驗

  1. 你在 foo::bar::baz 想引用 foo::utils::Helper,可以用 super::super::utils::Helper
    > 能用但可讀性差
  2. 何時需要 crate:: 絕對路徑而不是層層 super::
    > 當要跨越多層且結構可能重排時,用 crate:: 穩定度較高
  3. self:: 是否能省略?省略後對可讀性有何影響?
    self:: 可省略,但在名稱重複時寫出來更清楚

使用 pub use 進行別名和重新匯出

你願意每次都打 42 個 :: 才拿到型別嗎? 還是希望使用者一句 use my_crate::Foo; 就完事
pub use 同時具備 引入重新匯出改名 三種能力:可以把深層模組裡的型別搬到頂層,整理 API 結構,必要時還能換成更易懂的別名

建立 別名(alias)— 給路名貼小抄

pub use self::complex_module::deeply_nested::SomeStruct as SimpleStruct;
  • 站在使用者角度:他們只看見 SimpleStruct,不必翻地圖
  • 維護者福利:日後重構路徑,只要別名不改,外部程式碼零痛感

外觀模式(Facade)— 一口氣搬整櫃工具

pub use self::utilities::*; // 小心滿櫃零件可能撞名
  • 適合把「常用小工具」集中到 crate root,打造「超商櫃台」
  • 風險:命名空間炸裂,跟第三方 crate 混用時特別容易衝突
  • 緩解招式
    1. 只 re‑export 必要 函式;別 通殺
    2. 若真的要 *,至少放在 prelude,讓使用者自願 use my_crate::prelude::*;

Prelude — 官方也愛用的前奏曲

pub mod prelude {
    pub use super::{Foo, Bar, Baz};
}
  • 使用者體驗use my_crate::prelude::*; 一行帶走常用型別
  • 可維護性:Prelude 應該只含「九成場景都要用」的 API,剩下交給進階用戶自己挑

pub use super::… 這招看起來像是在自家巷口轉一圈再貼回門牌,其實有三個好處:

  • 少打一截路徑,不用寫滿 crate::某模組::某東西
    你已經站在子模組裡(例如 mod prelude { … }),只想把爸爸那層的 FooBar 端進來公開。用 super::Foo 就像跟樓上拿工具——比打完整絕對路徑省字、省手誤。
  • 避免把上層模組整個 pub 出去
    如果你在子模組直接寫 pub use crate::Foo;
    那還好,但很多人懶得思考,就順手在父模組把一堆項目 pub 了。用 super:: 只匯出需要的東西,父模組其餘細節仍可保持私有,封裝不被破壞
  • 明示「來源在樓上」,讀者馬上懂脈絡
    尤其 Prelude 常放在每個子功能最底層。你寫 pub use super::{Foo, Bar};
    讀者一看就知道這些型別原本就在父模組,而不是從別的 crate 抓來

何時該用哪招?

需求推薦手法
單一路徑太長,想簡稱別名 (alias)
想把子模組常用工具冒到頂層外觀模式 (Facade)
想提供快速入門、一鍵 bring‑in 所有常用型別Prelude

命名衝突怎麼辦?

use std::io::Error as IoError;
use std::fmt::Error as FmtError;
兩張同名卡片放進同一副牌,改個花色就不會搞混

排雷清單

  1. 為衝突型別加後綴(JsonError / XmlError
  2. as 改別名,保持呼叫端可讀
  3. 避免在同一模組大量 use *::*;,不利衝突診斷

設計指南:最小特權原則

不要看到什麼就標 pub。內部細節一旦全攤在外,改個小函式都得擔心誰被牽連,維護負擔爆表。把實作鎖起來,只對外開少數必要的 API,才能讓專案穩定又好改,這就是 Rust 可見性機制的核心精神

四步驟落實最小特權

步驟自我拷問對應語法行動提醒
1️⃣ 先私有,後公開外部真的需要用到嗎?預設私有 → 視需求加 pub / pub(crate)不確定就先鎖再說
2️⃣ API 與實作分離這是骨架(API) 還是螺絲(impl)?模組劃分 api + impl骨架可以秀,螺絲上鎖
3️⃣ 漸進式暴露只給兄弟模組共用?pub(super) / pub(crate)給自家人通行證,外人免談
4️⃣ 文件同步檢查cargo doc 乾淨嗎?cargo doc --document-private-items文件曝露 = 鎖沒關好

進階示範:三層鎖的資料庫連線池

// db/connection.rs
pub(crate) struct RawConn { /* socket 等細節 */ }

pub struct Pool {
    // RawConn 私有封裝
    conns: Vec<RawConn>,
}

impl Pool {
    pub fn get(&mut self) -> RawConnGuard<'_> { /* ... */ }
}

// db/mod.rs
pub mod connection;                    // 只把 Pool 暴露出去
pub(crate) use connection::RawConn;    // 給同 crate 其他模組測試
  • 外部 只看得到 Pool:怎麼開 socket 全無感
  • crate 內部 可以直接測 RawConn,不用再加測試用 cfg(test) pub
  • 子模組 繼續藏更深的細節,封裝完整

雷區清單

  1. 一開專案就全 pub:未來重構像拆炸彈
  2. pub use *::*; 取代設計:拉伸型 API 終究打回票
  3. 文件洩漏實作細節cargo doc 如果堆滿 internal_,請回去鎖門
先把門鎖好,再決定發鑰匙,而不是邊發鑰匙邊想要不要裝門

小結:把鑰匙掛好,城市才不會失控

掌握 usepubpub(crate)selfsuperpub use,對程式設計最直接的幫助只有一句話:讓你的專案又大又穩還好維護

  • 降低耦合:內部重構不會牽動外部呼叫者,版本升級少破壞
  • 加速開發:路徑簡潔、API 清晰,IDE 補完更順手,閱讀成本大幅下降
  • 提升安全與穩定:私有細節鎖起來,避免誤用,公共介面明確,減少不必要的風險
可見性關得好,程式碼才能長得快,城市有規畫,居民才住得爽