Hãy xem xét một cách thực hiện đơn giản này :
struct Parent {
count: u32,
}
struct Child<'a> {
parent: &'a Parent,
}
struct Combined<'a> {
parent: Parent,
child: Child<'a>,
}
impl<'a> Combined<'a> {
fn new() -> Self {
let parent = Parent { count: 42 };
let child = Child { parent: &parent };
Combined { parent, child }
}
}
fn main() {}
Điều này sẽ thất bại với lỗi:
error[E0515]: cannot return value referencing local variable `parent`
--> src/main.rs:19:9
|
17 | let child = Child { parent: &parent };
| ------- `parent` is borrowed here
18 |
19 | Combined { parent, child }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ returns a value referencing data owned by the current function
error[E0505]: cannot move out of `parent` because it is borrowed
--> src/main.rs:19:20
|
14 | impl<'a> Combined<'a> {
| -- lifetime `'a` defined here
...
17 | let child = Child { parent: &parent };
| ------- borrow of `parent` occurs here
18 |
19 | Combined { parent, child }
| -----------^^^^^^---------
| | |
| | move out of `parent` occurs here
| returning this value requires that `parent` is borrowed for `'a`
Để hiểu hoàn toàn lỗi này, bạn phải suy nghĩ về cách các giá trị được biểu diễn trong bộ nhớ và điều gì xảy ra khi bạn di chuyển
các giá trị đó. Hãy chú thích Combined::new
với một số địa chỉ bộ nhớ giả định cho biết vị trí của các giá trị:
let parent = Parent { count: 42 };
// `parent` lives at address 0x1000 and takes up 4 bytes
// The value of `parent` is 42
let child = Child { parent: &parent };
// `child` lives at address 0x1010 and takes up 4 bytes
// The value of `child` is 0x1000
Combined { parent, child }
// The return value lives at address 0x2000 and takes up 8 bytes
// `parent` is moved to 0x2000
// `child` is ... ?
Điều gì sẽ xảy ra child
? Nếu giá trị chỉ được di chuyển như cũ parent
, thì nó sẽ đề cập đến bộ nhớ không còn được đảm bảo có giá trị hợp lệ trong đó. Bất kỳ đoạn mã nào khác đều được phép lưu trữ giá trị tại địa chỉ bộ nhớ 0x1000. Truy cập bộ nhớ giả định rằng đó là một số nguyên có thể dẫn đến sự cố và / hoặc lỗi bảo mật và là một trong những loại lỗi chính mà Rust ngăn chặn.
Đây chính xác là vấn đề mà cuộc sống ngăn chặn. Thời gian tồn tại là một chút siêu dữ liệu cho phép bạn và trình biên dịch biết giá trị sẽ có hiệu lực trong bao lâu tại vị trí bộ nhớ hiện tại của nó . Đó là một sự khác biệt quan trọng, vì đó là một lỗi phổ biến mà những người mới chơi Rust mắc phải. Thời gian sống của Rust không phải là khoảng thời gian giữa khi một vật thể được tạo ra và khi nó bị phá hủy!
Tương tự như vậy, hãy nghĩ về nó theo cách này: Trong cuộc sống của một người, họ sẽ cư trú ở nhiều địa điểm khác nhau, mỗi địa điểm có một địa chỉ riêng biệt. Cả đời Rust liên quan đến địa chỉ mà bạn hiện đang cư trú , không phải về bất cứ khi nào bạn sẽ chết trong tương lai (mặc dù chết cũng thay đổi địa chỉ của bạn). Mỗi khi bạn di chuyển nó có liên quan vì địa chỉ của bạn không còn hợp lệ.
Cũng cần lưu ý rằng thời gian sống không thay đổi mã của bạn; mã của bạn kiểm soát thời gian sống, thời gian sống của bạn không kiểm soát mã. Câu nói rất hay là "cuộc sống là mô tả, không phải là kê đơn".
Chúng ta hãy chú thích Combined::new
với một số số dòng mà chúng ta sẽ sử dụng để làm nổi bật vòng đời:
{ // 0
let parent = Parent { count: 42 }; // 1
let child = Child { parent: &parent }; // 2
// 3
Combined { parent, child } // 4
} // 5
Các đời bê tông của parent
là 1-4, bao gồm (mà tôi sẽ thể hiện như [1,4]
). Tuổi thọ cụ thể child
là [2,4]
và tuổi thọ cụ thể của giá trị trả về là [4,5]
. Có thể có các vòng đời cụ thể bắt đầu từ 0 - đại diện cho thời gian tồn tại của một tham số cho một chức năng hoặc một cái gì đó tồn tại bên ngoài khối.
Lưu ý rằng thời gian tồn tại của child
chính nó là [2,4]
, nhưng nó đề cập đến một giá trị với thời gian tồn tại là [1,4]
. Điều này tốt miễn là giá trị tham chiếu trở nên không hợp lệ trước khi giá trị được giới thiệu thực hiện. Vấn đề xảy ra khi chúng tôi cố gắng trở về child
từ khối. Điều này sẽ "kéo dài quá mức" cuộc sống vượt quá chiều dài tự nhiên của nó.
Kiến thức mới này sẽ giải thích hai ví dụ đầu tiên. Cái thứ ba đòi hỏi phải nhìn vào việc thực hiện Parent::child
. Rất có thể, nó sẽ trông giống như thế này:
impl Parent {
fn child(&self) -> Child { /* ... */ }
}
Điều này sử dụng elision trọn đời để tránh viết các tham số chung chung rõ ràng . Nó tương đương với:
impl Parent {
fn child<'a>(&'a self) -> Child<'a> { /* ... */ }
}
Trong cả hai trường hợp, phương thức nói rằng một Child
cấu trúc sẽ được trả về đã được tham số hóa với tuổi thọ cụ thể của
self
. Nói một cách khác, Child
cá thể chứa một tham chiếu đến cái Parent
đã tạo ra nó, và do đó không thể sống lâu hơn Parent
cá thể đó
.
Điều này cũng cho phép chúng tôi nhận ra rằng có điều gì đó thực sự sai với chức năng tạo của chúng tôi:
fn make_combined<'a>() -> Combined<'a> { /* ... */ }
Mặc dù bạn có nhiều khả năng thấy điều này được viết dưới một hình thức khác:
impl<'a> Combined<'a> {
fn new() -> Combined<'a> { /* ... */ }
}
Trong cả hai trường hợp, không có tham số trọn đời nào được cung cấp thông qua một đối số. Điều này có nghĩa là thời gian tồn tại Combined
sẽ được tham số hóa mà không bị ràng buộc bởi bất cứ điều gì - nó có thể là bất cứ điều gì mà người gọi muốn. Điều này là vô nghĩa, bởi vì người gọi có thể chỉ định 'static
thời gian tồn tại và không có cách nào để đáp ứng điều kiện đó.
Làm thế nào để tôi sửa chữa nó?
Giải pháp đơn giản và được khuyến nghị nhất là không cố gắng đặt các mục này trong cùng một cấu trúc. Bằng cách này, cấu trúc lồng nhau của bạn sẽ bắt chước tuổi thọ của mã của bạn. Đặt các loại sở hữu dữ liệu vào một cấu trúc với nhau và sau đó cung cấp các phương thức cho phép bạn nhận được các tham chiếu hoặc các đối tượng có chứa các tham chiếu khi cần.
Có một trường hợp đặc biệt khi theo dõi suốt đời là quá nhiệt tình: khi bạn có một cái gì đó được đặt trên đống. Điều này xảy ra khi bạn sử dụng một
Box<T>
ví dụ. Trong trường hợp này, cấu trúc được di chuyển chứa một con trỏ vào heap. Giá trị nhọn sẽ vẫn ổn định, nhưng địa chỉ của chính con trỏ sẽ di chuyển. Trong thực tế, điều này không thành vấn đề, vì bạn luôn đi theo con trỏ.
Các thùng cho thuê (KHÔNG CÒN ĐƯỢC DUY TRÌ HOẶC HỖ TRỢ) hoặc thùng owning_ref nhiều cách để trình bày trường hợp này, nhưng họ yêu cầu rằng địa chỉ cơ sở không bao giờ di chuyển . Điều này loại trừ các vectơ đột biến, có thể gây ra sự phân bổ lại và di chuyển các giá trị phân bổ heap.
Ví dụ về các vấn đề được giải quyết với Cho thuê:
Trong các trường hợp khác, bạn có thể muốn chuyển sang một số loại đếm tham chiếu, chẳng hạn như bằng cách sử dụng Rc
hoặc Arc
.
Thêm thông tin
Sau khi chuyển parent
vào struct, tại sao trình biên dịch không thể có được một tham chiếu mới parent
và gán nó vào child
trong struct?
Mặc dù về mặt lý thuyết là có thể làm điều này, nhưng làm như vậy sẽ giới thiệu một lượng lớn phức tạp và chi phí chung. Mỗi khi đối tượng được di chuyển, trình biên dịch sẽ cần chèn mã để "sửa" tham chiếu. Điều này có nghĩa là sao chép một cấu trúc không còn là một hoạt động rất rẻ mà chỉ di chuyển một số bit xung quanh. Nó thậm chí có thể có nghĩa là mã như thế này đắt tiền, tùy thuộc vào mức độ tối ưu hóa giả thuyết sẽ tốt như thế nào:
let a = Object::new();
let b = a;
let c = b;
Thay vì buộc điều này xảy ra cho mọi di chuyển, lập trình viên sẽ chọn khi điều này xảy ra bằng cách tạo các phương thức sẽ lấy các tham chiếu thích hợp chỉ khi bạn gọi chúng.
Một loại có tham chiếu đến chính nó
Có một trường hợp cụ thể trong đó bạn có thể tạo một loại có tham chiếu đến chính nó. Bạn cần phải sử dụng một cái gì đó như Option
để làm cho nó trong hai bước:
#[derive(Debug)]
struct WhatAboutThis<'a> {
name: String,
nickname: Option<&'a str>,
}
fn main() {
let mut tricky = WhatAboutThis {
name: "Annabelle".to_string(),
nickname: None,
};
tricky.nickname = Some(&tricky.name[..4]);
println!("{:?}", tricky);
}
Điều này không hoạt động, trong một số ý nghĩa, nhưng giá trị được tạo ra bị hạn chế cao - nó không bao giờ có thể được di chuyển. Đáng chú ý, điều này có nghĩa là nó không thể được trả về từ một hàm hoặc được truyền bởi giá trị cho bất cứ thứ gì. Hàm xây dựng cho thấy cùng một vấn đề với thời gian sống như trên:
fn creator<'a>() -> WhatAboutThis<'a> { /* ... */ }
Thế còn Pin
?
Pin
, được ổn định trong Rust 1.33, có tài liệu này trong tài liệu mô-đun :
Một ví dụ điển hình của kịch bản như vậy sẽ là xây dựng các cấu trúc tự tham chiếu, vì việc di chuyển một đối tượng có con trỏ sang chính nó sẽ vô hiệu hóa chúng, điều này có thể gây ra hành vi không xác định.
Điều quan trọng cần lưu ý là "tự giới thiệu" không nhất thiết có nghĩa là sử dụng tài liệu tham khảo . Thật vậy, ví dụ về cấu trúc tự tham chiếu nói cụ thể (nhấn mạnh của tôi):
Chúng tôi không thể thông báo cho trình biên dịch về điều đó với một tham chiếu bình thường, vì mẫu này có thể được mô tả với các quy tắc vay thông thường. Thay vào đó, chúng tôi sử dụng một con trỏ thô , mặc dù một con trỏ được biết là không rỗng, vì chúng tôi biết nó đang trỏ vào chuỗi.
Khả năng sử dụng một con trỏ thô cho hành vi này đã tồn tại kể từ Rust 1.0. Thật vậy, sở hữu-ref và cho thuê sử dụng con trỏ thô dưới mui xe.
Điều duy nhất Pin
thêm vào bảng là một cách phổ biến để nói rằng một giá trị nhất định được đảm bảo không di chuyển.
Xem thêm:
Parent
vàChild
có thể giúp ...