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

Lỗi Rust E0616: field of struct is private (field riêng tư)

E0616 nghĩa là: field (trường) bạn yêu cầu có tồn tại, nhưng nó riêng tư (private) đối với module nơi struct được định nghĩa. Bạn được phép giữ struct, chỉ là không được thò tay vào bên trong nó. Ai cũng gặp lỗi này khi lần đầu chuyển một struct vào module riêng, và khi gõ v.len thay vì v.len(). Cách sửa thường là gọi một phương thức. Trang này thuộc loạt bài Giải thích các lỗi trình biên dịch Rust.

error[E0616]: field `balance` of struct `Account` is private
--> src/main.rs:16:50
|
16 | println!("{} has {}", account.owner, account.balance); // ❌ private field
| ^^^^^^^ private field

Trình biên dịch báo field balance của struct Account là riêng tư. Đây là chương trình gây ra lỗi:

mod bank {
pub struct Account {
pub owner: String,
balance: u64, // private
}
impl Account {
pub fn new(owner: &str) -> Account {
Account { owner: owner.to_string(), balance: 0 }
}
}
}
fn main() {
let account = bank::Account::new("Ferris");
println!("{} has {}", account.owner, account.balance); // ❌ private field
}

Cùng một lỗi này có vài biến thể:

Bạn viết rustc nói Cách sửa
account.balance field `balance` of struct `Account` is private Cách sửa 2 hoặc 3
scores.len trên một Vec field `len` of struct `Vec` is private, kèm help: a method `len` also exists, call it with parentheses Cách sửa 1
timeout.secs trên một Duration field `secs` of struct `Duration` is private Cách sửa 1 (as_secs())
account.balance = 500 cùng lỗi E0616: ghi cũng bị chặn Cách sửa 2

Mũi tên chỉ vào tên field sau dấu chấm. account.owner trên cùng dòng thì không sao, vì owner được đánh dấu pub. balance thì không, nên chỉ code bên trong mod bank mới được đọc hay thay đổi nó.

Hãy để ý rằng bản thân Account là pub. Đây là điều khiến nhiều người bất ngờ: cho struct công khai không làm cho các field của nó công khai. Mỗi field cần pub riêng, nếu không thì vẫn riêng tư.

Hãy nghĩ đến một ngân hàng. Bạn có thể bước vào, và ai cũng thấy tên trên tài khoản của bạn. Nhưng bạn không thể đi vòng ra sau quầy và mở két để đếm tiền. Bạn phải hỏi giao dịch viên, và giao dịch viên làm theo quy định của ngân hàng.

Trong Rust, mọi thứ mặc định là riêng tư: hàm, module, và cả field của struct (xem Module). Tính riêng tư gắn với module, không phải với kiểu. Mọi code bên trong mod bank đều chạm được vào balance, còn code bên ngoài thì không.

Mục đích là để struct giữ được quy tắc của riêng nó. Giả sử số dư ngân hàng không bao giờ được âm. Nếu mọi phần của chương trình đều viết được account.balance = ..., thì phần nào cũng có thể phá vỡ quy tắc đó, và bạn sẽ phải lục tung cả codebase để tìm xem phần nào đã làm. Nếu field là riêng tư, chỉ vài phương thức bên trong mod bank mới thay đổi được nó. Kiểm tra quy tắc ở đó, là nó đúng ở mọi nơi. Ý tưởng này gọi là đóng gói (encapsulation).

Thư viện chuẩn cũng làm như vậy. Một Vec có các field riêng tư cho độ dài và vùng nhớ đệm của nó. Nếu bạn viết được v.len = 100 trên một vector chỉ chứa 3 phần tử, thì đọc v[50] sẽ đọc bộ nhớ ngẫu nhiên. Giữ len riêng tư chính là điều làm cho Vec an toàn khi dùng.

Cách sửa 1: Gọi phương thức thay vì đọc field

Phần tiêu đề “Cách sửa 1: Gọi phương thức thay vì đọc field”

Chọn cách này khi struct đến từ thư viện chuẩn hoặc một crate khác. Bạn không thể sửa code của họ, và họ luôn cung cấp phương thức cho những gì bạn được phép xem. Khi rustc nói help: a method ... also exists, hãy thêm ():

use std::time::Duration;
fn main() {
let scores = vec![10, 20, 30];
println!("{} điểm số", scores.len());
let timeout = Duration::from_secs(90);
println!("Thời gian chờ: {} giây", timeout.as_secs());
}

Kết quả:

3 điểm số
Thời gian chờ: 90 giây
▶ Tự chạy thử

Tên phương thức không phải lúc nào cũng trùng tên field (secs được đọc bằng as_secs()). Hãy xem trang của kiểu đó trên docs.rs, hoặc để editor liệt kê các phương thức sau khi bạn gõ dấu chấm.

Cách sửa 2: Thêm getter, và các phương thức tuân theo quy tắc

Phần tiêu đề “Cách sửa 2: Thêm getter, và các phương thức tuân theo quy tắc”

Chọn cách này khi đó là struct của chính bạn và field có một quy tắc cần bảo vệ. Giữ field riêng tư, thêm một pub fn trả về giá trị của nó (một getter), và chỉ thay đổi nó qua các phương thức có kiểm tra quy tắc:

