Tìm hiểu Lifetimes trong Rust

Table of Contents
Giới thiệu
Rust thường được ca ngợi vì khả năng cung cấp an toàn bộ nhớ mà không cần đến bộ thu gom rác (garbage collector). Cốt lõi của sự đảm bảo an toàn này là một khái niệm được gọi là "quyền sở hữu" (ownership), và gắn liền với quyền sở hữu là "vòng đời" (lifetimes). Đối với nhiều nhà phát triển đến từ các ngôn ngữ có quản lý bộ nhớ tự động (như Python, Java, hoặc C#) hoặc các ngôn ngữ có quản lý bộ nhớ thủ công (như C hoặc C++), vòng đời có thể là một trong những khái niệm khó nắm bắt nhất.
Trong hướng dẫn toàn diện này, chúng ta sẽ làm rõ vòng đời là gì, tại sao chúng tồn tại và cách bạn có thể làm việc hiệu quả với chúng trong các chương trình Rust của mình. Đến cuối bài viết này, bạn sẽ có một sự hiểu biết vững chắc về tính năng cơ bản này.
Vòng đời là gì?
Nói một cách đơn giản nhất, vòng đời là một cấu trúc mà trình biên dịch Rust sử dụng để đảm bảo rằng tất cả các lần mượn (borrows) đều hợp lệ. Mọi tham chiếu trong Rust đều có một vòng đời, đó là phạm vi mà tham chiếu đó hợp lệ. Hầu hết thời gian, vòng đời là ngầm định và được suy luận, giống như hầu hết thời gian các kiểu dữ liệu được suy luận. Tuy nhiên, khi chúng ta có các hàm hoặc cấu trúc sử dụng tham chiếu, chúng ta thường cần chú thích các vòng đời này để cho trình biên dịch biết vòng đời của các tham chiếu khác nhau liên quan đến nhau như thế nào.
Mục tiêu chính của vòng đời là ngăn chặn "tham chiếu lơ lửng" (dangling references). Tham chiếu lơ lửng xảy ra khi một chương trình cố gắng truy cập dữ liệu đã được giải phóng hoặc loại bỏ. Trong C++, điều này thường dẫn đến hành vi không xác định, lỗi phân đoạn (segfaults) hoặc các lỗ hổng bảo mật. Trình biên dịch của Rust (rustc) sử dụng bộ kiểm tra mượn (borrow checker) để đảm bảo rằng tất cả các lần mượn đều hợp lệ và không có tham chiếu nào tồn tại lâu hơn dữ liệu mà nó trỏ đến.
Bộ kiểm tra mượn
Để hiểu vòng đời, trước tiên chúng ta phải hiểu bộ kiểm tra mượn. Bộ kiểm tra mượn so sánh các phạm vi để xác định xem tất cả các lần mượn có hợp lệ hay không. Hãy xem một ví dụ kinh điển về những gì bộ kiểm tra mượn ngăn chặn:
{
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
} // ---------+
Trong đoạn mã này, r có vòng đời 'a, trong khi x có vòng đời 'b. Khối bên trong tạo ra x, và chúng ta cố gắng gán một tham chiếu đến x cho r. Khi khối bên trong kết thúc, x bị loại bỏ và ra khỏi phạm vi. Tuy nhiên, r vẫn còn trong phạm vi và đang cố gắng trỏ đến bộ nhớ nơi x từng tồn tại. Bộ kiểm tra mượn nhìn vào điều này và nói, "Lỗi: x không tồn tại đủ lâu." Vòng đời 'b ngắn hơn 'a, vì vậy tham chiếu không hợp lệ.
Chú thích vòng đời rõ ràng
Mặc dù trình biên dịch đủ thông minh để suy luận vòng đời trong nhiều trường hợp đơn giản, nhưng có những lúc nó cần sự giúp đỡ của bạn. Khi một hàm nhận nhiều tham chiếu hoặc trả về một tham chiếu, nó cần biết vòng đời của tham chiếu được trả về liên quan đến vòng đời của các tham chiếu đầu vào như thế nào.
Hãy xem xét một hàm trả về chuỗi con dài hơn trong hai chuỗi con:
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
Nếu bạn cố gắng biên dịch đoạn mã này, Rust sẽ báo lỗi: missing lifetime specifier. Trình biên dịch không biết liệu tham chiếu được trả về sẽ trỏ đến x hay y. Vì x và y có thể có vòng đời khác nhau, trình biên dịch cần biết cái nào quyết định vòng đời của giá trị trả về.
Để khắc phục điều này, chúng ta giới thiệu các tham số vòng đời chung:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
Ở đây, chúng ta khai báo một tham số vòng đời 'a trong dấu ngoặc nhọn <'a>. Sau đó, chúng ta chú thích các tham số x và y và kiểu trả về với vòng đời này. Điều này cho bộ kiểm tra mượn biết: "Hàm longest nhận hai tham chiếu tồn tại ít nhất bằng một vòng đời 'a, và nó trả về một tham chiếu cũng tồn tại ít nhất bằng 'a."
Trong thực tế, điều này có nghĩa là tham chiếu được trả về sẽ hợp lệ miễn là cả x và y đều hợp lệ. Bộ kiểm tra mượn sẽ giới hạn vòng đời của giá trị trả về theo vòng đời ngắn hơn của các đối số.
Loại bỏ vòng đời
Viết rõ ràng vòng đời ở khắp mọi nơi sẽ làm cho mã Rust rất dài dòng. Để giảm bớt điều này, nhóm Rust đã triển khai "các quy tắc loại bỏ vòng đời" vào trình biên dịch. Đây là ba quy tắc đơn giản mà trình biên dịch tuân theo để suy luận vòng đời trong các chữ ký hàm:
- Mỗi vòng đời bị loại bỏ ở vị trí đầu vào trở thành một tham số vòng đời riêng biệt.
- Nếu chỉ có chính xác một vị trí vòng đời đầu vào (bị loại bỏ hay không), vòng đời đó được gán cho tất cả các vòng đời đầu ra bị loại bỏ.
- Nếu có nhiều vị trí vòng đời đầu vào, nhưng một trong số đó là
&selfhoặc&mut self(như trong một phương thức), vòng đời củaselfđược gán cho tất cả các vòng đời đầu ra bị loại bỏ.
Nếu trình biên dịch áp dụng các quy tắc này mà vẫn không thể xác định vòng đời của các tham chiếu đầu ra, nó sẽ báo lỗi và buộc bạn phải chú thích chúng theo cách thủ công.
Cấu trúc và vòng đời
Vòng đời không chỉ giới hạn ở các hàm. Nếu một cấu trúc giữ một tham chiếu, nó cũng phải được chú thích bằng một vòng đời.
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let i = ImportantExcerpt {
part: first_sentence,
};
}
Ở đây, ImportantExcerpt giữ một tham chiếu đến một chuỗi con. Chú thích vòng đời 'a đảm bảo rằng một thể hiện của ImportantExcerpt không thể tồn tại lâu hơn tham chiếu mà nó giữ trong trường part của nó. Nếu biến novel bị loại bỏ trước thể hiện ImportantExcerpt, trình biên dịch sẽ ngăn chặn mã biên dịch.
Vòng đời tĩnh
Có một vòng đời đặc biệt mà bạn nên biết: 'static. Vòng đời 'static có nghĩa là tham chiếu có thể tồn tại trong toàn bộ thời gian chạy của chương trình. Tất cả các chuỗi ký tự đều có vòng đời 'static.
let s: &'static str = "I have a static lifetime.";
Mặc dù bạn có thể buộc một tham chiếu có vòng đời 'static (ví dụ, bằng cách làm rò rỉ bộ nhớ bằng cách sử dụng Box::leak), bạn nên sử dụng nó một cách tiết kiệm. Hầu hết thời gian, khi trình biên dịch đề xuất thêm một ràng buộc 'static, đó là dấu hiệu cho thấy có vấn đề về quyền sở hữu, chứ không phải bạn thực sự cần dữ liệu tồn tại mãi mãi.
Kết luận
Vòng đời là một khái niệm độc đáo trong Rust cho phép nó đảm bảo an toàn bộ nhớ mạnh mẽ mà không cần bộ thu gom rác. Bằng cách hiểu cách bộ kiểm tra mượn phân tích các phạm vi, cách sử dụng chú thích vòng đời rõ ràng và cách trình biên dịch suy luận vòng đời thông qua việc loại bỏ, bạn có thể viết mã Rust an toàn, hiệu quả và biểu cảm.
Mặc dù chúng có thể gây khó chịu khi bạn mới gặp chúng, vòng đời cuối cùng sẽ trở thành bản năng thứ hai. Chúng buộc bạn phải suy nghĩ cẩn thận về bố cục bộ nhớ và vòng đời của dữ liệu, cuối cùng giúp bạn trở thành một lập trình viên giỏi hơn. Hãy tiếp tục luyện tập, đừng ngại đọc các thông báo lỗi của trình biên dịch (chúng cực kỳ hữu ích!), và chẳng bao lâu nữa bạn sẽ thành thạo vòng đời trong các dự án của riêng mình.
Bạn cũng có thể thích
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

Tại sao tôi học Rust với tư cách là một nhà phát triển web (và bạn cũng nên như vậy)
Rust không chỉ dành cho các lập trình viên hệ thống. Đây là lý do tại sao các nhà phát triển web đang chọn Rust, và cách sáu tháng làm việc với borrow checker đã thay đổi cách tôi nghĩ về JavaScript và Python.
Read more
Rust cho nhà phát triển Frontend: Hướng dẫn chuyển đổi thực tế
Tìm hiểu lý do tại sao các nhà phát triển frontend ngày càng áp dụng Rust cho tooling và WebAssembly, cùng cách bạn có thể chuyển đổi mô hình tư duy từ JavaScript/TypeScript.
Read more
Xây dựng tác nhân AI tự trị trong Rust
Kỹ sư tác nhân AI tự trị thông lượng cao trong Rust: tận dụng đồng thời tokio, lược đồ công cụ LLM có kiểu, tìm kiếm vector và độ trễ dưới mili giây.
Read more