Lifetimes và domain model tài chính
Nối tiếp bài ownership: khi reference phải bước ra khỏi một hàm
Bài trước bạn đã nắm luật borrow: một &mut tại một thời điểm, và reference không bao giờ được sống lâu hơn owner. Bài đó mọi thứ nằm gọn trong một hàm. Thực tế trong service ngân hàng thì khác: dữ liệu đi qua nhiều hàm, nhiều struct, nhiều tầng. Bài này trả lời câu hỏi tiếp theo: khi nào compiler bắt bạn viết lifetime, và khi nào câu trả lời đúng lại là "đừng dùng reference nữa, dùng owned type".
Sau 30 phút này bạn cần:
- Đọc được lỗi lifetime và biết compiler đang chỉ vào đâu.
- Nhận ra 3 tình huống bắt buộc phải viết lifetime annotation.
- Thiết kế
Account,Transaction,Moneybằng owned type, đúng kiểu codebase tài chính. - Biết khi nào struct có lifetime là lựa chọn tốt (report, view) và khi nào là bẫy.
- Tránh ba cạm bẫy tôi gặp nhiều nhất khi review code banking: lạm dụng
'static, self-referential struct, và thêm lifetime để "chữa" lỗi compile.
Lifetime là gì, và compiler đang hỏi bạn điều gì
Lifetime không phải tuổi thọ của giá trị. Nó là _____. Khi bạn viết &Account, compiler cần biết reference đó hợp lệ tới đâu để chứng minh không có chỗ nào dùng nó sau khi Account bị drop. 'a chỉ là cái nhãn cho vùng đó.
Điểm quan trọng: bạn không "gán" lifetime cho biến. Bạn khai báo quan hệ giữa các reference, rồi compiler tự kiểm tra.
Phần lớn thời gian bạn không viết lifetime vì elision rules đã lo:
fn balance_of(account: &Account) -> &Money {
&account.balance
}
// compiler hiểu thành:
// fn balance_of<'a>(account: &'a Account) -> &'a Money
Quy tắc cần nhớ: mỗi input reference có lifetime riêng; nếu function chỉ có một input reference thì output nhận _____; nếu có &self thì output nhận lifetime của self.
Khi nào bắt buộc phải viết lifetime annotation
Ba tình huống, và chỉ ba:
Sign in to view the full content of this lesson. Sign in
- Struct hoặc enum chứa reference. Kiểu dữ liệu đó bắt buộc phải khai báo _____.
- Function có nhiều input reference và trả về reference. Compiler không đoán được output mượn từ tham số nào.
- Method trả về reference không xuất phát từ
&self. Hiếm, nhưng có.
Trường hợp 2 là chỗ hay gặp nhất:
// error[E0106]: missing lifetime specifier
fn older_account(a: &Account, b: &Account) -> &Account { ... }
// đúng: nói rõ output sống bằng vùng giao của cả hai
fn older_account<'a>(a: &'a Account, b: &'a Account) -> &'a Account {
if a.opened_at <= b.opened_at { a } else { b }
}
Lưu ý cái sai phổ biến: nếu output chỉ đến từ một tham số, chỉ tham số đó cần 'a, tham số còn lại để lifetime riêng. Viết 'a cho tất cả là tự bó hẹp quyền dùng hàm.
Bài tập 1 (capstone): bạn viết hàm tìm account theo id, có thêm tham số
note: &strđể log. Vì sao signature dưới đây không compile, và sửa thế nào?
fn find_account(accounts: &[Account], note: &str) -> Option<&Account>
Xem đáp án
Có hai input reference nên elision không áp dụng, compiler báo E0106. Chỉ accounts là nguồn của kết quả, nên chỉ nó cần lifetime:
fn find_account<'a>(accounts: &'a [Account], note: &str) -> Option<&'a Account> {
// ... dùng note để log
}
Domain model tài chính: mặc định là owned type
Đây là bài học đắt nhất tôi rút ra sau nhiều lần refactor: entity nghiệp vụ thì dùng owned type, không dùng reference.
pub struct AccountId(pub u64);
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Currency { Vnd, Usd }
#[derive(Clone, Copy)]
pub struct Money {
pub amount_minor: i64, // VND: minor = đồng, USD: minor = cent
pub currency: Currency,
}
pub struct Account {
pub id: AccountId,
pub owner_name: String,
pub balance: Money,
pub opened_at: u64,
}
pub struct Transaction {
pub id: u64,
pub from: AccountId,
pub to: AccountId,
pub amount: Money,
}
Chú ý Transaction trỏ tới account bằng _____, không bằng &Account. Lý do rất thực tế: transaction được validate, ghi DB, đẩy qua channel, đọc lại vài giờ sau. Reference không sống được qua những ranh giới đó, và borrow checker sẽ chặn bạn ngay khi thử. Trong banking, quan hệ giữa entity là ID, không phải reference.
Lỗi kinh điển khi mới học là cố làm ngược lại:
struct Account<'a> { owner_name: &'a str }
fn make_account<'a>() -> Account<'a> {
let name = String::from("Nguyen Van A");
Account { owner_name: &name }
}
// error[E0515]: cannot return value referencing local variable `name`
Đây chính là _____. Sửa đúng không phải thêm lifetime, mà là đổi sang String.
Bài tập 2 (capstone): chuyển struct sau sang owned type và nêu một trường hợp vẫn nên giữ bản có lifetime.
struct Transfer<'a> { from: &'a Account, amount: Money }
Xem đáp án
struct Transfer { from: AccountId, amount: Money }
Bản có lifetime vẫn hữu ích khi Transfer chỉ là view tạm trong một hàm nội bộ, ví dụ bước validate đọc account đã load sẵn, không lưu lại, không gửi đi đâu. Ngay khi nó cần đi qua async, DB hay channel, hãy quay về owned.
Khi nào struct có lifetime thực sự thắng
Có một chỗ lifetime là lựa chọn đúng: báo cáo read-only trong cùng một scope, nơi bạn không muốn clone hàng nghìn transaction chỉ để đọc.
pub struct AccountStatement<'a> {
pub account: &'a Account,
pub rows: Vec<&'a Transaction>,
pub total_debit: Money,
}
pub fn build_statement<'a>(
account: &'a Account,
txns: &'a [Transaction],
) -> AccountStatement<'a> {
// duyệt và tham chiếu, không clone
todo!()
}
flowchart TD
Start["Struct moi"] --> Q1{"Di qua async, DB hay channel?"}
Q1 -->|Co| Owned["Owned type<br/>String, AccountId, Money"]
Q1 -->|Khong| Q2{"Chi doc tam trong mot scope?"}
Q2 -->|Co| Borrowed["Struct co lifetime<br/>AccountStatement"]
Q2 -->|Khong| Owned
Một cảnh báo ngắn: nếu future giữ &Account trỏ vào dữ liệu local, tokio::spawn sẽ đòi 'static và bạn sẽ phải đổi sang owned hoặc Arc. Chi tiết để bài async/await lo.
Những lỗi lifetime tôi gặp nhiều nhất khi review code banking
- Hiểu sai
'static. Nó không có nghĩa là "giá trị sống mãi trong bộ nhớ". Nó là _____.&'static strtừ string literal là ví dụ. - Thêm lifetime để tắt lỗi. Nếu bạn phải viết ba lifetime cho một struct nghiệp vụ, gần như chắc chắn thiết kế sai. Dừng lại, đổi sang owned.
- Self-referential struct. Struct vừa sở hữu
Stringvừa giữ&strtrỏ vào chính nó là bất khả thi an toàn. Dùng ID, index, hoặcArc. - Nghe lời compiler "consider using 'static". Trong service production, đây thường là lời khuyên dẫn tới chỗ khác cũng hỏng. Hãy đổi sang owned type.
- Rule of thumb: entity đi qua async/DB/channel thì owned; view đọc tạm trong một scope thì lifetime.
Mang gì sang bài traits và generics
Ba điều cần nhớ: lifetime là ràng buộc về phạm vi hợp lệ của reference, không phải tuổi thọ giá trị; chỉ ba tình huống bắt buộc annotate; và domain model tài chính mặc định dùng owned type, reference chỉ dành cho view tạm.
Bài tiếp theo chúng ta sẽ trừu tượng hóa trên chính domain model này: dùng traits và generics để viết một LedgerRepository hay FeePolicy mà không phải sửa Account mỗi lần thêm loại phí mới.