Bài viết này tìm hiểu về Rust Lifetime
. Lifetime
là một thuật ngữ không hẳn là mới nhưng chỉ đến ngôn ngữ Rust, nó được đưa thành cú pháp và được quan tâm đặc biệt.
Viết code sử dụng generic với lifetime sẽ hơi khó đọc. Đây là một chủ đề khó. Bạn cũng không nên phiền lòng nếu như ngay lần đầu bạn không thể hiểu nó là gì. Nhiều người cũng như bạn thôi.
Lifetime là gì?
- Rust cho phép 1 và chỉ 1 owner của một vùng nhớ
- Rust cho phép nhiều tham chiếu
- Lifetime thực thi phần của bộ nhớ là valid cho một reference
Điều này nghĩa là Rust đảm bảo cho tham chiếu luôn trỏ đến vùng nhớ hợp lệ, và lifetime đảm nhiệm tính năng này
Cú pháp lifetime
Sử dụng 'a
, 'b
, hoặc bất kỳ 'this_is_lifetime
. Bạn có thể tuỳ ý đặc tên cho lifetime đó
Ngoài ra có một tham chiếu đặc biệt 'static
. Nó báo hiệu lifetime này sẽ tồn tại đến hết chương trình. Các biến constant mặc định là dạng lifetime này
Nguyên tắc
- Lifetime không áp dụng tới các hàm mà đầu vào và đầu ra không phải là tham chiếu. Ví dụ hàm
fn test_1(param1: Vec<f64>)-> Vec<f64>
không áp dụng. Hoặc nếu chỉ có đầu vào tham chiếu mà đầu ra không tham chiếufn test_1(param1: &'a Vec<f64>)-> Vec<f64>
thì cũng không áp dụng - Lifetime đảm bảo cho biến trả về có tham chiếu hợp lệ
- Lifetime khai báo khi định nghĩa
function
,struct
,impl
- Những tham số nào không phải tham chiếu, compiler gán lifetime mặc định ok
Ví dụ 1: Tham chiếu đến một đối tượng bị huỷ
fn. main() {
let a;
{
let b = String::from("Howdy");
a = &b;
}
println!("{}",a);
}
Đoạn code trên gặp lỗi như sau:
error[E0597]: `b` does not live long enough
--> src/lifetime_test.rs:5:13
|
4 | let b = String::from("Howdy");
| - binding `b` declared here
5 | a = &b;
| ^^ borrowed value does not live long enough
6 | }
| - `b` dropped here while still borrowed
7 |
8 | println!("{}", a);
| - borrow later used here
Giải thích:
Bởi vì a thì trỏ đến b, mà khi sử dụng println!("{}",a);
thì b
đã bị drop
đi sau khi ra khỏi {}
Ví dụ 2: Trả về một tham chiếu tới biến tạm hàm
fn get_int_ref() -> &i32 {
let a = 1;
&a
}
Thông tin lỗi
instead, you are more likely to want to return an owned valuerustcE0106
lifetime_test.rs(22, 21): original diagnostic
consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`: `'static `rustcE0106
lifetime_test.rs(22, 21): original diagnostic
mismatched types
expected `&i32`, found `()`rustcClick for full compiler diagnostic
lifetime_test.rs(22, 4): implicitly returns `()` as its body has no tail or `return` expression
i32
Nó gợi ý cho chúng ta nên trả về 1 tham chiếu liftime dạng 'static
sẽ giải quyết được vấn đề
Ví du 3: Nhận vào một tham chiếu và trả về tham chiếu
fn get_in_ref(param1 : &i32) -> &i32 {
param1
}
fn main() {
let variable = 10;
let result = get_int_ref(&variable);
println!("{}", result);
}
Chương trình này biến dịch được không có vấn đề gì
Ta có thể viết như sau bởi biến truyền vào với biến trả về cùng lifetime là trong giống {} của hàm main
fn get_in_ref<'a>(param1 : &'a i32) -> &'a i32 {
param1
}
Ví dụ 4: Hàm với nhiều biến đầu vào tham chiếu
fn get_in_ref<'a>(param1 : &'a i32, param2 : &'a i32, param3 : &'a i32, param4 :i32) -> &'a i32 {
param1
}
Trong turường hợp có nhiều đầu vào thì chúng ta cần xác định như sau:
- Mỗi một đầu vào là tham chiếu sẽ có một có 1 lifetime riêng mà nó sở hữu và đặc tên lần lượt là
'a
,'b
,'c
fn get_in_ref<'a, 'b, 'c>(param1 : &'a i32, param2 : &'b i32, param3 : &'c i32, param4 :i32) -> &i32 {
param1
}
Tham số thứ 4 không phải là reference
nên không cần thêm chỉ thị lifetime.
Giờ bạn nhận thấy rằng tham số param2
, param3
không bao giờ trả về
Đầu ra chúng ta thấy nó có cùng vòng đời với param1
nên ta có thể viết như sau
fn get_in_ref<'a, 'b, 'c>(param1 : &'a i32, param2 : &'b i32, param3 : &'c i32, param4 :i32) -> &'a i32 {
param1
}
Biên dịch thành công!
Giả sử logic như này
fn get_in_ref<'a, 'b, 'c>(param1 : &'a i32, param2 : &'b i32, param3 : &'c i32, param4 :i32) -> &'a i32 {
if param1 > param2 {
param1
} else {
param2 // <-- may not live long enough
}
}
Lúc đó thì chương trình sẽ báo lỗi
lifetime may not live long enough
consider adding the following bound: `'b: 'a`rustcClick for full compiler diagnostic
lifetime_test.rs(31, 19): lifetime `'b` defined here
lifetime_test.rs(31, 15): lifetime `'a` defined here
// size = 8, align = 0x8
param2: &i32
Hàm này bạn bảo rằng trả về một biến có vòng đời (lifetime) là 'a
, nhưng tại sao trong code bạn trả về param2
có vòng đời là 'b
Do dó compiler lo ngại rằng điều bạn muốn không được cam kết nên nó không compile và báo lỗi trên.
Compile cũng sẽ hỏi bạn một câu là thế thì 'b
có liên quan gì đến 'a
không. Nếu 'b > 'a
thì tôi thấy okie đấy, còn 'b < 'a
thì thôi không thấy ổn chút nào
Bạn có thể sửa như sau:'b: 'a
đọc là 'b
ít nhất có vòng đời là 'a
fn get_in_ref<'a, 'b: 'a, 'c>(param1 : &'a i32, param2 : &'b i32, param3 : &'c i32, param4 :i32) -> &'a i32 {
if param1 > param2 {
param1
} else {
param2 // <-- may not live long enough
}
}
Biên dịch tốt!
Hoặc bạn nhận ra rằng param2
cũng có vòng đời giống param
. Nghĩa là 'b = 'a
nên thay thì viết như trên bạn thay 'b
là 'a
luôn xong cho nhanh.
fn main() {
let variable1 = 10; // <--- Có cùng scope với varable2
let variable2 = 20; // <--- Có cùng scope với variable1
let variable3 = 30;
let variable4 = 40;
let result = get_int_ref(&variable1, &variable2, &variable3, variable4);
println!("{}", result);
}
fn get_in_ref<'a, 'c>(
param1: &'a i32,
param2: &'a i32,
param3: &'c i32,
param4: i32,
) -> &'a i32 {
if param1 > param2 {
param1
} else {
param2
}
}
Compiler vẫn happy và biên dịch tốt.
Ví du 5: Tham chiếu đầu ra, nhưng đầu vào không có tham chiếu
fn test4(param1: Vec<f64>) -> &Vec<f64> {
¶m1
}
Sẽ báo lỗi
...or alternatively, you might want to return an owned valuerustcE0106
lifetime_test.rs(52, 30): original diagnostic
consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`: `'static `rustcE0106
lifetime_test.rs(52, 30): original diagnostic
Sửa lại như sau:
fn test4<'a>(param1: Vec<f64>) -> &'a Vec<f64> {
¶m1
}
Nhưng vẫn bị lỗi
cannot return reference to function parameter `param1`
returns a reference to data owned by the current functionrustcClick for full compiler diagnostic
// size = 24 (0x18), align = 0x8
param1: Vec<f64>
Ownership của param1
là được chuyển tới hàm này. Nó sẽ bị huỷ đi sau khi ra khỏi hàm. Trở về một tham chiếu bị drop
sẽ không hợp lệ. Nên trong trường hợp hàm này
chỉ có thể trả về một tham chiếu 'static
hoặc đơn giản nhất là viết
fn test4(param1: Vec<f64>) -> Vec<f64> {
param1
}
Vừa hợp lý vừa đúng.
Ví dụ: Lifetime với slice
fn get_vec_slice(param1: &[i32]) -> &[i32]{
¶m1[0..2]
}
Biên dịch ok và không vấn đề.
Ví dụ 7: Static lifetime
const A : &str = "Constant A";
const B: &str = "Constant B";
fn main() {
let greater = some_func(&A, &B);
}
fn some_func(param1: &'static str, param2: &'static str) -> &'static str {
if param1 > param2 {
param1
} else {
param2
}
}
Biên dịch ok và không vấn đề.
Nếu thay đổi thế này thì bị lỗi
const A : &str = "Constant A";
const B: &str = "Constant B";
fn main() {
let a = String::from("a");
let greater = some_func(&a, &B);
}
fn some_func(param1: &'static str, param2: &'static str) -> &'static str {
if param1 > param2 {
param1
} else {
param2
}
}
`a` does not live long enough
borrowed value does not live long enoughrustcClick for full compiler diagnostic
Ví du 8: Lifetime với generic function
fn get_smaller<T: std::cmp::PartialOrd>(param1: &T, param2: &T) -> &T {
if param1 < param2 {
param1
} else {
param2
}
}
Lỗi như sau:
missing lifetime specifier
this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `param1` or `param2`rustcClick for full compiler diagnostic
Sửa lại như sau:
fn get_smaller<'a, T: std::cmp::PartialOrd>(param1: &'a T, param2: &'a T) -> &'a T {
if param1 < param2 {
param1
} else {
param2
}
}
Ví dụ 9: Lifetime trong struct
struct Mystruct<'a> {
some_data: Vec<i32>,
some_ref1: &'a Vec<i32>,
}
struct Mystruct<'a,'b> {
some_data: Vec<i32>,
some_ref1: &'a Vec<i32>,
some_ref2: &'b Vec<i32>,
}