Bỏ qua để đến nội dung

Module trong Rust

Mọi ví dụ cho đến giờ đều nằm gọn trong một file main.rs. Với chương trình nhỏ thì vậy là ổn, nhưng dự án thật sẽ chia code thành những phần hợp lý. Công cụ của Rust cho việc đó là hệ thống module — mod, pub và use.

Module là một khối có tên, gom các đoạn code liên quan lại với nhau:

mod math {
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
}
fn main() {
let sum = math::add(2, 3);
println!("{sum}");
}

Kết quả:

5
▶ Tự chạy thử

Hai điều cần để ý: hàm được gọi bằng math::add(...), dùng tên module như một đường dẫn (path). Và nó được đánh dấu pub — nếu không có, sẽ không thể gọi nó từ bên ngoài module.

Điều này đáng nói rõ vì nó khiến nhiều người bất ngờ: nội dung của module mặc định là riêng tư (private). Nếu bạn bỏ pub khỏi ví dụ trên, nó sẽ không biên dịch được:

mod math {
fn add(a: i32, b: i32) -> i32 { // không có `pub`
a + b
}
}
fn main() {
let sum = math::add(2, 3); // ❌ lỗi: hàm `add` là riêng tư
println!("{sum}");
}

Bạn phải đánh dấu pub một cách tường minh cho mọi thứ mà bạn muốn code bên ngoài module được dùng. Đây là chủ ý — nó cho phép module giấu các hàm phụ trợ bên trong và chỉ lộ ra những phần được thiết kế làm API công khai.

Module có thể chứa module khác, đó là cách các dự án lớn xây dựng nên một cấu trúc dạng cây:

mod shapes {
pub mod circle {
pub fn area(radius: f64) -> f64 {
3.14159 * radius * radius
}
}
}
fn main() {
let area = shapes::circle::area(2.0);
println!("{area}");
}
▶ Tự chạy thử

Lần nào cũng viết shapes::circle::area(...) thì thật mệt. use đưa một đường dẫn vào phạm vi để bạn gọi nó trực tiếp:

mod shapes {
pub mod circle {
pub fn area(radius: f64) -> f64 {
3.14159 * radius * radius
}
}
}
use shapes::circle;
fn main() {
let area = circle::area(2.0); // ngắn hơn shapes::circle::area
println!("{area}");
}
▶ Tự chạy thử

Thật ra bạn đã dùng use rồi — đó chính là từ khóa trong bài HashMap (use std::collections::HashMap;).

Các ví dụ ở trên đặt mọi thứ trong một file, vì Rust Playground yêu cầu như vậy. Trong một dự án thật trên máy của bạn, mỗi module thường có file riêng. Nếu bạn có một dự án với module math, nó thường trông như thế này:

src/
├── main.rs
└── math.rs
src/main.rs
mod math; // báo cho Rust: tìm code của module này trong math.rs
fn main() {
let sum = math::add(2, 3);
println!("{sum}");
}
src/math.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}

mod math; (có dấu chấm phẩy, không có thân) bảo Rust nạp nội dung module từ math.rs trong cùng thư mục. Đây là mẫu bạn sẽ dùng ngay khi dự án lớn hơn một file.

Tạo module tên convert với hai hàm công khai: c_to_f (độ C sang độ F) và f_to_c (chiều ngược lại). Từ main, in ra 100°C là 212°F và 212°F là 100°C.

Gợi ý: công thức là c * 9.0 / 5.0 + 32.0 và (f - 32.0) * 5.0 / 9.0. Đừng quên pub, và gọi các hàm kèm tên module, như convert::c_to_f(100.0).

Xem lời giải
mod convert {
pub fn c_to_f(c: f64) -> f64 {
c * 9.0 / 5.0 + 32.0
}
pub fn f_to_c(f: f64) -> f64 {
(f - 32.0) * 5.0 / 9.0
}
}
fn main() {
println!("100°C là {}°F", convert::c_to_f(100.0));
println!("212°F là {}°C", convert::f_to_c(212.0));
}

Kết quả:

100°C là 212°F
212°F là 100°C
▶ Tự chạy thử

Cả hai hàm đều là pub, nên main (nằm bên ngoài module) gọi được chúng. Bỏ một chữ pub đi và trình biên dịch sẽ báo hàm đó là riêng tư.

Tạo module lồng school::grades với hàm công khai letter(score: u32) -> char trả về 'A' cho điểm từ 90 trở lên, 'B' từ 80 trở lên, và 'C' cho các trường hợp còn lại. Bên trong grades, viết thêm một hàm phụ trợ riêng tư is_at_least(score: u32, min: u32) -> bool mà letter sử dụng. Đưa school::grades vào phạm vi bằng use, rồi in xếp loại cho 95, 85 và 70.

Gợi ý: cả mod grades bên trong school và fn letter đều cần pub, nhưng is_at_least thì không nên có. Code bên trong cùng một module luôn gọi được các hàm riêng tư.

Xem lời giải
mod school {
pub mod grades {
pub fn letter(score: u32) -> char {
if is_at_least(score, 90) {
'A'
} else if is_at_least(score, 80) {
'B'
} else {
'C'
}
}
// riêng tư: chỉ code bên trong `grades` mới gọi được hàm này
fn is_at_least(score: u32, min: u32) -> bool {
score >= min
}
}
}
use school::grades;
fn main() {
for score in [95, 85, 70] {
println!("{score} -> {}", grades::letter(score));
}
}

Kết quả:

95 -> A
85 -> B
70 -> C
▶ Tự chạy thử

letter gọi được is_at_least vì chúng nằm trong cùng một module. Từ main, grades::is_at_least(95, 90) sẽ báo lỗi “function is_at_least is private” (hàm is_at_least là riêng tư). Đó chính là mục đích: module chỉ lộ ra một API công khai nhỏ và giữ các hàm phụ trợ cho riêng mình.

  • mod name { ... } gom code liên quan thành một module có tên.
  • Mọi thứ bên trong module mặc định là riêng tư — thêm pub để lộ nó ra bên ngoài module.
  • use đưa một đường dẫn vào phạm vi để bạn không phải viết đầy đủ mỗi lần.
  • Trong dự án nhiều file, mod name; (không có thân) nạp code của module đó từ file cùng tên.
  • Code bên trong module luôn dùng được các phần riêng tư của module đó, nên hãy giữ các hàm phụ trợ ở chế độ riêng tư và chỉ lộ ra những gì nơi gọi cần.

Kiểm tra nhanh

1. Mặc định, các phần tử bên trong một mod là công khai hay riêng tư?

2. Làm sao bảo Rust nạp code của module từ math.rs thay vì viết thẳng bên trong?

Điểm: 0 / 2