[Rust] Rust 模組系統 & 可見性全攻略:pub / use / self / super 一篇搞懂
你有沒有過這種窘境:
「我明明寫了Spoon
struct,為什麼編譯器死都找不到?」
結果一翻檔案,發現它被塞在kitchen::utils::nested::deeply::more_nested
裡
模組 是程式碼的收納箱,可見性 則是箱子上的密碼鎖。今天就帶你拆解鎖頭機關,學會優雅地開關抽屜,讓該看的看得到、不該看的乖乖待箱子裡
為什麼 mod 像樂高?
如果你把專案想成一座樂高城市,模組是分區街道,可見性就是那些「員工專用」「一般通行」的門禁卡。
要是把每棟大樓全敞開,大人小孩都能跑進機房——維修人員會高興嗎?
小規模專案時,你或許不在意,但一旦城市擴張,就必須:
- 封裝維修室:維護者只修自己的範圍
- 降低耦合:改動一棟樓不會讓整個街區停電
- 清晰導覽:新來的觀光客(新人開發者)不會迷路
理解 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 Foo
,mod b
直接用得到吧?」→ 不行,因為mod b
必須先use crate::a::Foo;
- 「我每個東西都加
pub
總不會有問題吧」→ code base越來越大你就知道問題在哪,亂加只會讓 API 混亂還埋下衝突炸彈
pub(crate)
:模組內通行證,向外鎖上鎖
你在my_crate
裡寫了一個 資料庫連線池,只想讓 同一個專案 的web
與worker
模組共用,卻又不想被其他 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
常見踩雷 & 建議
- 二進位 (binary) 專案沒差?
Binary 預設不可被外部use
,但若日後抽成 library,pub(crate)
早就幫你把界線劃好 - 測試模組需要存取?
測試 (#[cfg(test)]
) 編譯在同一個 crate,能直接用pub(crate)
內容,不必再加#[cfg(test)] pub
- 文件生成 (cargo doc)
pub(crate)
會出現在 private 文件頁面 (cargo doc --document-private-items
),預設外部使用者看不到,文件乾淨不雜
當你心想這工具大家都用得到,但外人不該拿到——九成九就是 pub(crate)
你該選哪個?
場景 | 建議可見性 |
公開函式庫 API | pub |
內部工具函式、多模組共用結構體 | 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::
會指向不存在的父模組,編譯器馬上打臉。遇到這類錯誤,先確認「我現在身在何處」
情境測驗
- 你在
foo::bar::baz
想引用foo::utils::Helper
,可以用super::super::utils::Helper
嗎
> 能用但可讀性差 - 何時需要
crate::
絕對路徑而不是層層super::
> 當要跨越多層且結構可能重排時,用crate::
穩定度較高 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 混用時特別容易衝突
- 緩解招式:
- 只 re‑export 必要 函式;別 通殺
- 若真的要
*
,至少放在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 { … }
),只想把爸爸那層的Foo
、Bar
端進來公開。用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;
兩張同名卡片放進同一副牌,改個花色就不會搞混
排雷清單:
- 為衝突型別加後綴(
JsonError
/XmlError
) - 用
as
改別名,保持呼叫端可讀 - 避免在同一模組大量
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
- 子模組 繼續藏更深的細節,封裝完整
雷區清單
- 一開專案就全
pub
:未來重構像拆炸彈 - 用
pub use *::*;
取代設計:拉伸型 API 終究打回票 - 文件洩漏實作細節:
cargo doc
如果堆滿internal_
,請回去鎖門
先把門鎖好,再決定發鑰匙,而不是邊發鑰匙邊想要不要裝門
小結:把鑰匙掛好,城市才不會失控
掌握 use
、pub
、pub(crate)
、self
/super
與 pub use
,對程式設計最直接的幫助只有一句話:讓你的專案又大又穩還好維護
- 降低耦合:內部重構不會牽動外部呼叫者,版本升級少破壞
- 加速開發:路徑簡潔、API 清晰,IDE 補完更順手,閱讀成本大幅下降
- 提升安全與穩定:私有細節鎖起來,避免誤用,公共介面明確,減少不必要的風險
可見性關得好,程式碼才能長得快,城市有規畫,居民才住得爽
Comments ()