mod bank {
pub struct Account {
pub owner: String,
balance: i64, // riêng tư: chỉ module này thay đổi được
}
impl Account {
pub fn new(owner: &str) -> Account {
Account { owner: owner.to_string(), balance: 0 }
}
// Getter: ai cũng được đọc số dư.
pub fn balance(&self) -> i64 {
self.balance
}
pub fn deposit(&mut self, amount: i64) {
if amount > 0 {
self.balance += amount;
}
}
// Quy tắc nằm ở đây: số dư không bao giờ xuống dưới 0.
pub fn withdraw(&mut self, amount: i64) -> Result<(), String> {
if amount > self.balance {
return Err(format!("chỉ còn {} trong tài khoản", self.balance));
}
self.balance -= amount;
Ok(())
}
}
}
fn main() {
let mut account = bank::Account::new("Ferris");
account.deposit(100);
println!("{} có {}", account.owner, account.balance());
match account.withdraw(500) {
Ok(()) => println!("Đã rút 500"),
Err(e) => println!("Không rút được 500: {e}"),
}
println!("Số dư vẫn là {}", account.balance());
}

Kết quả:

Ferris có 100
Không rút được 500: chỉ còn 100 trong tài khoản
Số dư vẫn là 100
▶ Tự chạy thử

Rust cho phép phương thức và field trùng tên, nên balance() là tên thông dụng cho getter (chứ không phải get_balance()). Khi getter đã có, rustc thậm chí còn gợi ý nó: help: a method `balance` also exists, call it with parentheses. deposit và withdraw là các setter của bạn: chúng thay đổi field, nhưng chỉ theo những cách mà quy tắc cho phép. Xem thêm về phương thức ở Struct.

Cách sửa 3: Cho field là pub khi dữ liệu đơn giản

Phần tiêu đề “Cách sửa 3: Cho field là pub khi dữ liệu đơn giản”

Chọn cách này khi giá trị nào cũng được và không có gì khác phụ thuộc vào field. Một điểm trên lưới, một màu sắc, hay một struct thiết lập không có quy tắc nào cần bảo vệ. Getter ở đó chỉ làm bạn gõ thêm:

mod geometry {
pub struct Point {
pub x: i32,
pub y: i32,
}
}
fn main() {
let mut p = geometry::Point { x: 3, y: 4 };
p.x += 1;
println!("Điểm tại ({}, {})", p.x, p.y);
}

Kết quả:

Điểm tại (4, 4)
▶ Tự chạy thử

Khi chỉ code của chính bạn mới nên thấy field, hãy viết pub(crate) x: i32 (mọi module trong crate này) hoặc pub(super) x: i32 (chỉ module cha). Trong một chương trình, chúng hoạt động như pub; trong một thư viện, chúng giữ field nằm ngoài API công khai của bạn. Trang E0603 có bảng liệt kê tất cả các mức.

Có nên cứ cho nó là pub? Hãy tự hỏi: “Có giá trị nào của field này, hoặc một thay đổi chỉ riêng trên nó, làm struct bị sai không?” Một số dư âm, một len không khớp với dữ liệu, một total không còn bằng tổng các món: hãy giữ những thứ đó riêng tư và dùng Cách sửa 2. Nếu câu trả lời là “không, giá trị nào cũng được”, thì pub là lựa chọn thẳng thắn.

len trên một Vec. Bạn viết scores.len và nhận field `len` of struct `Vec` is private. Đó là vì Vec thật sự có một field riêng tư tên len. Cách sửa là scores.len(). Chi tiết thú vị: name.len trên một String cho ra một lỗi khác, E0615 (attempted to take value of method), vì String không có field nào tên như vậy. Cách sửa vẫn thế.

Gán field từ bên ngoài. account.balance = 500; cũng cho ra E0616. Ghi cũng bị chặn. Đừng cho field là pub chỉ để gán nó; hãy thêm một phương thức như deposit thực hiện thay đổi và kiểm tra nó.

Sau khi chuyển struct vào module hay file riêng. Code trong main.rs đã dùng user.email suốt mấy tháng. Rồi bạn chuyển User vào src/user.rs và mọi chỗ truy cập field đều hỏng. Những field trước đây vẫn ổn giờ đã thành riêng tư đối với mod user. Hãy thêm pub cho các field là dữ liệu đơn giản, và getter cho phần còn lại.

Test. Một khối #[cfg(test)] mod tests trong cùng file là một module con, nên nó đọc được field riêng tư. Các test trong thư mục tests/ nằm ngoài crate của bạn và chỉ dùng được API pub, như mọi người dùng khác.

  • E0603: function `x` is private. Cùng ý tưởng, nhưng cho hàm, struct và module.
  • E0451: cùng một field riêng tư, nhưng khi tạo struct bằng Account { ... }.
  • E0624: method `x` is private. Phương thức (hoặc new) cũng cần pub.
  • E0609: no field `x` on type `T` . Field hoàn toàn không tồn tại (thường do gõ nhầm).
  • E0615: attempted to take value of method. Bạn viết x.len nhưng ý là x.len().
  • Tất cả các lỗi trình biên dịch: danh sách đầy đủ các trang lỗi.

Kiểm tra nhanh

1. pub struct Account { balance: u64 } nằm trong mod bank. main có đọc được account.balance không?

2. Bạn nhận `field len of struct Vec is private`. Cách sửa là gì?

3. Một struct Score có field points phải luôn nằm trong khoảng 0 đến 100. Thiết kế nào tốt nhất?

Điểm: 0 / 3