Bỏ qua để đến nội dung

Lifetime trong Rust

Lifetime (vòng đời) thường là chỗ người mới học Rust gặp bức tường thật sự đầu tiên. Trang này đi thật nhẹ nhàng: mục tiêu chỉ là nhận ra lifetime là gì và vì sao trình biên dịch đòi nó, chứ không phải nắm hết mọi quy tắc xung quanh.

Qua bài mượn, bạn đã biết một tham chiếu (&) cho phép dùng giá trị mà không sở hữu nó. Nhưng tham chiếu chỉ hợp lệ khi giá trị nó trỏ tới vẫn còn tồn tại. Rust cần đảm bảo bạn không bao giờ cầm một tham chiếu tới thứ đã bị dọn dẹp. Loại lỗi đó gọi là tham chiếu treo (dangling reference), một nguồn gây sập chương trình kinh điển trong các ngôn ngữ như C.

Phần lớn thời gian, Rust tự suy ra điều này bằng cách nhìn các phạm vi trong code, bạn không phải nghĩ gì cả (đúng như bạn đã thấy ở trang Phạm vi). Nhưng đôi khi, nhất là với hàm, trình biên dịch không tự suy ra được và nhờ bạn viết rõ ra. Đó chính là chú thích lifetime (lifetime annotation).

Đây là hàm trả về slice chuỗi dài hơn trong hai slice:

fn longest(a: &str, b: &str) -> &str {
if a.len() > b.len() { a } else { b }
}

Hàm này không biên dịch được. Lỗi trông như sau (trình biên dịch báo thiếu chỉ định lifetime):

error[E0106]: missing lifetime specifier

Vấn đề là: tham chiếu trả về có thể được mượn từ a hoặc từ b, và Rust không thể biết là cái nào chỉ bằng cách đọc hàm. Vì vậy nó không đảm bảo được tham chiếu bạn nhận lại vẫn còn hợp lệ vào lúc bạn dùng.

Bạn sửa bằng cách đặt tên cho một lifetime (theo quy ước là một chữ thường như 'a) và nói với Rust rằng lifetime của giá trị trả về gắn với cả hai tham số:

fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}
fn main() {
let s1 = String::from("hello");
let s2 = String::from("hi");
let result = longest(&s1, &s2);
println!("Dài nhất: {result}");
}

Kết quả:

Dài nhất: hello
▶ Tự chạy thử

Hai chuỗi được giữ tiếng Anh vì len() đếm byte, còn chữ tiếng Việt có dấu chiếm nhiều byte hơn.

Hãy đọc 'a như một cái nhãn, không phải một giá trị: nó không đặt ra một khoảng thời gian cụ thể nào. Nó chỉ nói với Rust rằng “các tham chiếu này đều phải còn hợp lệ ít nhất trên cùng một đoạn code”. Sau đó trình biên dịch kiểm tra từng chỗ gọi hàm để chắc chắn điều đó thật sự đúng.

Không có chú thích này, trình biên dịch thật sự không biết được tham chiếu mà longest trả về có thể sống lâu hơn một trong hai chuỗi gốc hay không. Chú thích lifetime là thứ giúp nó chứng minh, ngay lúc biên dịch và không tốn chi phí lúc chạy, rằng tham chiếu bạn nhận lại luôn dùng được an toàn.

Chỗ tiếp theo bạn sẽ gặp lifetime là trong struct. Đến giờ, mọi trường của struct đều sở hữu dữ liệu của nó, ví dụ một String. Một trường cũng có thể là tham chiếu, nhưng khi đó struct chỉ hợp lệ khi dữ liệu nó trỏ tới còn tồn tại. Rust bắt bạn ghi điều đó ra bằng một lifetime:

struct Highlight<'a> {
text: &'a str, // mượn từ một String nằm ở chỗ khác
}
fn main() {
let book = String::from("Call me Ishmael. Some years ago...");
let first = Highlight { text: &book[..15] };
println!("Trích đoạn: {}", first.text);
println!("Cả cuốn sách dài {} byte", book.len());
}

Kết quả:

Trích đoạn: Call me Ishmael
Cả cuốn sách dài 34 byte
▶ Tự chạy thử

Câu mở đầu cuốn sách được giữ tiếng Anh vì ví dụ cắt theo byte ([..15]) và đếm byte.

Hãy đọc Highlight<'a> là “một Highlight mượn một thứ gì đó trong lifetime 'a”. Phần &book[..15] là một slice: nó mượn 15 byte đầu tiên của book mà không sao chép. Vì first chỉ mượn, book vẫn dùng được sau đó.

Lifetime là thứ giúp Rust bắt được lỗi sau, khi String bị drop trong lúc struct vẫn còn trỏ vào nó:

struct Highlight<'a> {
text: &'a str,
}
fn main() {
let highlight;
{
let book = String::from("Call me Ishmael. Some years ago...");
highlight = Highlight { text: &book[..15] };
} // book is dropped here...
println!("{}", highlight.text); // ...but highlight still points into it
}
error[E0597]: `book` does not live long enough
--> src/main.rs:9:40
|
8 | let book = String::from("Call me Ishmael. Some years ago...");
| ---- binding `book` declared here
9 | highlight = Highlight { text: &book[..15] };
| ^^^^ borrowed value does not live long enough
10 | } // book is dropped here...
| - `book` dropped here while still borrowed
11 |
12 | println!("{}", highlight.text); // ...but highlight still points into it
| -------------- borrow later used here

