Danh sách bài viết

Bài 89: Associated Function — Self::new()

Bài 89 của series Rust Cơ Bản — khép lại Nhóm 12 với dạng còn lại trong impl block: associated function — hàm không nhận self, gọi qua Type::function(). Đây chính là chỗ để định nghĩa constructor trong Rust: idiom là fn new(...) -> Self, gọi User::new(...). Bài này phân tích cú pháp, vai trò của kiểu trả về Self (alias cho impl type, refactor-friendly), idiom multiple constructor thay cho overload (from_string, with_age, default), trait Default chuẩn hoá ::default(), conversion constructor (from_csv, from_json) và pattern builder cho struct nhiều field optional, cuối cùng là quy tắc khi nào dùng associated function vs method.

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

Mục Tiêu Bài Học

Sau bài học, bạn sẽ:

  • Phân biệt được associated function (không nhận self, gọi qua Type::fn()) với method (nhận self, gọi qua instance.fn()) — đều nằm trong impl block.
  • Liên hệ được với static method của Java/C++ và class method của Python: bản chất là hàm gắn với type chứ không gắn với instance.
  • Viết được idiom Rust cho constructor: impl User { fn new(...) -> Self { Self { ... } } }, gọi User::new(...).
  • Hiểu vai trò của Self trong return type và body: là alias cho kiểu mà impl đang implement; ngắn gọn hơn lặp tên struct và an toàn khi đổi tên struct.
  • Áp dụng idiom multiple constructor: thay vì overload (Rust không hỗ trợ), viết nhiều associated function có tên ý nghĩa như from_string, with_age, default, from_csv.
  • Implement trait Default để có User::default(); biết khi nào derive #[derive(Default)] được và khi nào phải tự impl.
  • Đọc hiểu pattern builder: associated function User::builder() trả về UserBuilder, chain .name().age(), kết bằng .build() -> User — dùng cho struct nhiều field optional.
  • Có quy tắc rõ để chọn: associated function cho constructor / utility / factory; method cho operation trên instance đã tồn tại.
2

Associated Function Là Gì

Trong Bài 88 bạn đã viết method — hàm trong impl block có first parameterself, &self hoặc &mut self. Associated function là tên gọi chính thức của Rust cho dạng còn lại: hàm trong impl block không nhận self. Hệ quả: bạn gọi nó qua Type::function(), không qua instance.function().

struct User {
    name: String,
    age: u32,
}

impl User {
    // Associated function: KHÔNG có self - thuộc về kiểu User
    fn new(name: String, age: u32) -> User {
        User { name, age }
    }

    // Method: có &self - thuộc về một instance User cụ thể
    fn greet(&self) {
        println!("Hi, I am {}", self.name);
    }
}

fn main() {
    // Associated function gọi qua Type::, dấu ::
    let u = User::new(String::from("Canh"), 30);

    // Method gọi qua instance., dấu .
    u.greet();
}

Hai cú pháp gọi khác nhau là điều dễ nhớ nhất: :: cho associated function, . cho method. Khái niệm tương đương ở các ngôn ngữ khác:

  • Java / C++: static method — gắn với class, không cần instance. User.create() trong Java tương ứng User::new() ở Rust.
  • Python: classmethod / staticmethodUser.from_dict(d) trong Python tương ứng User::from_dict(d) ở Rust.
  • JavaScript / TypeScript: static method trong class — User.create() tương ứng.

Khác biệt quan trọng: Rust không có khái niệm "constructor đặc biệt" như Java (new User(...)) hay C++ (User(...)). Mọi constructor đều chỉ là một associated function bình thường bạn tự viết — Rust không "ưu ái" tên nào cả. Tên new phổ biến đến mức được coi là convention của community, nhưng compiler không bắt buộc.

Một associated function có thể nhận 0, 1 hay nhiều parameter; có thể trả về Self (constructor), hoặc trả về bất kỳ kiểu nào khác (utility function gắn với type). Bài này tập trung vào dạng phổ biến nhất: associated function dùng làm constructor — trả về Self.

3

Constructor Pattern — new()

Idiom Rust: nếu struct của bạn cần một constructor duy nhất (hoặc một "default constructor"), đặt tên là new. Khi đọc code Rust ở bất cứ crate nào — std, tokio, serde, axum — bạn sẽ gặp Type::new(...) ở khắp nơi. Đây là convention mạnh đến mức người dùng crate kỳ vọng mọi struct phi-trivial đều có ::new().

