Danh sách bài viết

Bài 83: Khởi Tạo Struct Instance

Bài 83 của series Rust Cơ Bản — hướng dẫn khởi tạo struct instance trong Rust 2024. Cú pháp Rust dùng block User { name: ..., age: ..., email: ... } — trông giống object literal trong JS hay struct literal trong C, nhưng có vài quy tắc nghiêm khắc hơn: field order trong block không bắt buộc khớp với thứ tự định nghĩa (Rust match theo tên), mọi field phải có giá trị (không có default value ngầm như C++), modify field yêu cầu instance được khai báo let mut — và mut áp dụng cho toàn bộ instance chứ không phải từng field. Bài này còn cover nested struct (struct chứa struct khác), cách đẩy struct lớn lên heap qua Box<User> để tránh stack overflow, và 3 lỗi compile thường gặp nhất khi mới làm quen: E0063 (thiếu field), E0308 (sai type field), và ownership move khi assign cả instance.

09/06/2026
10 phút đọc
1 lượt xem
1

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++ (= 0 cho 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: E0063 thiếu field, E0308 sai type ở value field, và ownership move khi let 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.

2

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ẳng fn 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 trait Into<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).

3

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ì 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.

4

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ọi User::default() (sẽ học ở phần Trait), hoặc viết hàm new() 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.

5

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ếu u là mut), và &u.age cho 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ết u.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.age vẫn dùng được sau đó. Với field non-Copy (String), let n = u.name;partial move (đã học ở Bài 66) — u.name chết, các field khác vẫn sống.
6

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/const riê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.

7

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: addr là field bắt buộc của User, và bên trong Address cũ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ần let mut u, không cần đánh dấu gì riêng cho addr).
  • Memory layout: Address được nhúng trực tiếp vào User (không phải reference). Size của User = size các field name + age + addr. Nếu muốn share Address giữa nhiều User, dùng Rc<Address> hoặc reference.
  • Helper function: khi nested sâu, viết hàm fn new_address(...) -> Address rồi gọi addr: new_address(...) giúp call site sạch. Đây là tiền đề cho idiom ::new ở Bài 89.
8

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ề User khi dùng dot notation — không cần viết (*u).name, viết u.name là đủ.
  • 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::new tố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.
9

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.

10

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 constructor new().
  • 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: E0063 thiếu field, E0308 sai type ở value field, và ownership move khi let u2 = u1; với struct chứa field non-Copy (dùng .clone() hoặc reference để fix).
11

Bài Tập Củng Cố

Tự trả lời, đáp án ở cuối:

  1. Cho struct Point { x: i32, y: i32, label: String }. Viết 2 cách khởi tạo instance với x = 3, y = 4, label = "origin" — một cách theo thứ tự định nghĩa, một cách đảo thứ tự.
  2. 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ì?
  3. Bạn muốn sửa u.age như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?
  4. Với nested struct User { addr: Address, ... }Address { city: String, ... }, viết biểu thức access city của user u. Để sửa được u.addr.city, u phải khai báo thế nào?
  5. 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.
  6. 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ả u1u2 cùng dùng được, sửa thế nào?
Đáp án
  1. 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.
  2. 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 field email: ... 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.
  3. Fix: đổi let u = ... thành let 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).
  4. Đọc: u.addr.city — chain 2 cấp dot notation. Để sửa u.addr.city = "Saigon".into();, chỉ cần let mut u = ... ở top level — không cần đánh dấu gì riêng cho addr. Mutability lan toả qua mọi cấp field khi binding gốc là mut.
  5. 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 — vd struct Node { next: Option<Box<Node>> }); (3) muốn polymorphism qua trait object Box<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ử trong Vec<User> vì Vec đã alloc heap rồi.
  6. (a) u2 là owner mới, dùng bình thường. (b) Khôngu1 đã bị move sang u2, compiler báo E0382 "borrow of moved value: u1". (c) Không — không thể borrow value đã moved. Fix: hoặc let u2 = u1.clone(); (cần #[derive(Clone)] trên struct), hoặc let u2 = &u1; nếu chỉ cần view, hoặc let u2 = u1.clone(); rồi cả hai cùng own bản sao riêng.
12

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.