Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Viết được biểu thức khởi tạo struct instance theo cú pháp
StructName { field1: value1, field2: value2, ... }và hiểu vì sao đây là biểu thức (expression) chứ không phải statement. - Biết Rust match field theo tên, không theo vị trí — đảo thứ tự trong block khởi tạo vẫn compile bình thường, không như C struct literal cũ.
- Hiểu quy tắc "tất cả field phải có giá trị" và đọc được compile error
E0063 "missing fields"; biết Rust cố tình không có default value ngầm như C++ (= 0cho int) để tránh bug bỏ sót khởi tạo. - Access field qua dot notation
instance.field; biết kết quả là một place expression với type chính xác là type field — có thể đọc, có thể assign nếu instance làmut. - Hiểu vì sao Rust không có "partial mut" (chỉ một số field mut) — mutability gắn với binding, không gắn với field; toàn instance mut hoặc toàn immutable.
- Khởi tạo nested struct (struct chứa struct khác) và biết khi nào nên đẩy struct lớn lên heap qua
Box::new(...)để tránh stack overflow. - Tránh 3 pitfall phổ biến:
E0063thiếu field,E0308sai type ở value field, và ownership move khilet u2 = u1;với struct chứa field non-Copy.
Bài này tiếp nối Bài 82: Định Nghĩa Struct Trong Rust và là bước chuẩn bị cho Bài 84: Field Init Shorthand — một idiom giúp viết block khởi tạo gọn hơn khi tên biến trùng tên field.
Cú Pháp Tạo Instance
Sau khi định nghĩa struct ở Bài 82, dùng cú pháp struct expression: tên struct, theo sau là cặp dấu { }, bên trong liệt kê field_name: value ngăn cách bởi dấu phẩy. Toàn bộ là một biểu thức trả về instance — có thể bind vào biến, return từ function, hoặc làm argument.
struct User {
name: String,
age: u32,
email: String,
}
fn main() {
let u = User {
name: String::from("Canh"),
age: 30,
email: "[email protected]".into(),
};
println!("name = {}, age = {}, email = {}", u.name, u.age, u.email);
}
Một số điểm đáng chú ý:
- Struct expression là một expression:
User { ... }có giá trị là instance vừa tạo. Có thể viết thẳngfn make_user() -> User { User { name: "x".into(), age: 0, email: "".into() } }mà không cần biến trung gian. - Mỗi field viết dạng
name: expression. Phía bên phải là biểu thức bất kỳ trả về đúng type khai báo trong định nghĩa struct. - Dấu phẩy cuối được phép:
email: "...".into(),với dấu phẩy sau value cuối là idiom Rust — giúp diff git sạch khi thêm field mới. - Hai cách tạo String:
String::from("Canh")tường minh, và"[email protected]".into()dùng traitInto<String>với type inference. Cả hai tương đương khi field type làString.
Struct expression chỉ hợp lệ khi compiler biết được struct definition (đã use hoặc cùng module). Nếu field pub bị giấu (field private), code ngoài module sẽ không gọi được constructor này — đây là cơ chế encapsulation Rust dùng để buộc đi qua hàm new() (sẽ học ở Bài 89).
Field Order Không Quan Trọng
Rust match field theo tên, không theo thứ tự khai báo trong block khởi tạo. Đảo thứ tự cho dễ đọc, hoặc nhóm field theo logic — compiler không phàn nàn.
struct User {
name: String,
age: u32,
email: String,
}
fn main() {
// Khai báo định nghĩa: name, age, email
// Khởi tạo theo thứ tự khác: age, email, name — vẫn OK
let u = User {
age: 30,
email: "[email protected]".into(),
name: String::from("Canh"),
};
println!("{} ({} tuổi) - {}", u.name, u.age, u.email);
}
Khác biệt với một số ngôn ngữ khác:
- C struct literal cũ (positional):
struct User u = {"Canh", 30, "..."}— phải đúng thứ tự, dễ nhầm khi thêm field giữa chừng. C99 mới thêm designated initializer.name = "Canh"cho phép đảo, nhưng vẫn không bắt buộc. - Rust struct literal: luôn dạng named — bắt buộc viết
field: value, nên thứ tự không quan trọng. Refactor thêm/bớt/đổi vị trí field trong definition không phá callsite (miễn là không đổi tên field). - Tuple struct (Bài 86) thì có theo thứ tự, vì không có tên field — sẽ học sau.
Lợi ích thực chiến: khi struct có 10-20 field, người đọc code có thể nhóm theo ngữ nghĩa (auth fields cùng nhau, profile fields cùng nhau) thay vì bị ép theo thứ tự khai báo. Linter (clippy) không yêu cầu giữ thứ tự — đây là quyết định style của team.
Tất Cả Field Bắt Buộc
Quy tắc cứng: trong struct expression, mọi field được khai báo trong definition đều phải có giá trị. Thiếu một field — compile error E0063.
struct User {
name: String,
age: u32,
email: String,
}
fn main() {
// SAI: Thiếu field email
let u = User {
name: String::from("Canh"),
age: 30,
};
}
Compiler báo:
error[E0063]: missing field `email` in initializer of `User`
--> src/main.rs:8:13
|
8 | let u = User {
| ^^^^ missing `email`
Đây là quyết định thiết kế có chủ ý của Rust — đối lập với C/C++:
- C/C++: field không khởi tạo nhận giá trị mặc định tuỳ ngữ cảnh — zero cho integer ở scope static, nhưng không xác định (undefined) cho stack-allocated local. Rất dễ đọc rác hoặc gây undefined behavior.
- Rust: không có default value ngầm cho field struct. Buộc lập trình viên đưa ra quyết định khởi tạo — nếu thật sự muốn default, dùng
#[derive(Default)]và gọiUser::default()(sẽ học ở phần Trait), hoặc viết hàmnew()tự xác định default. - Lợi ích: không có "instance nửa vời" — mỗi instance compile được nghĩa là mọi field đã có giá trị hợp lệ. Đây là nền tảng cho rất nhiều safety guarantee về sau (Option, validation, ...).
Nếu một field thực sự "có thể vắng mặt", model nó bằng Option<T> rồi khởi tạo None: email: None. Đây là cách Rust ép bạn xử lý case "không có giá trị" tường minh ở mọi nơi đọc field, thay vì để giá trị rác trôi nổi.
Access Field instance.field
Sau khi tạo instance, dùng dot notation instance.field để truy cập từng field. Biểu thức u.name có type là type của field name (ở ví dụ trên là String), u.age có type u32.
struct User {
name: String,
age: u32,
email: String,
}
fn main() {
let u = User {
name: String::from("Canh"),
age: 30,
email: "[email protected]".into(),
};
// u.name: &String khi dùng trong context cần reference;
// ở println! qua trait Display, compiler tự lấy reference.
println!("name: {}", u.name);
// u.age: u32 — primitive Copy, dùng thoải mái
let next_age: u32 = u.age + 1;
println!("sang năm: {next_age}");
// Có thể truyền &u.name vào function nhận &str (qua auto-deref String → str)
print_name(&u.name);
}
fn print_name(name: &str) {
println!("hello, {name}");
}
Vài nguyên tắc đáng nhớ:
- Dot notation là place expression — không chỉ là "đọc giá trị" mà là "địa chỉ của field". Vì thế
u.age = 31;hợp lệ (nếuulà mut), và&u.agecho ra reference tới chính ô nhớ của field, không phải copy. - Auto-borrow trong method call: khi gọi method nhận
&self, viếtu.method()compiler tự suy ra(&u).method(). Sẽ học chi tiết ở Bài 88 (impl block). - Access field copy vs move: với field Copy (như
u32),let a = u.age;là copy —u.agevẫn dùng được sau đó. Với field non-Copy (String),let n = u.name;là partial move (đã học ở Bài 66) —u.namechết, các field khác vẫn sống.
Modify Field — Cần let mut
Muốn sửa field, instance phải được khai báo let mut. Khi đó mọi field đều có thể modify — Rust không có khái niệm "field này mut, field kia không". Mutability là thuộc tính của binding, không phải của từng field trong struct.
struct User {
name: String,
age: u32,
email: String,
}
fn main() {
let mut u = User {
name: String::from("Canh"),
age: 30,
email: "[email protected]".into(),
};
// OK: Sửa từng field — instance đã mut
u.age = 31;
u.email = "[email protected]".into();
u.name.push_str(" Nguyen"); // String có method mutate in-place
println!("{} ({} tuổi) - {}", u.name, u.age, u.email);
// Nếu khai báo là `let u = ...` (không mut):
// u.age = 31; // ERROR E0594: cannot assign to `u.age`, as `u` is not declared as mutable
}
Khác biệt với Java/C++ final/const:
- Java / C++: có thể khai báo từng field
final/constriêng — một số field readonly, một số mutable. Granularity ở mức field. - Rust: granularity ở mức binding. Một là
let u(immutable, không sửa field nào), hai làlet mut u(mutable, sửa field nào cũng được). Không có giữa. - Tại sao thiết kế vậy? Đơn giản và nhất quán: chỉ một quy tắc cho mọi binding. Nếu thật sự cần "một số field mutable, một số không", design lại bằng cách tách struct (struct con immutable + struct chứa mut), hoặc dùng interior mutability (
Cell/RefCell— học sau).
Lưu ý: mut ở binding khác hẳn mut ở reference (&mut T) và mut ở field tương ứng. Khi binding mut, bạn được phép tạo &mut u hoặc &mut u.field — đó là điều mở khoá khả năng modify, không phải bản thân từ khoá mut tự "biến" field thành mutable.
Nested Struct
Struct có thể chứa field là struct khác. Khi khởi tạo, struct con phải được tạo tường minh bằng struct expression riêng — Rust không tự "init" struct con thay bạn.
struct Address {
street: String,
city: String,
country: String,
}
struct User {
name: String,
age: u32,
addr: Address,
}
fn main() {
let u = User {
name: String::from("Canh"),
age: 30,
addr: Address {
street: "123 Tran Hung Dao".into(),
city: "Hanoi".into(),
country: "Vietnam".into(),
},
};
// Access field lồng nhau: chain dot notation
println!("{} sống ở {}, {}", u.name, u.addr.city, u.addr.country);
}
Một số ghi chú:
- Init phải đầy đủ ở mọi cấp:
addrlà field bắt buộc củaUser, và bên trongAddresscũng có 3 field bắt buộc — cả 3 phải có giá trị, không thiếu được. Quy tắc E0063 áp dụng cho từng level. - Access chain:
u.addr.cityđọc qua 2 cấp dot notation, mỗi cấp vẫn là place expression — vẫn có thể assign nếu instance gốc là mut:u.addr.city = "Saigon".into();(chỉ cầnlet mut u, không cần đánh dấu gì riêng choaddr). - Memory layout:
Addressđược nhúng trực tiếp vàoUser(không phải reference). Size củaUser= size các fieldname+age+addr. Nếu muốn shareAddressgiữa nhiềuUser, dùngRc<Address>hoặc reference. - Helper function: khi nested sâu, viết hàm
fn new_address(...) -> Addressrồi gọiaddr: new_address(...)giúp call site sạch. Đây là tiền đề cho idiom::newở Bài 89.
Struct Trên Heap Với Box
Mặc định let u = User { ... } đặt instance trên stack. Với struct nhỏ (vài chục đến vài trăm byte), điều này tốt: alloc nhanh, locality cao. Nhưng với struct rất lớn (chứa array cố định lớn, hoặc nhiều field), stack có thể bị overflow — nhất là khi gọi function deep, hoặc trong thread phụ với stack mặc định nhỏ (8 MB trên main thread, có thể chỉ 2 MB ở thread phụ).
Giải pháp: bọc trong Box<T> để alloc trên heap, stack chỉ giữ pointer (8 byte trên hệ 64-bit).
struct BigUser {
name: String,
age: u32,
// Giả lập field rất lớn — buffer cố định 1 MB trên stack
buffer: [u8; 1024 * 1024],
}
fn main() {
// SAI: Có thể stack overflow nếu chạy trong thread phụ stack nhỏ
// let u = BigUser { name: "Canh".into(), age: 30, buffer: [0; 1024 * 1024] };
// OK: Đẩy lên heap — stack chỉ giữ pointer 8 byte
let u: Box<BigUser> = Box::new(BigUser {
name: "Canh".into(),
age: 30,
buffer: [0; 1024 * 1024],
});
// Access vẫn dùng dot notation — Box deref tự động
println!("name = {}, age = {}, buf[0] = {}", u.name, u.age, u.buffer[0]);
}
Vài điểm cần nhớ:
Box::new(expr)nhận một expression và move nó lên heap. Trên hệ thống không có heap cấu hình đặc biệt, đây là cách phổ biến nhất để alloc heap cho dữ liệu owned.- Auto deref:
Box<User>tự deref vềUserkhi dùng dot notation — không cần viết(*u).name, viếtu.namelà đủ. - Khi nào dùng Box: (a) struct lớn ngẫu nhiên có thể vượt vài KB; (b) struct tự tham chiếu (recursive type — sẽ học); (c) muốn polymorphism qua trait object
Box<dyn Trait>. - Khi nào KHÔNG cần Box: struct nhỏ (< 1 KB), dùng trong hot path, hoặc cần nhiều instance liên tiếp trong
Vec<User>(vìVecđã alloc heap cho phần tử, không cần Box ngoài). - Cost:
Box::newtốn 1 allocator call (chậm hơn stack alloc), drop tốn 1 deallocator call. Với struct nhỏ, overhead này có thể đáng kể trong loop — đo benchmark trước khi tối ưu.
Common Pitfall
Pitfall 1: Thiếu field — E0063
Đã nhắc ở mục 4. Hay xảy ra khi thêm field mới vào struct definition mà quên cập nhật tất cả callsite tạo instance. Compiler chỉ đúng chỗ và liệt kê field thiếu:
error[E0063]: missing field `email` in initializer of `User`
Fix: thêm field thiếu, hoặc dùng ..Default::default() nếu struct có #[derive(Default)], hoặc dùng struct update syntax ..other với một instance khác (sẽ học ở Bài 85).
Pitfall 2: Type mismatch field — E0308
Đưa value sai type vào field. Ví dụ field age: u32 nhưng truyền số có dấu hoặc string:
error[E0308]: mismatched types
--> src/main.rs:5:14
|
5 | age: -1,
| ^^ expected `u32`, found integer
|
= note: `-1` is a negative integer, but `u32` is unsigned
Fix: ép đúng type (30_u32, String::from(...), "...".into(), hoặc cast tường minh x as u32 với cảnh báo về truncation).
Pitfall 3: Ownership move khi assign instance
Vì struct chứa field non-Copy (như String) là không Copy mặc định, gán let u2 = u1; sẽ move toàn bộ ownership từ u1 sang u2 — sau đó u1 không dùng được:
struct User { name: String, age: u32 }
fn main() {
let u1 = User { name: "Canh".into(), age: 30 };
let u2 = u1; // move u1 → u2
println!("{} {}", u2.name, u2.age);
// SAI: println!("{}", u1.name);
// error[E0382]: borrow of moved value: `u1`
}
Fix: hoặc dùng let u2 = u1.clone(); (cần #[derive(Clone)] trên struct, mọi field cũng phải Clone), hoặc truyền reference let u2 = &u1; nếu chỉ cần view, hoặc design lại để tránh giữ 2 owner.
Đáng lưu ý: struct mà tất cả field đều Copy (vd struct Point { x: i32, y: i32 }) có thể #[derive(Copy, Clone)] để gán không move — học chi tiết ở phần Trait. Còn struct chứa String, Vec, Box thì không thể Copy được, luôn phải tính tới move/clone/borrow.
Tổng Kết
- Khởi tạo instance bằng struct expression:
User { field1: value1, field2: value2, ... }— là một biểu thức trả về instance, có thể bind, return, hoặc làm argument. - Field order trong block khởi tạo không quan trọng — Rust match theo tên field. Refactor đổi vị trí field trong definition không phá callsite.
- Mọi field bắt buộc — không có default value ngầm. Thiếu một field là compile error
E0063 "missing fields". Muốn default, dùng#[derive(Default)]hoặc viết constructornew(). - Access field qua dot notation
instance.field— kết quả là place expression với type chính xác là type field; có thể đọc, có thể lấy reference, có thể assign (nếu instance là mut). - Modify field cần
let mut instance. Mutability gắn với binding, không gắn với field — không có "partial mut". Một là cả instance mut, hai là cả instance immutable. - Nested struct: struct con phải được khởi tạo tường minh bằng struct expression riêng. Access chain dot notation (
u.addr.city) hoạt động ở mọi cấp. - Struct lớn nên đẩy lên heap bằng
Box::new(User { ... })để tránh stack overflow; auto deref cho phép dùng dot notation như instance thường. - 3 pitfall:
E0063thiếu field,E0308sai type ở value field, và ownership move khilet u2 = u1;với struct chứa field non-Copy (dùng.clone()hoặc reference để fix).
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Cho struct
Point { x: i32, y: i32, label: String }. Viết 2 cách khởi tạo instance vớix = 3,y = 4,label = "origin"— một cách theo thứ tự định nghĩa, một cách đảo thứ tự. - Compile error
E0063 "missing field `email` in initializer of `User`"nghĩa là gì? 2 cách fix phổ biến nhất là gì? - Bạn muốn sửa
u.agenhưng compiler báo"cannot assign to `u.age`, as `u` is not declared as mutable". Cách fix? Tại sao Rust không cho khai báo riêng một field là mutable trong khi field khác immutable? - Với nested struct
User { addr: Address, ... }vàAddress { city: String, ... }, viết biểu thức accesscitycủa useru. Để sửa đượcu.addr.city,uphải khai báo thế nào? - Khi nào nên đẩy struct lên heap bằng
Box<T>? Nêu 2 trường hợp nên dùng và 1 trường hợp không nên dùng. - Sau
let u1 = User { name: "A".into(), age: 30 }; let u2 = u1;— biểu thức nào compile được: (a)println!("{}", u2.name);, (b)println!("{}", u1.name);, (c)let r = &u1;? Nếu muốn cảu1vàu2cùng dùng được, sửa thế nào?
Đáp án
- Cách 1 theo thứ tự:
let p = Point { x: 3, y: 4, label: "origin".into() };. Cách 2 đảo thứ tự:let p = Point { label: "origin".into(), y: 4, x: 3 };. Cả hai compile như nhau — Rust match theo tên field. - Compiler báo struct expression thiếu field
email— không thể tạo instance "nửa vời". 2 cách fix: (a) thêm fieldemail: ...với giá trị cụ thể; (b) nếu struct có#[derive(Default)], dùng..Default::default()cuối block để lấy default cho các field không liệt kê, hoặc dùng struct update syntax..other(Bài 85) với một instance khác làm fallback. - Fix: đổi
let u = ...thànhlet mut u = .... Rust không cho per-field mutability vì mutability là thuộc tính của binding, không phải của struct definition — giúp quy tắc đơn giản, nhất quán, dễ kiểm tra borrow checker. Nếu thật sự cần "một số field mutable, một số không", design lại: tách struct con immutable + struct chứa mut, hoặc dùng interior mutability (Cell/RefCell). - Đọc:
u.addr.city— chain 2 cấp dot notation. Để sửau.addr.city = "Saigon".into();, chỉ cầnlet mut u = ...ở top level — không cần đánh dấu gì riêng choaddr. Mutability lan toả qua mọi cấp field khi binding gốc là mut. - Nên dùng
Box: (1) struct lớn ngẫu nhiên (vài KB trở lên), nhất là chạy trong thread phụ stack nhỏ; (2) struct tự tham chiếu (recursive type — vdstruct Node { next: Option<Box<Node>> }); (3) muốn polymorphism qua trait objectBox<dyn Trait>. KHÔNG nên dùng: struct nhỏ (< 1 KB) trong hot path — overhead alloc/dealloc heap đáng kể so với stack; cũng không cần Box cho phần tử trongVec<User>vì Vec đã alloc heap rồi. - (a) Có —
u2là owner mới, dùng bình thường. (b) Không —u1đã bị move sangu2, compiler báoE0382 "borrow of moved value: u1". (c) Không — không thể borrow value đã moved. Fix: hoặclet u2 = u1.clone();(cần#[derive(Clone)]trên struct), hoặclet u2 = &u1;nếu chỉ cần view, hoặclet u2 = u1.clone();rồi cả hai cùng own bản sao riêng.
Bài Tiếp Theo
Bài 84: Field Init Shorthand — khi tên biến trùng tên field, có thể viết User { name, age } thay vì User { name: name, age: age }. Đây là idiom cực phổ biến trong constructor fn new(name: String, age: u32) -> Self { Self { name, age } }, giúp giảm boilerplate khi struct có nhiều field. Bài tiếp theo cũng giới thiệu rule khi trộn shorthand với field thường (Self { name, age, email: default_email() }) và lưu ý khi tên biến không khớp.