struct User {
    name: String,
    age: u32,
}

impl User {
    // Constructor mặc định: chỉ nhận name, age tự set 0
    fn new(name: String) -> Self {
        Self {
            name,
            age: 0,
        }
    }
}

fn main() {
    // Cách gọi idiomatic: User::new với arg
    let u = User::new("Canh".into());
    println!("{} - {}", u.name, u.age);   // Canh - 0
}

Vài chi tiết đáng nhớ:

  • Return type Self: trong impl User, Self = User. Có thể viết -> User nhưng -> Self idiomatic hơn (mục 4).
  • Body trả về Self { ... }: cùng lý do — Self { name, age: 0 } đọc rõ hơn User { name, age: 0 } và không phải sửa nếu sau này đổi tên User thành Account.
  • Field init shorthand (Bài 84) thường đi kèm: tham số name: String trùng tên field nên viết gọn Self { name, age: 0 }.
  • Không có visibility modifier ở đây — bài này không bàn pub. Trong module thực, bạn sẽ thấy pub fn new(...) để cho phép caller ngoài module gọi.

new trong Rust không phải keyword, không phải method magic, không được compiler đối xử đặc biệt. Nó chỉ là một hàm bạn tự đặt tên — community chọn tên new để mọi user của crate biết "đây là điểm vào tạo instance". Tên khác (create, build, make) hợp lệ về kỹ thuật nhưng đi ngược convention.

4

Self vs User Trong Return Type

Self (viết hoa, type) là type alias tự động cho kiểu mà impl block đang implement. Bên trong impl User { ... }, Self đồng nghĩa với User ở mọi vị trí dùng type. Hai phiên bản dưới biên dịch như nhau:

struct User {
    name: String,
    age: u32,
}

// Phiên bản A: dùng tên User
impl User {
    fn new_a(name: String, age: u32) -> User {
        User { name, age }
    }
}

// Phiên bản B: dùng Self - idiomatic
impl User {
    fn new_b(name: String, age: u32) -> Self {
        Self { name, age }
    }
}

fn main() {
    let u1 = User::new_a("An".into(), 30);
    let u2 = User::new_b("Binh".into(), 25);
    println!("{} {} | {} {}", u1.name, u1.age, u2.name, u2.age);
}

Tại sao community prefer Self?

  • Ngắn hơn khi tên struct dài: HttpRequestBuilder::new() -> Self đọc dễ hơn -> HttpRequestBuilder.
  • Refactor-friendly: đổi tên struct (UserAccount) chỉ cần sửa khai báo struct và mỗi impl Userimpl Account; body và signature dùng Self không cần sửa.
  • Generic-friendly: với impl<T> Container<T> { ... }, viết Self tự động giữ generic Container<T>; viết -> Container<T> dễ sai khi thêm/bớt type param.
  • Trait-friendly: khi sau này impl trait có -> Self trong trait definition (vd Clone::clone(&self) -> Self), bạn buộc dùng Self trong impl để khớp signature.

Quy tắc thực hành: trong impl block của bạn, mặc định dùng Self cho mọi chỗ cần tham chiếu chính kiểu đang impl — return type, body khởi tạo, parameter type. Chỉ dùng tên struct cụ thể khi ngoài impl block (vd ở free function: fn make_user(...) -> User).

Lưu ý cú pháp: Self viết hoa là type; self viết thường là parameter đại diện instance hiện tại (Bài 88). Hai cái khác hoàn toàn — đừng lẫn.

5

Multiple Constructor Thay Cho Overload

Rust không hỗ trợ function overload. Bạn không thể có hai hàm cùng tên new nhưng số lượng / kiểu parameter khác nhau như Java hay C++. Thay vào đó, idiom Rust là viết nhiều associated function có tên ý nghĩa, mỗi tên ứng với một cách tạo instance:

struct User {
    name: String,
    age: u32,
}

impl User {
    // Cách 1: constructor cơ bản
    fn new(name: String, age: u32) -> Self {
        Self { name, age }
    }

    // Cách 2: tạo từ &str (tiện cho literal "An")
    fn from_string(s: &str) -> Self {
        Self {
            name: s.to_string(),
            age: 0,
        }
    }

    // Cách 3: chỉ cần name, age set default
    fn with_age(name: String, age: u32) -> Self {
        Self { name, age }
    }

