Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Biết 3 special path keyword Rust:
self,super,crate— và ánh xạ với./,../,/trong filesystem để dễ nhớ. - Viết được
use crate::routes::users;dạng absolute bắt đầu từ root crate, cho cross-module import đường dài. - Viết được
use super::config;dạng relative đi lên parent module, để import sibling trong cùng folder. - Hiểu
self::phần lớn redundant (default đã là self), nhưng vẫn hữu dụng trong nested use dạnguse foo::{self, bar}. - Phân biệt khi nào nên dùng absolute (cross-module xa, public API readable) và khi nào nên dùng relative (sibling cùng folder, refactor-friendly khi move cả folder).
- Áp dụng convention Rust API Guidelines: prefer absolute
crate::cho clarity trong project lớn.
3 Special Path Keyword
Mỗi crate Rust có một cây module bắt đầu từ root (file main.rs với binary, hoặc lib.rs với library). Trong cây đó, một item (function, struct, enum, mod) có thể được tham chiếu qua một path — chuỗi tên cách nhau bởi ::. Rust cung cấp ba từ khóa đặc biệt làm điểm xuất phát của path, mỗi cái có ý nghĩa rõ ràng:
self— bắt đầu từ module hiện tại. Tương tự./trong filesystem: "ở ngay đây".super— bắt đầu từ parent module, tức module chứa module hiện tại. Tương tự../: "lên một tầng".crate— bắt đầu từ root crate, tức gốc cây module. Tương tự/(absolute path từ root).
Hình dung một cấu trúc 3 tầng đơn giản:
crate (root: src/main.rs)
├── config (mod config; → src/config.rs)
├── routes (mod routes; → src/routes/mod.rs)
│ ├── users (mod users; → src/routes/users.rs)
│ └── posts (mod posts; → src/routes/posts.rs)
└── services (mod services; → src/services/mod.rs)
├── auth (mod auth; → src/services/auth.rs)
└── db (mod db; → src/services/db.rs)
Từ bên trong routes::users, bạn có thể tham chiếu:
self::helper— itemhelpernằm trong chínhroutes::users.super::posts— sibling moduleposts(cùng parentroutes).crate::services::auth— đi xuyên cây từ root xuốngservices::auth.
Ba keyword này dùng được cả trong use statement (import) lẫn trong path inline (gọi trực tiếp crate::config::PORT). Tất cả ví dụ ở các mục sau đều dựa trên cấu trúc trên.
crate:: — Absolute Path Từ Root
crate luôn trỏ về root của crate hiện tại — file main.rs (binary) hoặc lib.rs (library). Mọi path bắt đầu bằng crate:: đều là absolute: nghĩa hoàn toàn không phụ thuộc nơi viết. Dù bạn đang ở routes::users, services::db, hay sâu hơn nữa, crate::config::PORT luôn trỏ tới cùng một item.
// src/main.rs (root crate)
mod config;
mod routes;
mod services;
fn main() {
println!("port = {}", config::PORT);
}
// src/config.rs
pub const PORT: u16 = 8080;
pub const HOST: &str = "0.0.0.0";
// src/routes/users.rs
use crate::config::PORT; // absolute: từ root đi xuống config::PORT
use crate::services::db; // absolute: import sibling-of-parent
pub fn list() {
println!("listening on port {PORT}");
db::query("SELECT * FROM users");
}
// src/services/auth.rs
use crate::config::HOST; // cùng cú pháp absolute, dù file nằm ở folder khác
use crate::routes::users; // cross-module
pub fn check() {
println!("auth checking host = {HOST}");
users::list();
}
Idiom của absolute path: "đọc xong import là biết item nằm đâu trong crate" — không cần mở terminal để xem file đang ở folder nào. Đây là cách tiếp cận chính trong các dự án lớn — Cargo crate phổ biến (tokio, axum, serde) đều dùng crate:: cho hầu hết import cross-module.
super:: — Relative Lên 1 Tầng
super trỏ về parent module của module hiện tại — đúng nghĩa lên một tầng. Có thể chain nhiều lần: super::super:: lên hai tầng (hiếm khi đẹp về code, nhưng hợp lệ). Useful nhất khi muốn import sibling: hai file cùng folder, cùng có parent — chỉ cần lên một tầng rồi xuống lại.
// src/routes/mod.rs
pub mod users;
pub mod posts;
// src/routes/users.rs
use super::posts; // lên 1 tầng (routes), xuống lại posts (sibling)
pub fn list_with_posts() {
println!("listing users");
posts::list(); // gọi sibling
}
// src/routes/posts.rs
use super::users; // ngược lại: posts dùng users
pub fn list() {
println!("listing posts");
}
// Có thể chain nhiều super:: nhưng hiếm khi đẹp
// src/services/db/postgres.rs
mod connection {
use super::super::db; // lên 2 tầng: từ connection → db → services::db
// → tương đương crate::services::db
}
super:: rất hữu dụng trong submodule muốn truy cập định nghĩa chung ở parent. Ví dụ: routes/users.rs và routes/posts.rs là hai sibling — nếu refactor đổi tên folder routes/ thành handlers/, các import super:: giữa chúng không cần sửa. Còn nếu viết crate::routes::posts, mọi nơi đều phải đổi thành crate::handlers::posts.
self:: — Cùng Module Hiện Tại
self bắt đầu path từ chính module hiện tại. Phần lớn trường hợp self:: là thừa — vì default của một path không có prefix đã là self. use self::helper::format và use helper::format hoàn toàn tương đương khi helper là child module trong cùng file.
// src/routes/users.rs
mod helper {
pub fn format(name: &str) -> String {
format!("[user] {name}")
}
}
// Cả 2 dòng dưới đây tương đương:
use self::helper::format; // explicit self
use helper::format; // implicit (default đã là self)
pub fn greet(name: &str) {
println!("{}", format(name));
}
Nơi self:: thật sự hữu dụng là trong nested use khi muốn vừa import module vừa import item bên trong nó:
// Nested use - self ở đây có nghĩa "import chính module này"
use std::io::{self, Read, Write};
// ↑ tương đương 3 dòng:
// use std::io;
// use std::io::Read;
// use std::io::Write;
fn read_stdin() -> io::Result<String> { // dùng io::Result được vì có self
let mut s = String::new();
io::stdin().read_to_string(&mut s)?;
Ok(s)
}
Trong nested form, self là cách duy nhất để biểu thị "module gốc của block {...}" cùng lúc với các con của nó. Không có self ở đây, bạn sẽ phải viết hai dòng use riêng. Đây là use case chính đáng nhất của từ khóa này.
Absolute Vs Relative Path
Hai cách tiếp cận có ưu nhược điểm rõ ràng — so sánh trực tiếp:
// Cùng tham chiếu services::auth từ trong routes::users
// ─── Cách 1: absolute (crate::) ───
use crate::services::auth;
// → Đọc: rõ ràng auth ở "crate root → services → auth"
// → Move file routes/users.rs sang folder khác: KHÔNG cần sửa
// → Đổi tên folder services/ thành svc/: PHẢI sửa "crate::svc::auth"
// ─── Cách 2: relative (super::) ───
use super::super::services::auth;
// → Đọc: phải biết file hiện tại đang ở đâu để hiểu "lên 2 tầng rồi xuống"
// → Move file routes/users.rs sang folder khác: PHẢI tính lại số super::
// → Đổi tên folder services/ thành svc/: vẫn phải sửa thành "super::super::svc::auth"
Tổng kết khác biệt:
- Absolute (
crate::): rõ ràng, stable về vị trí; đọc xong biết item đó ở đâu trong crate; không bị ảnh hưởng khi move file (giữ nguyên path). - Relative (
super::,self::): ngắn hơn khi target gần; refactor folder dễ (move cả folder không phải sửa); nhưng đọc một dònguse super::super::phải biết file đang ở folder nào mới hiểu đầy đủ.
Không có cái nào "đúng tuyệt đối" — phụ thuộc khoảng cách path và cấu trúc thay đổi của project.
Khi Nào Dùng Absolute
Ưu tiên crate:: trong các tình huống:
- Cross-module call xa: từ
routes::api::v1::usersmuốn dùngservices::db::pool. Viếtcrate::services::db::pooltường minh hơn làsuper::super::super::services::db::pool— đếmsuper::dễ sai. - Public API và utility chia sẻ: hằng số global, type công khai trong
lib.rs, các helper toàn project — đường tới chúng từ bất cứ đâu đều rõ quacrate::. - Readable không cần biết file ở đâu: khi review PR trên GitHub hay đọc snippet rời,
use crate::services::auth;tự đủ thông tin; cònuse super::auth;phải mở file thư mục mới hiểu. - Code generator / macro: macro mở rộng ở chỗ khác file gốc — nếu dùng
super::tương đối, kết quả expand có thể trỏ sai.crate::tuyệt đối luôn đúng. Đây là lý do hầu hết macro chính thống (Tokio, Serde) chỉ generate::crate_name::hoặccrate::.
Khi Nào Dùng Relative
Ưu tiên super::/self:: khi:
- Sibling import trong cùng folder: hai file
users.rsvàposts.rscùng nằm trongroutes/—use super::posts;ngắn và rõ "lấy sibling". - Sub-module bên trong module ngang hàng: trong
services/db/mod.rstham chiếuservices/db/postgres.rsbằngself::postgreshayuse postgres;đều ngắn hơncrate::services::db::postgres. - Refactor friendly khi move cả folder: nếu chuyển toàn bộ
routes/thànhweb::routes/, mọisuper::giữa các file con củaroutes/vẫn đúng. Còncrate::routes::...ngoài folder phải sửa thànhcrate::web::routes::...— đổi lại, code bên trongroutes/không phải sửa. - Test module trong file: pattern
#[cfg(test)] mod tests { use super::*; }rất phổ biến — test cần truy cập tất cả item ở module cha, dùngsuper::*ngắn gọn. Đây là use case "kinh điển" củasuper::trong code Rust.
Best Practice
- Mỗi project chọn 1 convention chính. Cả team viết cùng kiểu, không trộn random — đọc PR sẽ ổn định hơn. Khuyến nghị: cross-module dùng
crate::, intra-module/test dùngsuper::. - Rust API Guidelines prefer absolute
crate::cho clarity, đặc biệt với library public — người dùng đọc source crate dễ định vị item. - Không dùng
self::redundant.use self::helperkhi đã đủ vớiuse helperchỉ thêm noise. Giữselfcho nested form{self, X, Y}. - Cảnh giác chain
super::super::super::. Quá 2 tầngsuper::là dấu hiệu cấu trúc cần refactor — đổi sangcrate::sẽ rõ và an toàn hơn. - Format with rustfmt.
cargo fmtsẽ sort import theo nhóm (std → extern crate →crate::→super::/self::), đồng nhất style cả project.
Tổng Kết
- 3 special path keyword:
self(module hiện tại, như./),super(parent module, như../),crate(root crate, như/). crate::= absolute path: ý nghĩa không đổi dù viết ở đâu trong cây module; idiom cho cross-module import xa và macro-generated code.super::= relative lên 1 tầng: tốt cho sibling import; chain nhiềusuper::trên 2 lần là dấu hiệu nên đổi sangcrate::.self::phần lớn redundant vì default đã là self; chỉ thực sự cần trong nested useuse foo::{self, bar}để vừa import module vừa import con.- Absolute vs relative: absolute = rõ và stable; relative = ngắn và refactor friendly khi move cả folder.
- Convention: Rust API Guidelines prefer absolute
crate::cho clarity; trường hợp dùngsuper::kinh điển là test moduleuse super::*;.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Cho cây module
crate → handlers → api → users. Từ trongusers.rs, viết hai cách import modulehandlers::api::posts: một dùngcrate::, một dùngsuper::. Cái nào ngắn hơn? - Cho file
src/services/db.rscó nội dungmod connection { ... }. Bên trongconnection, viếtuseđể truy cậpcrate::config::HOSTtheo 3 cách: absolute, relative bằngsuper::, và trộn (lên một tầng rồi crate). Cách nào dễ đọc nhất? - Test module
#[cfg(test)] mod tests { ... }nằm trongsrc/services/auth.rs. Bên trongtests, viết một dònguseđể bring toàn bộ item của module cha (auth) vào scope. Tại sao đây là pattern phổ biến nhất củasuper::? - Bạn viết
use self::helper::format;trong một file có child modulehelper. Đồng nghiệp review nói "bỏself::đi". Có nên bỏ không? Tại sao? - So sánh hai dòng:
use std::io::{self, Read};vàuse std::io; use std::io::Read;. Chúng tương đương về mặt ngữ nghĩa không?selfở đây có nghĩa gì? - Project có cấu trúc
routes/đang bị refactor đổi tên thànhhandlers/. Fileroutes/users.rshiện đanguse super::posts;vàuse crate::routes::common;. Sau khi đổi tên folder, dòng nào cần sửa, dòng nào không?
Đáp án
- Absolute:
use crate::handlers::api::posts;. Relative:use super::posts;(chỉ 1 tầng vìpostslà sibling củauserstrong cùngapi). Relative ngắn hơn rõ rệt — đây là use case lý tưởng chosuper::. - Absolute:
use crate::config::HOST;(luôn đúng, đọc rõ). Relative full:use super::super::config::HOST;(đếmsuper::dễ sai). Trộn: không thực sự có "trộn" được —cratechỉ dùng ở đầu path. Absolute dễ đọc nhất; đây là minh họa rõ vì sao API Guidelines prefercrate::. use super::*;. Đây là pattern phổ biến vì test cần truy cập tất cả private item của module cha (cả pub lẫn private trong cùng crate đều thấy nhau theo Rust visibility rules), vàsuper::*là cách ngắn gọn nhất.- Nên bỏ.
self::ở đây không thêm thông tin gì — default đã là self. Code base nhất quán không cóself::redundant đọc gọn hơn. Chỉ giữselfkhi nested form{self, ...}. - Có, hoàn toàn tương đương.
selftrong nested form nghĩa là "import chính moduleio" (để dùngio::Result,io::stdin()) cùng lúc với các con (Read). Không cóself, chỉuse std::io::Read;sẽ không bringiovào scope. use super::posts;không cần sửa — vẫn lên 1 tầng (giờ làhandlers/) rồi xuốngposts.use crate::routes::common;phải sửa thànhuse crate::handlers::common;. Đây chính là điểm relative path "thắng" khi refactor cả folder.
Bài Tiếp Theo
Bài 116: pub use — Re-export — biết path rồi, bước tiếp là phơi item ra ngoài qua một path khác. pub use foo::bar; không chỉ bring bar vào scope, mà còn re-export nó như thành viên của module hiện tại. Đây là kỹ thuật xương sống của facade pattern trong lib.rs — flatten public API, ẩn cấu trúc nội bộ, kiểm soát những gì user thấy.
