Mục lục
- Mục Tiêu Bài Học
- Associated Function Là Gì
- Constructor Pattern —
new() SelfvsUserTrong Return Type- Multiple Constructor Thay Cho Overload
- Trait
DefaultVà#[derive(Default)] - Conversion Constructor:
from_csv,from_json - Use Case: Builder Pattern
- Khi Nào Dùng Associated Function vs Method
- Tổng Kết
- Bài Tập Củng Cố
- Bài Tiếp Theo
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 quaType::fn()) với method (nhậnself, gọi quainstance.fn()) — đều nằm trongimplblock. - 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ọiUser::new(...). - Hiểu vai trò của
Selftrong 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.
Associated Function Là Gì
Trong Bài 88 bạn đã viết method — hàm trong impl block có first parameter là self, &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 ứngUser::new()ở Rust. - Python: classmethod / staticmethod —
User.from_dict(d)trong Python tương ứngUser::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.
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: trongimpl User,Self=User. Có thể viết-> Usernhưng-> Selfidiomatic hơn (mục 4). - Body trả về
Self { ... }: cùng lý do —Self { name, age: 0 }đọc rõ hơnUser { name, age: 0 }và không phải sửa nếu sau này đổi tênUserthànhAccount. - Field init shorthand (Bài 84) thường đi kèm: tham số
name: Stringtrùng tên field nên viết gọnSelf { 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ấypub 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.
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 (
User→Account) chỉ cần sửa khai báo struct và mỗiimpl User→impl Account; body và signature dùngSelfkhông cần sửa. - Generic-friendly: với
impl<T> Container<T> { ... }, viếtSelftự động giữ genericContainer<T>; viết-> Container<T>dễ sai khi thêm/bớt type param. - Trait-friendly: khi sau này impl trait có
-> Selftrong trait definition (vdClone::clone(&self) -> Self), bạn buộc dùngSelftrong 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.
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 optionXngoài tham số chuẩn (vdwith_capacity,with_timeout).from_X: tạo từ format / kiểu khác — kết hợp tốt với traitFrom(mục 7).default: instance mặc định không cần arg — thường được chuẩn hoá qua traitDefault(mục 6).builder: trả về một builder để chain config — dùng khi nhiều field optional (mục 8).
Trait Default Và #[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
Defaultsẵ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: 0vô nghĩa, bạn muốn8080;workers: 0vô nghĩa, bạn muốn4. - 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
}
}
}
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 implFrom<T> for Uservà caller dùngUser::from(value)hoặcvalue.into(). Nếu có thể fail, dùngTryFrom<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ạngType::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.
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ậnselfvì lúc đó chưa có instance nào. - Method chain: mỗi setter
fn name(mut self, ...) -> Selfnhậnselfbằ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ả raUserhoàn chỉnh. - Optional field tự nhiên: caller có thể bỏ qua
.email()hoặc.active(), vẫn raUserhợ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.
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ậnself.
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ỏselfparameter, biến nó thành associated function. Caller chuyển từu.foo()sangUser::foo()— rõ ràng hơn về intent.
Tổng Kết
- Associated function = hàm trong
implblock không nhậnself; gọi quaType::function(), không quainstance.function(). Tương đương static method Java/C++ hay classmethod Python. - Constructor idiom: tên
new, return-> Self, bodySelf { ... }. Đây là convention không phải keyword — caller mong đợiType::new()ở mọi struct phi-trivial. Selfalias 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
Defaultchuẩn hoá::default(), dùng được trong genericT: 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 traitFrom(infallible) hoặcTryFrom(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
selftrong 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.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Cho struct
Article { title: String, body: String, views: u64, published: bool }. Viết associated functionnew(title: String, body: String) -> Selfvớiviews = 0,published = false. DùngSelftrong cả return type và body. - 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. - So sánh
User::new("An".into(), 30)và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 trongimplblock? - Cho struct
Counter { value: u32 }. Khi nào nên derive#[derive(Default)]và khi nào nên implDefaulttay? Cho ví dụ vớivaluemặc định0(derive được) vàvaluemặc định100(phải tay). - Cho struct
HttpRequest { url: String, method: String, headers: Vec<(String, String)>, body: Option<String>, timeout_ms: u64 }. Vì sao constructornew(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. - Hàm
fn print_debug() { println!("debug"); }được đặt trongimpl User. Đây là associated function hay method? Caller gọi nó thế nào? Có nên giữ nó trongimpl Userkhông, vì sao?
Đáp án
Dùngimpl Article { fn new(title: String, body: String) -> Self { Self { title, body, views: 0, published: false } } }Selfở cả return và body để refactor-friendly: đổi tênArticlesau này chỉ cần sửa khai báo struct + dòngimpl Article; body không phải đổi.- Không compile vì Rust không hỗ trợ function overload — hai hàm cùng tên
footrong cùngimpllà duplicate definition. Fix: (a) đặt tên khác nhau theo idiom:fn new() -> Uservàfn with_name(name: String) -> User. (b) gộp thành một hàm nhậnOption<String>:fn new(name: Option<String>) -> User; hoặc dùng builder nếu nhiều biến thể. User::new(...)dùng::— gọi associated function (không nhậnself);u1.greet()dùng.— gọi method (nhậnself). 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.- Derive được khi mặc định mong muốn trùng với default của field:
u32default = 0, nên#[derive(Default)] struct Counter { value: u32 }sinh raCounter::default()vớivalue = 0— đúng ý đồ. Impl tay khi cần giá trị khác default field:
Lý do: derive chỉ gọiimpl Default for Counter { fn default() -> Self { Self { value: 100 } } }Default::default()cho từng field, không cho phép tuỳ chỉnh giá trị khác. - 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ềnvec![]/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.
HttpRequest::builder().url("...").method("GET").build(). - Là associated function — vì không có
selftrong parameter. Caller gọiUser::print_debug(). Nên giữ trongimpl Userchỉ khi hàm có ý nghĩa gắn với typeUser(vd in debug info của domain User); nếu chỉ là utility chung không liên quanUser, nên chuyển ra free function ngoàiimpl— đặt trongimpl Usersẽ gây hiểu nhầm là hàm thao tác trên User.
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> và 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.
