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

Lỗi Rust E0451: field of struct is private (field riêng tư khi tạo struct)

E0451 nghĩa là: bạn cố tạo một struct bằng một literal như Account { ... }, nhưng một trong các field (trường) của nó là riêng tư. Để viết một struct literal, bạn phải điền mọi field, nên chỉ cần một field riêng tư là bạn bị khóa ngoài. Đó là có chủ đích: module muốn bạn đi qua hàm khởi tạo (constructor) của chính nó. Cách sửa gần như luôn là gọi (hoặc thêm) một hàm new. 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[E0451]: field `balance` of struct `Account` is private
--> src/main.rs:21:9
|
19 | let account = bank::Account {
| ------------- in this type
20 | owner: String::from("Ferris"),
21 | balance: 1_000_000, // ❌ private field
| ^^^^^^^ private field

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

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

Bạn cũng có thể gặp một trong những “họ hàng” gần của nó:

Bạn viết rustc nói
field riêng tư trong literal error[E0451]: field `balance` of struct `Account` is private
Config { verbose: true, ..Default::default() } cũng là E0451, chỉ vào ..Default::default() (xem Cách sửa 3)
literal không có field riêng tư cannot construct `Account` with struct literal syntax due to private fields, thường kèm help: you might have meant to use the `new` associated function
Percent(150) cho một tuple struct E0603: tuple struct constructor `Percent` is private

Tất cả đều có chung cách sửa.

Mũi tên chỉ vào field mà bạn không được phép gán: balance. owner là pub, nên gán nó không sao. Nhưng một struct literal là “tất cả hoặc không gì cả”. Bạn không thể bỏ balance ra (Rust không tự điền field bị thiếu), và cũng không thể gán nó. Vì vậy, bên ngoài mod bank, đơn giản là không có cách nào viết Account { ... }.

Hãy hình dung việc mở một tài khoản ngân hàng thật. Bạn không được tự ghi số dư ban đầu lên tờ đơn. Bạn điền tên, ngân hàng tạo tài khoản, và ngân hàng quyết định nó bắt đầu từ 0. Account::new chính là tờ đơn đó.

Field của struct mặc định là riêng tư, giống như hàm và module (xem Module). Nếu code bên ngoài có thể tạo một Account với bất kỳ số dư nào nó thích, các quy tắc của module sẽ chẳng còn ý nghĩa gì. Ai đó có thể tạo tài khoản với một triệu, hoặc âm năm mươi. Vì chỉ mod bank mới viết được literal, mọi Account trong chương trình đều được tạo bởi code biết rõ quy tắc.

Còn một lợi ích thứ hai. Nếu sau này bạn thêm một field riêng tư, như created_at hay history, code bên ngoài module không bị hỏng, vì nó chưa bao giờ viết literal. Nó gọi new, và bạn chỉ cần cập nhật new ở một chỗ.

Cách sửa 1: Dùng (hoặc thêm) hàm khởi tạo pub fn new

Phần tiêu đề “Cách sửa 1: Dùng (hoặc thêm) hàm khởi tạo pub fn new”

Hầu hết thời gian hãy chọn cách này. Trước hết hãy tìm một hàm new (hoặc from_..., with_...). rustc thường chỉ bạn đến đó. Nếu đó là struct của bạn và chưa có, hãy viết một hàm:

mod bank {
pub struct Account {
pub owner: String,
balance: i64, // riêng tư
}
impl Account {
// Mọi tài khoản bắt đầu từ 0. Tiền chỉ vào qua deposit().
pub fn new(owner: &str) -> Account {
Account { owner: owner.to_string(), balance: 0 }
}
pub fn deposit(&mut self, amount: i64) {
if amount > 0 {
self.balance += amount;
}
}
pub fn balance(&self) -> i64 {
self.balance
}
}
}
fn main() {
let mut account = bank::Account::new("Ferris");
account.deposit(250);
println!("{} có {}", account.owner, account.balance());
}

Kết quả:

Ferris có 250
▶ Tự chạy thử

Bên trong mod bank literal được phép, nên new có thể thoải mái viết Account { ... }. Đừng quên pub cho chính new; thiếu nó bạn sẽ nhận E0624 (associated function `new` is private). Nếu có những đầu vào cần bị từ chối, hãy cho new trả về Option<Account> hoặc Result<Account, String>; trang E0603 có một ví dụ. Xem thêm về hàm khởi tạo ở Struct.

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

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

Chọn cách này khi mọi tổ hợp giá trị đều ổn. Một màu sắc chỉ là ba con số; không có màu nào “sai” cần đề phòng. Khi đó literal là cách rõ ràng nhất để tạo nó:

mod theme {
pub struct Color {
pub r: u8,
pub g: u8,
pub b: u8,
}
}
fn main() {
let ferris_orange = theme::Color { r: 247, g: 76, b: 0 };
println!("#{:02x}{:02x}{:02x}", ferris_orange.r, ferris_orange.g, ferris_orange.b);
}

Kết quả:

#f74c00
▶ Tự chạy thử

Bạn cần pub cho mọi field. Nếu chỉ crate của bạn mới nên tạo nó, pub(crate) trên các field cũng được.

Khi nào nên giữ field riêng tư: khi một giá trị có thể sai (số dư âm, phần trăm trên 100, email không có @), hoặc khi hai field phải khớp với nhau (một total phải bằng tổng của items). Khi đó literal sẽ cho phép người ta bỏ qua bước kiểm tra, và Cách sửa 1 là lựa chọn đúng.

Cách sửa 3: Bắt đầu từ Default, rồi thay đổi các field công khai

Phần tiêu đề “Cách sửa 3: Bắt đầu từ Default, rồi thay đổi các field công khai”

Chọn cách này cho các struct “thiết lập” có giá trị mặc định hợp lý, trong đó một số field công khai và một số thì không. Trước hết là cái bẫy. Cú pháp cập nhật struct ..Default::default() trông như phải chạy được, nhưng không phải vậy:

mod settings {
pub struct Config {
pub verbose: bool,
retries: u32, // private: must stay between 1 and 10
}
impl Default for Config {
fn default() -> Config {
Config { verbose: false, retries: 3 }
}
}
}
fn main() {
let config = settings::Config { verbose: true, ..Default::default() };
println!("{}", config.verbose);
}
error[E0451]: field `retries` of struct `Config` is private
--> src/main.rs:15:54
|
15 | let config = settings::Config { verbose: true, ..Default::default() };
| ^^^^^^^^^^^^^^^^^^ field `retries` is private

..Default::default() vẫn là một struct literal: nó sao chép retries vào struct mới, và điều đó được tính là gán field. Thay vào đó, hãy tạo giá trị mặc định trước rồi thay đổi các field công khai sau:

mod settings {
pub struct Config {
pub verbose: bool,
retries: u32, // riêng tư: phải nằm trong khoảng 1 đến 10
}
impl Default for Config {
fn default() -> Config {
Config { verbose: false, retries: 3 }
}
}
impl Config {
pub fn retries(&self) -> u32 {
self.retries
}
}
}
fn main() {
let mut config = settings::Config::default();
config.verbose = true; // field công khai có thể thay đổi sau đó
println!("verbose: {}, retries: {}", config.verbose, config.retries());
}

Kết quả:

verbose: true, retries: 3
▶ Tự chạy thử

Nếu mọi field đều là pub, ..Default::default() chạy bình thường. Chỉ có field riêng tư mới chặn nó.

Cách sửa 4: Cung cấp các phương thức kiểu builder

Phần tiêu đề “Cách sửa 4: Cung cấp các phương thức kiểu builder”

Chọn cách này khi có nhiều tùy chọn và một số trong đó có quy tắc. Mỗi phương thức nhận struct, thay đổi một thiết lập (có kiểm tra), rồi trả struct lại, nên các lời gọi nối tiếp nhau rất gọn:

mod settings {
pub struct Config {
verbose: bool,
retries: u32,
}
impl Config {
pub fn new() -> Config {
Config { verbose: false, retries: 3 }
}
pub fn verbose(mut self, on: bool) -> Config {
self.verbose = on;
self
}
// Quy tắc nằm ở đây: từ 1 đến 10 lần thử lại.
pub fn retries(mut self, n: u32) -> Config {
self.retries = n.clamp(1, 10);
self
}
pub fn describe(&self) -> String {
format!("verbose: {}, retries: {}", self.verbose, self.retries)
}
}
}
fn main() {
let config = settings::Config::new().verbose(true).retries(50);
println!("{}", config.describe());
}

Kết quả:

verbose: true, retries: 10
▶ Tự chạy thử

Yêu cầu 50 lần thử lại đã lặng lẽ trở thành 10, vì quy tắc nằm ở một chỗ duy nhất. Nhiều crate làm như vậy, đôi khi với một kiểu ConfigBuilder riêng và một lời gọi .build() cuối cùng. Xem phiên bản đó ở Design pattern.

Sau khi chuyển struct vào một module. Mọi User { name, email } trong main.rs đều chạy tốt cho đến khi User được chuyển vào mod user (hoặc src/user.rs). Giờ tất cả đều lỗi. Hãy quyết định cho từng struct: dữ liệu đơn giản thì cho field pub (Cách sửa 2); thứ gì có quy tắc thì dùng pub fn new (Cách sửa 1).

Kiểu từ thư viện chuẩn hoặc từ crate. Bạn không thể viết Vec { ... } hay Duration { ... }. rustc nói cannot construct `Vec<_, _>` with struct literal syntax due to private fields và liệt kê các hàm như Vec::new() và Vec::with_capacity(_). Hãy dùng chúng, hoặc Duration::from_secs(5).

Một struct #[non_exhaustive] từ crate khác. Ở đây ngay cả field công khai cũng không giúp được, và mã lỗi cũng khác: error[E0639]: cannot create non-exhaustive struct using struct expression. Crate đó muốn thêm field sau này mà không làm hỏng code của bạn, nên hãy dùng hàm khởi tạo của nó hoặc Default, đúng như Cách sửa 1 và Cách sửa 3.

  • E0616: cùng một field riêng tư, nhưng khi đọc nó (account.balance).
  • E0603: tuple struct constructor `X` is private, và các hàm, struct hay module riêng tư.
  • E0624: associated function `new` is private. Hàm khởi tạo của bạn cũng cần pub.
  • E0560: struct `X` has no field named `y` . Gõ nhầm tên field trong literal.
  • E0063 (tiếng Anh): missing field `x` in initializer of `Y` . Bạn đã bỏ sót một field công khai.
  • 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 Ticket { pub title: String, id: u32 } nằm trong mod tickets. Làm sao main tạo được một Ticket?

2. Vì sao Config { verbose: true, ..Default::default() } lỗi khi Config có một field riêng tư?

3. Struct nào hợp với việc cho mọi field là pub và dùng literal đơn giản?

Điểm: 0 / 3