    // Cách 4: instance mặc định, không cần arg
    fn default() -> Self {
        Self {
            name: String::from("anonymous"),
            age: 0,
        }
    }
}

fn main() {
    let u1 = User::new("An".into(), 30);
    let u2 = User::from_string("Binh");
    let u3 = User::with_age("Cuong".into(), 25);
    let u4 = User::default();

    for u in [&u1, &u2, &u3, &u4] {
        println!("{} - {}", u.name, u.age);
    }
}

Cách tiếp cận này có vài ưu điểm so với overload:

  • Tên nói rõ ý đồ: đọc User::from_string("An") hiểu ngay là tạo từ chuỗi, không phải đoán xem version overload nào được gọi.
  • Không lỗi resolution mơ hồ: ngôn ngữ có overload thường gặp lỗi "ambiguous overload" khi compiler không quyết được; Rust không có vấn đề này vì tên hàm là duy nhất.
  • IDE / docs rõ ràng: hover thấy đúng signature, không phải scroll qua N variant của new.
  • Mở rộng dễ: thêm cách tạo mới chỉ là thêm function mới, không động tới function cũ.

Convention đặt tên trong community Rust:

  • new: constructor "mặc định" của struct, nhận ít nhất tham số tối thiểu để tạo instance valid.
  • with_X: constructor nhận thêm option X ngoài tham số chuẩn (vd with_capacity, with_timeout).
  • from_X: tạo từ format / kiểu khác — kết hợp tốt với trait From (mục 7).
  • default: instance mặc định không cần arg — thường được chuẩn hoá qua trait Default (mục 6).
  • builder: trả về một builder để chain config — dùng khi nhiều field optional (mục 8).
6

Trait Default#[derive(Default)]

Ở mục 5 bạn thấy associated function default(). Std cung cấp một trait chuẩn cho ý tưởng đó: std::default::Default. Impl trait này cho struct của bạn để mọi caller có thể viết User::default() theo cách thống nhất với toàn ecosystem Rust (vd nhiều generic function của std nhận type ràng buộc T: Default).

struct User {
    name: String,
    age: u32,
}

impl Default for User {
    fn default() -> Self {
        Self {
            name: String::new(),
            age: 0,
        }
    }
}

fn main() {
    // Gọi qua associated function path
    let u1 = User::default();

    // Hoặc qua type annotation - compiler suy ra
    let u2: User = Default::default();

    println!("{:?} {} | {:?} {}", u1.name, u1.age, u2.name, u2.age);
}

Nếu mọi field trong struct đều đã có impl Default sẵn (mọi primitive numeric mặc định 0, bool mặc định false, String mặc định rỗng, Option<T> mặc định None, Vec<T> mặc định rỗng...), bạn có thể bỏ hẳn impl tay và dùng derive:

#[derive(Default, Debug)]
struct ServerConfig {
    host: String,        // default = ""
    port: u16,           // default = 0
    workers: u32,        // default = 0
    tls_enabled: bool,   // default = false
}

fn main() {
    let cfg = ServerConfig::default();
    println!("{cfg:?}");
    // ServerConfig { host: "", port: 0, workers: 0, tls_enabled: false }
}

Khi nào derive không đủ và phải impl tay:

  • Field có kiểu không impl Default sẵn (vd kiểu của crate khác chưa derive).
  • Default mặc định không hợp lý cho domain — vd port: 0 vô nghĩa, bạn muốn 8080; workers: 0 vô nghĩa, bạn muốn 4.
  • Cần logic khởi tạo (vd created_at: Instant::now()) — derive không cho phép code tuỳ ý.

Pattern phối hợp thường thấy: derive Default cho struct, sau đó viết thêm fn new(...) với tham số bắt buộc và dùng ..Default::default() ở struct update syntax (Bài 85) để rút gọn:

#[derive(Default, Debug)]
struct ServerConfig {
    host: String,
    port: u16,
    workers: u32,
    tls: bool,
}

impl ServerConfig {
    fn new(host: String, port: u16) -> Self {
        Self {
            host,
            port,
            ..Self::default()    // các field còn lại = default
        }
    }
}
7

Conversion Constructor: from_csv, from_json

Khi cần tạo instance từ một format khác (CSV row, JSON string, byte buffer...), idiom là viết associated function tên bắt đầu bằng from_:

