Mục lục
- Mục Tiêu Bài Học
- Default Private — Encapsulation
pubCơ Bản — fn / struct / enum- Struct Field — Cần
pubTừng Field - Enum Variant — Tự Pub Theo Enum
pub(crate)— Visible Toàn Cratepub(super)— Visible Parent Modulepub(in path)— Granular Visibility- Re-export Pattern Preview —
pub use - Tổng Kết
- Bài Tập Củng Cố
- Bài Tiếp Theo
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Hiểu vì sao Rust chọn default private cho mọi item và lợi ích về encapsulation mà lựa chọn này mang lại.
- Dùng được
pubđể exposefn,struct,enumra khỏi module — cú pháp đặt trước item. - Phân biệt rõ struct
pubkhông đồng nghĩa fieldpub: muốn truy cập field từ ngoài phải thêmpubriêng cho từng field. - Biết variant của
pub enumtự độngpubtheo enum — không cần ghipubtrước từng variant. - Dùng được ba biến thể granular:
pub(crate)cho item internal nội bộ crate,pub(super)cho parent module,pub(in path)cho một path cụ thể. - Hiểu
pub use foo::bar;ở mức preview — pattern re-export để flatten public API. Chi tiết và facade pattern sẽ ở Bài 116. - Áp dụng các quy tắc trên để thiết kế một internal API có ranh giới rõ ràng — public cho user, private/internal cho team.
Default Private — Encapsulation
Rust quy định: mặc định mọi item là private. Một item private chỉ visible trong:
- Chính module khai báo nó.
- Mọi child module (module con) nằm bên trong module đó.
Sibling module và parent module không thấy item private. Khác với Java (mặc định package-private), Python (không có khái niệm private thật, chỉ convention _name), hay JavaScript (mọi export đều public), Rust ép bạn phải nghĩ về ranh giới ngay từ đầu — viết pub là một quyết định có ý thức.
// src/lib.rs
mod auth {
// Item này default private - chỉ visible trong module `auth` và child của `auth`
fn hash_password(raw: &str) -> String {
format!("hashed:{raw}")
}
// Hàm cùng module - gọi được hash_password vì cùng scope private
pub fn login(user: &str, pw: &str) -> bool {
let _h = hash_password(pw);
user == "admin"
}
}
fn main() {
// OK: login đã pub
let _ = auth::login("admin", "123");
// Compile error: hash_password là private của module auth
// let _ = auth::hash_password("123");
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// error[E0603]: function `hash_password` is private
}
Triết lý đằng sau: encapsulation by default. Khi viết một module, mọi helper, mọi cấu trúc nội bộ đều được giấu khỏi thế giới bên ngoài cho đến khi bạn chủ động expose. Điều này có ba lợi ích lớn:
- Tự do refactor: thay đổi tên, signature, hoặc xoá item private không bao giờ ảnh hưởng caller bên ngoài — vì không ai bên ngoài thấy nó.
- Bảo toàn invariant: nếu một
struct Usercó ràng buộc "tuổi không âm", che fieldagesau private và chỉ expose constructor đảm bảo điều kiện sẽ không ai bên ngoài phá được. - API surface gọn:
cargo docchỉ liệt kê itempub— user chỉ thấy đúng những gì bạn muốn họ thấy, không bị nhiễu bởi helper nội bộ.
Lưu ý quan trọng về quan hệ parent-child: child thấy item private của parent (qua super::), nhưng parent không thấy item private của child. Đây là quy tắc bất đối xứng đặc trưng của Rust — sẽ rõ hơn khi bạn dùng pub(super) ở mục 7.
pub Cơ Bản — fn / struct / enum
Để expose một item ra ngoài module khai báo, đặt pub trước item. Quy tắc áp dụng đồng nhất cho fn, struct, enum, const, static, type, trait, mod (cả module cũng default private).
mod api {
// Hàm public - mọi nơi đều gọi được api::process
pub fn process(input: &str) -> String {
normalize(input)
}
// Helper vẫn private - không lộ ra ngoài
fn normalize(s: &str) -> String {
s.trim().to_lowercase()
}
// Struct public - tên type lộ ra ngoài, nhưng field thì chưa (xem mục 4)
pub struct User {
pub name: String,
age: u32, // field này vẫn private mặc dù struct là pub
}
// Enum public - mọi variant tự động pub theo enum
pub enum Status {
Ok,
NotFound,
InternalError,
}
}
fn main() {
let _ = api::process(" Hello ");
let _ = api::Status::Ok;
}
Khi pub mod api; hay pub fn process(), item đó được expose ra tới chỗ module cha cho phép. Nói cách khác, để dùng api::process từ ngoài, cả module api phải pub và process phải pub. Hai điều kiện tách rời nhau: một item pub bên trong module private vẫn vô hình ra ngoài. Nắm chắc nguyên tắc "visible đến đâu tuỳ chuỗi pub liền nhau" sẽ tránh nhiều rắc rối khi cấu trúc lại module tree.
Quy ước thực hành: chỉ pub những gì bạn chắc chắn muốn đưa thành public API. Một item pub rồi rất khó rút lại — vì có thể đã có code khác phụ thuộc, rút lại sẽ là breaking change.
Struct Field — Cần pub Từng Field
Đây là chỗ rất nhiều người mới Rust nhầm. Đặt pub trước struct chỉ expose tên type, không expose field. Mỗi field muốn truy cập từ ngoài phải tự khai báo pub.
mod model {
pub struct User {
pub name: String, // field name public
age: u32, // field age vẫn private
}
impl User {
// Constructor public - ép caller đi qua đây thay vì tự dựng struct literal
pub fn new(name: String, age: u32) -> Self {
User { name, age }
}
pub fn age(&self) -> u32 {
self.age
}
}
}
fn main() {
// OK: đi qua constructor
let mut u = model::User::new("Canh".into(), 30);
// OK: name là pub field
u.name = "Canh NV".into();
println!("{}", u.name);
// OK: getter
println!("{}", u.age());
// Compile error: age là private field
// u.age = 31;
// ^^^^^ error[E0616]: field `age` of struct `User` is private
// Compile error: không dựng được struct literal vì có field private
// let _ = model::User { name: "x".into(), age: 1 };
}
Quy tắc dẫn xuất quan trọng: nếu struct có bất kỳ field private nào, ngoài module không thể dựng struct literal cho nó. Buộc phải đi qua một fn new hoặc tương đương. Đây chính là cách Rust ép bạn dùng constructor để bảo toàn invariant — một dạng pattern controlled construction.
Best practice: với struct dùng làm data carrier đơn giản (DTO, request body), pub tất cả field để dễ dùng. Với struct mang invariant (ID không trùng, balance không âm, version chỉ tăng), giữ field private và expose getter + builder.
Enum Variant — Tự Pub Theo Enum
Khác hẳn struct, variant của pub enum tự động pub theo enum. Bạn không ghi pub trước từng variant — và thực tế nếu ghi, compiler sẽ báo lỗi cú pháp.
mod http {
pub enum Status {
Ok,
NotFound,
InternalError,
}
// Lỗi cú pháp nếu viết:
// pub enum Bad {
// pub Ok, // ^^^ unnecessary visibility qualifier
// }
}
fn main() {
let s = http::Status::Ok;
match s {
http::Status::Ok => println!("ok"),
http::Status::NotFound => println!("404"),
http::Status::InternalError => println!("500"),
}
}
Lý do của sự bất đối xứng giữa struct và enum nằm ở ngữ nghĩa: enum là một SET đóng các trạng thái — không có ý nghĩa "che giấu một số variant trong khi expose các variant khác". Nếu bạn muốn che một số trạng thái, đó là dấu hiệu nên tách thành hai enum, hoặc đưa biến thể đó thành struct riêng. Với field bên trong variant (variant kiểu struct, ví dụ Move { x: i32, y: i32 }) — các field này cũng tự pub theo enum, không cần ghi pub.
Hệ quả: khi viết một crate có enum public, hãy cẩn thận thêm variant mới. Vì variant tự pub, mọi thêm variant đều là breaking change với user dùng match exhaustive. Pattern phổ biến để tránh là gắn attribute #[non_exhaustive] lên enum — caller buộc phải có nhánh _ wildcard, cho phép bạn thêm variant mới mà không vỡ build user. Chi tiết ở các bài về API stability sau này.
pub(crate) — Visible Toàn Crate
Giữa hai cực private (chỉ trong module) và public (cả thế giới), Rust có một mức trung gian rất hữu ích: pub(crate). Item gắn pub(crate) visible trong toàn bộ crate hiện tại, nhưng không visible ra ngoài crate (tức user khi use my_crate::... sẽ không thấy).
// src/lib.rs (crate gọi là `myapp`)
mod util {
// Visible khắp myapp, nhưng dùng myapp::util::helper từ crate khác sẽ lỗi
pub(crate) fn helper(input: &str) -> String {
format!("[helper] {input}")
}
}
mod api {
use crate::util::helper; // OK - cùng crate myapp
pub fn process(input: &str) -> String {
helper(input)
}
}
pub use api::process;
// Trong một crate khác:
// use myapp::util::helper;
// ^^^^^^^^^^^^^^^^^^^^^^^^
// error[E0603]: function `helper` is private
pub(crate) là idiom chuẩn cho internal API. Bạn có nhiều helper, nhiều type cần chia sẻ giữa các module bên trong một thư viện, nhưng không muốn lộ ra cho user vì chúng có thể đổi signature bất cứ lúc nào. Đặt pub(crate) giữ chúng linh hoạt mà vẫn dùng được mọi nơi trong crate.
Khi nào không nên dùng pub(crate)? Khi item chỉ dùng trong một module và child của nó — giữ private là đủ. Đừng pub(crate) "cho chắc" — visibility là tài liệu cho người đọc, ghi rộng hơn cần thiết là gây hiểu nhầm về phạm vi sử dụng.
pub(super) — Visible Parent Module
pub(super) hẹp hơn pub(crate): item visible tới parent module trực tiếp (và tất nhiên là trong module hiện tại), nhưng không lan ra anh em hay grandparent.
mod outer {
mod inner {
// Visible cho outer (parent), nhưng không cho main hay sibling khác của outer
pub(super) fn internal() {
println!("from inner");
}
// Helper riêng cho test - parent thấy được để gọi từ test util
#[cfg(test)]
pub(super) fn test_seed() -> u32 {
42
}
}
pub fn run() {
inner::internal(); // OK - outer là parent của inner
}
}
fn main() {
outer::run();
// Lỗi: main không phải parent của inner
// outer::inner::internal();
}
Use case điển hình của pub(super):
- Test helper: một submodule chứa fixture, mock factory; parent module dùng để viết test nhưng không expose lên cao hơn nữa.
- Logical grouping: tách logic phụ trợ thành submodule cho gọn file, nhưng coordinator nằm ở parent vẫn cần truy cập. Lúc này
pub(crate)là quá rộng —pub(super)mới chính xác phạm vi dự định. - Internal callback: child cung cấp một hàm cho parent gọi vào, không phải API cho cả thế giới.
Lưu ý: pub(super) chỉ tới parent trực tiếp, không truyền tiếp lên ông bà. Nếu cần expose lên cao hơn, dùng pub(in path) (mục 8) hoặc pub(crate).
pub(in path) — Granular Visibility
Khi pub(super) hẹp quá còn pub(crate) rộng quá, Rust cho phép chỉ định chính xác module nào được thấy item, qua cú pháp pub(in path::to::module). Path phải là một ancestor (tổ tiên) của module khai báo item — không thể "mở visibility" cho module ngẫu nhiên.
mod api {
pub mod routes {
pub mod users {
// Visible khắp api::routes (gồm users, posts, comments...)
// nhưng không visible ra ngoài api::routes
pub(in crate::api::routes) fn build_handler() {
println!("building user handler");
}
}
pub mod posts {
pub fn list() {
// OK: posts cũng nằm trong crate::api::routes
super::users::build_handler();
}
}
}
}
fn main() {
api::routes::posts::list();
// Lỗi: main không nằm trong crate::api::routes
// api::routes::users::build_handler();
}
pub(in path) dùng khi bạn có một subsystem nhiều module ngang hàng (ví dụ routes/users, routes/posts, routes/auth) cần chia sẻ helper giữa nhau, nhưng không muốn helper đó visible cho phần khác của crate (controller, service, repository). Đây là dạng visibility hiếm gặp nhất — rarely needed — vì đa phần pub(crate) đã đủ. Nhưng khi cần, nó cứu bạn khỏi việc đặt module ở chỗ kỳ quặc chỉ để chỉnh visibility.
Quy ước: cố gắng dùng pub(crate) trước; chỉ rút xuống pub(in path) khi thực sự thấy việc lộ ra cả crate gây nhầm lẫn. Đọc pub(in crate::api::routes) ở dòng đầu file ngay lập tức nói lên "chỉ subsystem này dùng, đừng tìm cách gọi từ chỗ khác".
Re-export Pattern Preview — pub use
Một pattern liên quan trực tiếp đến visibility nhưng đáng dành nguyên một bài để học là re-export qua pub use. Ý tưởng: bên trong crate, code chia thành nhiều module sâu cho dễ tổ chức; nhưng khi expose ra public API, bạn muốn user gọi my_crate::User chứ không phải my_crate::model::user::types::User.
// src/model/user.rs
pub struct User {
pub name: String,
}
// src/model/mod.rs (hoặc src/model.rs)
pub mod user;
// src/lib.rs
mod model;
// Re-export: nâng User lên cấp crate root
pub use model::user::User;
// Bây giờ user gọi:
// use my_crate::User; // OK - flatten
// use my_crate::model::user::User; // vẫn OK nhưng không cần
pub use kết hợp cả use (bring path vào scope hiện tại) và pub (expose item ra ngoài). Item được nâng lên cùng visibility với chỗ pub use đó — bạn có thể đặt ở root lib.rs để expose ra ngoài crate, hoặc đặt sâu hơn với pub(crate) use để re-export nội bộ.
Các pattern thực tế:
- Flatten API: như ví dụ trên — user không cần biết cấu trúc thư mục nội bộ.
- Facade:
lib.rstrở thành mặt tiền duy nhất; thay đổi vị tríUsertrong tree không phá user vì re-export vẫn ổn. - Prelude: tạo module
preludegom các symbol thường dùng — user chỉ cầnuse my_crate::prelude::*;.
Bài 116 pub-use-reexport sẽ đi sâu vào từng dạng trên, kèm ví dụ thực tế từ lib.rs của các crate nổi tiếng. Trong bài này, chỉ cần nhận diện pub use để khi đọc code không nhầm lẫn.
Tổng Kết
- Default private: mọi item Rust mặc định chỉ visible trong module khai báo và child module — ép encapsulation, hỗ trợ tự do refactor.
pubcơ bản: đặt trướcfn/struct/enum/mod/const/static/traitđể expose ra ngoài; cả chuỗi module từ root đến item phải đềupubmới thực sự visible.- Struct field:
pubtrên struct chỉ expose tên type; mỗi field phảipubriêng. Struct có field private không dựng được literal từ ngoài — ép qua constructor. - Enum variant: tự động
pubtheo enum, không ghipubtrước variant. Field bên trong variant kiểu struct cũng tự pub. pub(crate): idiom cho internal API — visible khắp crate nhưng ẩn với user bên ngoài. Đa phần helper nội bộ dùng mức này.pub(super): visible tới parent trực tiếp — phù hợp test helper, internal callback, logical grouping màpub(crate)quá rộng.pub(in path): visible đúng một path ancestor cụ thể — granular nhất, hiếm dùng, để chia sẻ helper giữa subsystem ngang hàng.pub use: re-export — flatten public API, facade pattern. Học sâu ở Bài 116.- Quy tắc thực hành: bắt đầu từ private, mở rộng dần khi thực sự cần. Visibility là tài liệu — ghi rộng hơn cần thiết gây hiểu nhầm về phạm vi.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Bạn có
pub struct Account { balance: i64 }trong modulebank. Từmain, bạn cố ghiAccount { balance: 100 }vàacc.balance = 200. Cả hai có compile được không? Giải thích. - Tại sao
pub enum Color { Red, Green, Blue }không cần ghipubtrước từng variant, trong khipub struct Point { x: i32, y: i32 }lại cầnpubcho từng field? - Bạn đang viết library
parservà có hàmlex_internalmà mọi module trong crate đều dùng nhưng tuyệt đối không muốn user thấy. Visibility nào phù hợp nhất? Vì sao không phảipubhaypub(super)? - Module
api::routes::userscó hàm helperbuild_handlerchỉ dùng cho các module ngang hàng trongapi::routes(posts,comments...). Viết visibility chính xác chobuild_handler. - Một crate có cấu trúc
src/model/user.rschứapub struct User. User crate muốn gọiuse my_crate::Userthay vìuse my_crate::model::user::User. Phải viết gì ởsrc/lib.rs?
Đáp án
- Cả hai đều không compile. Field
balancemặc định private dùstruct Accountlàpub. Struct literalAccount { balance: 100 }đòi truy cập trực tiếp field — lỗifield `balance` of struct `Account` is private. Phép gánacc.balance = 200cũng lỗi cùng kiểu. Muốn dùng phải đi qua constructor nhưAccount::new(100)hoặc methoddepositmà modulebankexpose. - Vì ngữ nghĩa kiểu khác nhau. Struct là một record với các field, một số field có thể nhạy cảm (giữ invariant) — phải che riêng từng field. Enum là một SET đóng các trạng thái, không có ý nghĩa "che một số trạng thái khỏi user" — nếu cần che, hãy tách thành enum khác hoặc struct riêng. Field bên trong variant struct-style cũng tự pub theo enum cùng lý do đó.
pub(crate) fn lex_internal(). Đây là idiom chuẩn cho internal API: visible khắp crateparserđể các module dùng được, nhưng ẩn với user khi họuse parser::....pubsẽ làm user thấy được — không mong muốn.pub(super)chỉ visible với parent trực tiếp, không lan ra các module ngang hàng — quá hẹp.pub(in crate::api::routes) fn build_handler() { ... }. Visible khắpapi::routes(chousers,posts,comments) nhưng không visible cho phần khác của crate.pub(super)chỉ tớiapi::routes(parent trực tiếp) — đúng với code trong fileusers.rsnhưng nếu hàm được khai báo sâu hơn (ví dụapi::routes::users::helpers) thìpub(super)không đủ —pub(in crate::api::routes)mới chính xác phạm vi mong muốn ở mọi vị trí trong subsystem.- Ở
src/lib.rs:mod model;(declaration để compiler thấy module tree) rồipub use model::user::User;để re-exportUserlên crate root. Lưu ý cảmod user;trongsrc/model.rs(hoặcsrc/model/mod.rs) cũng phải có để khai báo submodule. Chi tiết pattern này ở Bài 116.
Bài Tiếp Theo
Bài 114: use Keyword — Bring Path Vào Scope — sau khi đã hiểu cách kiểm soát visibility ai thấy được gì, bài kế tiếp dạy cách rút ngắn path khi đã thấy. use std::io::Write; để gọi Write thay vì std::io::Write mỗi lần; use foo::bar as baz để đổi tên tránh xung đột; group use std::{io, fs}; để gom; và glob use foo::* tiện nhưng nên tránh trong production. pub và use phối hợp với nhau là hai mặt của cùng một bài toán tổ chức code Rust.