Rustc báo rằng book không sống đủ lâu: nó bị drop ở dòng 10 trong khi highlight vẫn còn mượn nó ở dòng 12. Các chú thích trong ví dụ này để tiếng Anh vì rustc in lại chúng trong thông báo lỗi.

Một mẹo thực tế: nếu lifetime trong struct gây vướng víu, hãy lưu một String có chủ sở hữu thay vì &str. Cách này sao chép văn bản, nhưng struct không còn phụ thuộc vào thứ gì khác, và lifetime biến mất. Nhiều lập trình viên Rust giàu kinh nghiệm bắt đầu như vậy, và chỉ chuyển sang tham chiếu khi có lý do.

Bạn sẽ viết 'a ít hơn nhiều so với những gì trang này có thể khiến bạn nghĩ. Rất nhiều code Rust không bao giờ cần lifetime tường minh, vì trình biên dịch suy ra được trong hầu hết tình huống hằng ngày. Bạn chủ yếu gặp chúng khi một hàm trả về tham chiếu mà trình biên dịch không tự biết nó đến từ đâu. Lúc đó, hãy nhớ: đó không phải lỗi, mà là trình biên dịch nhờ bạn xác nhận một điều nó không tự chứng minh được.

Hãy viết hàm shortest nhận hai slice chuỗi và trả về slice ngắn hơn. Nếu chúng dài bằng nhau, trả về slice đầu tiên. Gọi hàm với "elephant" và "cat".

Gợi ý: hàm có hình dạng giống longest ở trên. Bạn cần <'a> sau tên hàm, và 'a ở cả hai tham số lẫn kiểu trả về.

Xem lời giải
fn shortest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() <= b.len() { a } else { b }
}
fn main() {
let big = String::from("elephant");
let small = String::from("cat");
println!("Ngắn nhất: {}", shortest(&big, &small));
println!("Bằng nhau thì chọn cái đầu: {}", shortest("dog", "cow"));
}

Kết quả:

Ngắn nhất: cat
Bằng nhau thì chọn cái đầu: dog
▶ Tự chạy thử

Slice trả về có thể đến từ a hoặc từ b, nên Rust cần 'a để biết kết quả chỉ hợp lệ khi cả hai đầu vào còn hợp lệ. Dùng <= thay vì < khiến trường hợp bằng nhau chọn a.

Hãy tạo struct Quote có hai trường &str là text và author. Tạo một giá trị trong đó text mượn 20 byte đầu của String "Fearless concurrency is a goal of Rust", còn author là literal "The Rust Book". In ra dạng Fearless concurrency (The Rust Book).

Gợi ý: giống Highlight ở trên, struct cần một lifetime: struct Quote<'a>, với &'a str cho cả hai trường. &speech[..20] là slice 20 byte đầu.

Xem lời giải
struct Quote<'a> {
text: &'a str,
author: &'a str,
}
fn main() {
let speech = String::from("Fearless concurrency is a goal of Rust");
let quote = Quote {
text: &speech[..20],
author: "The Rust Book",
};
println!("{} ({})", quote.text, quote.author);
}

Kết quả:

Fearless concurrency (The Rust Book)
▶ Tự chạy thử

quote mượn từ speech, nên nó không thể sống lâu hơn speech. Literal "The Rust Book" được nhúng sẵn vào chương trình và sống suốt thời gian chạy, nên dư sức đáp ứng 'a. Hãy thử tạo speech bên trong một khối { } riêng rồi in quote sau khi khối kết thúc, giống ví dụ lỗi ở trên, và xem trình biên dịch chặn bạn lại.

  • Lifetime cho trình biên dịch biết các tham chiếu liên quan với nhau ra sao, để nó loại trừ được tham chiếu treo.
  • Bạn chỉ cần viết lifetime khi trình biên dịch không tự suy ra được, thường gặp nhất là hàm trả về tham chiếu.
  • 'a không đặt ra khoảng thời gian nào: nó nối lifetime của các tham chiếu khác nhau để trình biên dịch kiểm tra.
  • Struct chứa tham chiếu cũng cần lifetime (struct Highlight<'a>), và không thể sống lâu hơn dữ liệu nó mượn. Lưu một String có chủ sở hữu sẽ tránh được chuyện này.
  • Đây hoàn toàn là kiểm tra lúc biên dịch; không tốn thêm chi phí lúc chạy.

Kiểm tra nhanh

1. Lifetime giải quyết vấn đề gì?

2. Vì sao fn longest(a: &str, b: &str) -> &str không biên dịch được khi thiếu chú thích lifetime?

3. Thêm chú thích lifetime có tốn chi phí gì lúc chạy không?

4. Theo trang này, khi nào bạn thường phải viết lifetime tường minh nhất?

Điểm: 0 / 4