use std::fmt;

struct User {
    name: String,
    age: u32,
}

impl User {
    // Convert từ CSV row "name,age"
    fn from_csv(line: &str) -> Self {
        let parts: Vec<&str> = line.split(',').collect();
        let name = parts.get(0).unwrap_or(&"").to_string();
        let age: u32 = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
        Self { name, age }
    }

    // Convert từ JSON pseudo: ở đây minh hoạ đơn giản,
    // thực tế dùng serde::from_str cho production
    fn from_json(raw: &str) -> Self {
        // raw dạng: {"name":"An","age":30}
        let trimmed = raw.trim_matches(|c| c == '{' || c == '}');
        let mut name = String::new();
        let mut age = 0u32;
        for kv in trimmed.split(',') {
            let mut it = kv.splitn(2, ':');
            let k = it.next().unwrap_or("").trim_matches(|c: char| c == '"' || c.is_whitespace());
            let v = it.next().unwrap_or("").trim_matches(|c: char| c == '"' || c.is_whitespace());
            match k {
                "name" => name = v.to_string(),
                "age" => age = v.parse().unwrap_or(0),
                _ => {}
            }
        }
        Self { name, age }
    }
}

// Bonus: impl Display thay cho method print_self
impl fmt::Display for User {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} ({})", self.name, self.age)
    }
}

fn main() {
    let u1 = User::from_csv("An,30");
    let u2 = User::from_json(r#"{"name":"Binh","age":25}"#);

    // Display impl cho phép {} trực tiếp
    println!("{u1}");   // An (30)
    println!("{u2}");   // Binh (25)
}

Tại sao prefix from_?

  • Đọc tự nhiên: User::from_csv(line) = "User tạo từ CSV line".
  • Cặp đôi với trait From: nếu conversion luôn thành công, có thể tiến lên một bước impl From<T> for User và caller dùng User::from(value) hoặc value.into(). Nếu có thể fail, dùng TryFrom<T> trả về Result. Hai trait này sẽ học chi tiết ở nhóm trait sau.
  • Quen thuộc với std: String::from(&str), Vec::from(&[T]), PathBuf::from(&str) — toàn dạng Type::from_X.

Ví dụ ở trên parse CSV / JSON thô và rơi về giá trị default khi sai — không lý tưởng cho production. Khi học error handling (Nhóm Result), bạn sẽ refactor về fn from_csv(line: &str) -> Result<Self, ParseError> để báo lỗi rõ ràng.

8

Use Case: Builder Pattern

Khi struct có nhiều field optional, new(...) với chục tham số trở nên khó dùng — đọc User::new("An", 30, None, None, true, None, ...) rất rối. Idiom Rust cho trường hợp này là builder pattern: associated function ::builder() trả về một struct phụ XxxBuilder, chain các method set field, kết bằng .build().

struct User {
    name: String,
    age: u32,
    email: Option<String>,
    active: bool,
}

#[derive(Default)]
struct UserBuilder {
    name: String,
    age: u32,
    email: Option<String>,
    active: bool,
}

impl User {
    // Associated function trả về builder rỗng
    fn builder() -> UserBuilder {
        UserBuilder::default()
    }
}

impl UserBuilder {
    fn name(mut self, name: &str) -> Self {
        self.name = name.to_string();
        self
    }

    fn age(mut self, age: u32) -> Self {
        self.age = age;
        self
    }

    fn email(mut self, email: &str) -> Self {
        self.email = Some(email.to_string());
        self
    }

    fn active(mut self, active: bool) -> Self {
        self.active = active;
        self
    }

    // build() chốt việc tạo User cuối cùng
    fn build(self) -> User {
        User {
            name: self.name,
            age: self.age,
            email: self.email,
            active: self.active,
        }
    }
}

fn main() {
    let u = User::builder()
        .name("Canh")
        .age(30)
        .email("[email protected]")
        .active(true)
        .build();

    println!("{} {} {:?} {}", u.name, u.age, u.email, u.active);
}

Quan sát:

  • Vào builder: User::builder() là associated function trả về UserBuilder — không nhận self vì lúc đó chưa có instance nào.
  • Method chain: mỗi setter fn name(mut self, ...) -> Self nhận self bằng giá trị (consume builder), set field, trả lại builder để chain tiếp. Pattern này gọi là consuming builder.
  • Kết thúc bằng .build(): consume builder lần cuối và trả ra User hoàn chỉnh.
  • Optional field tự nhiên: caller có thể bỏ qua .email() hoặc .active(), vẫn ra User hợp lệ với giá trị default.

Builder phổ biến trong các crate lớn: reqwest::Client::builder(), tokio::runtime::Builder::new_multi_thread(), axum::Router::new()...route()...layer(). Có crate derive_builder tự sinh boilerplate này từ macro — khi quen tay viết tay bạn có thể chuyển sang dùng macro để tiết kiệm code.

9

Khi Nào Dùng Associated Function vs Method

Cả hai đều nằm trong impl block và có thể có cùng tên gọi từ phía code, nhưng dùng khi nào khác hẳn nhau.

Dùng associated function khi:

  • Constructor: tạo instance mới (new, with_X, from_X, default, builder). Đây là use case dominate — chiếm phần lớn associated function trong code base Rust.
  • Factory: tạo instance theo logic phức tạp (đọc config, query DB, parse arg) — chưa có instance nào ở thời điểm gọi.
  • Utility gắn với type: hàm logic chỉ làm việc với type chứ không cần instance, vd i32::from_str_radix("ff", 16), u32::MAX, PathBuf::from(...).
  • Conversion từ format khác: from_csv, from_json, from_bytes.
  • Constants / associated values (sẽ học sau): impl Color { const RED: Color = ...; }.

Dùng method khi:

  • Operation trên instance đã tồn tại: đọc field (&self), thay đổi field (&mut self), consume instance (self).
  • Behavior phụ thuộc state hiện tại của instance: user.is_admin(), order.total_price(), conn.is_open().
  • Fluent / chain API: builder setters (mục 8) là method nhận self để chain.
  • Implement trait method: trait như Display, Clone, Iterator... gần như mọi method đều nhận self.

Quy tắc nhớ nhanh:

  • Nếu code bạn viết bắt đầu bằng "tạo một instance" → associated function (return Self).
  • Nếu code bắt đầu bằng "có sẵn instance, làm gì đó với nó" → method (nhận self).
  • Nếu hàm không dùng tới self ở body — đừng để nó là method. Loại bỏ self parameter, biến nó thành associated function. Caller chuyển từ u.foo() sang User::foo() — rõ ràng hơn về intent.
10

Tổng Kết

  • Associated function = hàm trong impl block không nhận self; gọi qua Type::function(), không qua instance.function(). Tương đương static method Java/C++ hay classmethod Python.
  • Constructor idiom: tên new, return -> Self, body Self { ... }. Đây là convention không phải keyword — caller mong đợi Type::new() ở mọi struct phi-trivial.
  • Self alias cho kiểu impl đang implement; ngắn hơn lặp tên struct, refactor-friendly khi đổi tên, generic-friendly khi nhiều type param, trait-friendly khi signature trait quy định.
  • Multiple constructor thay overload (Rust không hỗ trợ overload): viết nhiều associated function tên ý nghĩa (from_string, with_age, default, from_csv) — rõ intent hơn overload, không bị ambiguous resolution.
  • Trait Default chuẩn hoá ::default(), dùng được trong generic T: Default; impl tay khi cần logic riêng, hoặc #[derive(Default)] nếu mọi field đều Default.
  • Conversion constructor: from_X để tạo từ format khác, pair với trait From (infallible) hoặc TryFrom (fallible — trả Result).
  • Builder pattern: Type::builder() trả về TypeBuilder, chain setter .x().y(), kết bằng .build() -> Type. Idiom cho struct nhiều field optional.
  • Quy tắc chọn: associated function cho constructor / factory / utility không cần instance; method cho operation trên instance đã tồn tại. Hàm không dùng self trong body — biến thành associated function.

Khép lại Nhóm 12 (Bài 82-89), bạn đã có đủ công cụ để mô hình hoá entity domain (User, Order, Article...): định nghĩa shape, khởi tạo, cập nhật instance, viết constructor và behavior. Nhóm 13 chuyển sang công cụ mô hình hoá bổ sung quan trọng: enum — kiểu "một-trong-nhiều" của Rust.

11

Bài Tập Củng Cố

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

  1. Cho struct Article { title: String, body: String, views: u64, published: bool }. Viết associated function new(title: String, body: String) -> Self với views = 0, published = false. Dùng Self trong cả return type và body.
  2. Tại sao đoạn impl User { fn foo() -> User { ... } fn foo(name: String) -> User { ... } } không compile? Đề xuất 2 cách fix theo idiom Rust.
  3. So sánh User::new("An".into(), 30)u1.greet(): cú pháp gọi khác nhau ở đâu, và mỗi cú pháp tương ứng dạng hàm nào trong impl block?
  4. Cho struct Counter { value: u32 }. Khi nào nên derive #[derive(Default)] và khi nào nên impl Default tay? Cho ví dụ với value mặc định 0 (derive được) và value mặc định 100 (phải tay).
  5. Cho struct HttpRequest { url: String, method: String, headers: Vec<(String, String)>, body: Option<String>, timeout_ms: u64 }. Vì sao constructor new(url, method, headers, body, timeout_ms) không hợp lý? Đề xuất pattern thay thế và liệt kê các associated function / method cần viết.
  6. Hàm fn print_debug() { println!("debug"); } được đặt trong impl User. Đây là associated function hay method? Caller gọi nó thế nào? Có nên giữ nó trong impl User không, vì sao?
Đáp án
  1. impl Article {
        fn new(title: String, body: String) -> Self {
            Self { title, body, views: 0, published: false }
        }
    }
    Dùng Self ở cả return và body để refactor-friendly: đổi tên Article sau này chỉ cần sửa khai báo struct + dòng impl Article; body không phải đổi.
  2. Không compile vì Rust không hỗ trợ function overload — hai hàm cùng tên foo trong cùng impl là duplicate definition. Fix: (a) đặt tên khác nhau theo idiom: fn new() -> Userfn with_name(name: String) -> User. (b) gộp thành một hàm nhận Option<String>: fn new(name: Option<String>) -> User; hoặc dùng builder nếu nhiều biến thể.
  3. User::new(...) dùng :: — gọi associated function (không nhận self); u1.greet() dùng . — gọi method (nhận self). Quy tắc: :: đi với type, . đi với instance. Tương ứng: associated function gắn với type, method gắn với instance.
  4. Derive được khi mặc định mong muốn trùng với default của field: u32 default = 0, nên #[derive(Default)] struct Counter { value: u32 } sinh ra Counter::default() với value = 0 — đúng ý đồ. Impl tay khi cần giá trị khác default field:
    impl Default for Counter {
        fn default() -> Self { Self { value: 100 } }
    }
    Lý do: derive chỉ gọi Default::default() cho từng field, không cho phép tuỳ chỉnh giá trị khác.
  5. 5 tham số dài, đa số optional (headers, body, timeout_ms đều có thể bỏ qua) — caller phải nhớ thứ tự và truyền vec![] / None / số mặc định cho field không dùng, mất tự nhiên. Pattern thay thế: builder. Cần viết:
    • impl HttpRequest { fn builder() -> HttpRequestBuilder { ... } } — associated function vào builder.
    • struct HttpRequestBuilder { ... } + #[derive(Default)].
    • Setter method: fn url(mut self, u: &str) -> Self, fn method(...), fn header(...) (push từng header), fn body(...), fn timeout_ms(...).
    • Method kết: fn build(self) -> HttpRequest.
    Caller viết: HttpRequest::builder().url("...").method("GET").build().
  6. associated function — vì không có self trong parameter. Caller gọi User::print_debug(). Nên giữ trong impl User chỉ khi hàm có ý nghĩa gắn với type User (vd in debug info của domain User); nếu chỉ là utility chung không liên quan User, nên chuyển ra free function ngoài impl — đặt trong impl User sẽ gây hiểu nhầm là hàm thao tác trên User.
12

Bài Tiếp Theo

Bài 90: Định Nghĩa Enum Đơn Giản — sang công cụ mô hình hoá thứ hai sau struct: enum Direction { North, South, East, West }. Enum của Rust khác enum của C ở bản chất — không phải alias cho integer, mà là tagged union chính danh: mỗi variant có thể mang data riêng, type system bắt buộc cover tất cả variant khi pattern match. Bạn sẽ làm quen với cú pháp định nghĩa, cách truy cập variant qua Enum::Variant, và so sánh trực diện với enum C — bước đệm để vào Option<T>Result<T, E> ở các bài sau.

Bài này khép lại Nhóm 12 - Structs. Tiếp theo sang Nhóm 13 - Enums.