Tham Chiếu và Mượn
Vấn đề với đoạn mã tuple trong Listing 4-5 là chúng ta phải trả lại String
cho
hàm gọi để chúng ta vẫn có thể sử dụng String
sau cuộc gọi đến
calculate_length
, bởi vì String
đã được chuyển vào calculate_length
. Thay
vào đó, chúng ta có thể cung cấp một tham chiếu đến giá trị String
. Tham
chiếu (reference) giống như một con trỏ ở chỗ nó là một địa chỉ mà chúng ta có
thể theo dõi để truy cập dữ liệu được lưu trữ tại địa chỉ đó; dữ liệu đó thuộc
sở hữu của một biến khác. Không giống như con trỏ, một tham chiếu được đảm bảo
trỏ đến một giá trị hợp lệ của một kiểu dữ liệu cụ thể trong suốt thời gian tồn
tại của tham chiếu đó.
Đây là cách bạn sẽ định nghĩa và sử dụng một hàm calculate_length
có một tham
chiếu đến một đối tượng làm tham số thay vì lấy quyền sở hữu của giá trị:
fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); println!("The length of '{s1}' is {len}."); } fn calculate_length(s: &String) -> usize { s.len() }
Đầu tiên, lưu ý rằng tất cả các mã tuple trong khai báo biến và giá trị trả về
của hàm đã biến mất. Thứ hai, lưu ý rằng chúng ta truyền &s1
vào
calculate_length
và trong định nghĩa của nó, chúng ta lấy &String
thay vì
String
. Các dấu và (&) này đại diện cho tham chiếu, và chúng cho phép bạn
tham chiếu đến một số giá trị mà không lấy quyền sở hữu của nó. Hình 4-6 minh
họa khái niệm này.
Hình 4-6: Một sơ đồ của &String s
trỏ đến
String s1
Lưu ý: Ngược lại với việc tham chiếu bằng cách sử dụng
&
là giải tham chiếu (dereferencing), được thực hiện với toán tử giải tham chiếu,*
. Chúng ta sẽ thấy một số cách sử dụng toán tử giải tham chiếu trong Chương 8 và thảo luận chi tiết về giải tham chiếu trong Chương 15.
Hãy xem xét kỹ hơn cuộc gọi hàm ở đây:
fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); println!("The length of '{s1}' is {len}."); } fn calculate_length(s: &String) -> usize { s.len() }
Cú pháp &s1
cho phép chúng ta tạo một tham chiếu trỏ đến giá trị của s1
nhưng không sở hữu nó. Vì tham chiếu không sở hữu nó, giá trị mà nó trỏ đến sẽ
không bị hủy khi tham chiếu không còn được sử dụng nữa.
Tương tự, chữ ký của hàm sử dụng &
để chỉ ra rằng kiểu của tham số s
là một
tham chiếu. Hãy thêm một số chú thích giải thích:
fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); println!("The length of '{s1}' is {len}."); } fn calculate_length(s: &String) -> usize { // s is a reference to a String s.len() } // Here, s goes out of scope. But because s does not have ownership of what // it refers to, the String is not dropped.
Phạm vi mà biến s
có giá trị giống như phạm vi của bất kỳ tham số hàm nào,
nhưng giá trị được trỏ đến bởi tham chiếu không bị hủy khi s
không còn được sử
dụng nữa, vì s
không có quyền sở hữu. Khi các hàm có tham chiếu làm tham số
thay vì các giá trị thực tế, chúng ta sẽ không cần trả lại các giá trị để trả
lại quyền sở hữu, vì chúng ta chưa bao giờ có quyền sở hữu.
Chúng ta gọi hành động tạo một tham chiếu là mượn (borrowing). Như trong cuộc sống thực, nếu một người sở hữu một thứ gì đó, bạn có thể mượn nó từ họ. Khi bạn xong việc, bạn phải trả lại nó. Bạn không sở hữu nó.
Vậy, điều gì sẽ xảy ra nếu chúng ta cố gắng sửa đổi thứ gì đó mà chúng ta đang mượn? Hãy thử mã trong Listing 4-6. Cảnh báo trước: nó không hoạt động!
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
Đây là lỗi:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
--> src/main.rs:8:5
|
8 | some_string.push_str(", world");
| ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference
|
7 | fn change(some_string: &mut String) {
| +++
For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
Giống như các biến là không thay đổi theo mặc định, các tham chiếu cũng vậy. Chúng ta không được phép sửa đổi một thứ mà chúng ta có tham chiếu đến.
Tham Chiếu Có Thể Thay Đổi
Chúng ta có thể sửa mã từ Listing 4-6 để cho phép chúng ta sửa đổi một giá trị đã mượn chỉ với một vài điều chỉnh nhỏ sử dụng, thay vào đó, một tham chiếu có thể thay đổi (mutable reference):
fn main() { let mut s = String::from("hello"); change(&mut s); } fn change(some_string: &mut String) { some_string.push_str(", world"); }
Đầu tiên, chúng ta thay đổi s
thành mut
. Sau đó, chúng ta tạo một tham chiếu
có thể thay đổi với &mut s
khi chúng ta gọi hàm change
, và cập nhật chữ ký
hàm để chấp nhận một tham chiếu có thể thay đổi với some_string: &mut String
.
Điều này làm cho nó rất rõ ràng rằng hàm change
sẽ thay đổi giá trị mà nó
mượn.
Tham chiếu có thể thay đổi có một hạn chế lớn: nếu bạn có một tham chiếu có thể
thay đổi đến một giá trị, bạn không thể có tham chiếu nào khác đến giá trị đó.
Mã này cố gắng tạo hai tham chiếu có thể thay đổi đến s
sẽ thất bại:
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{r1}, {r2}");
}
Đây là lỗi:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> src/main.rs:5:14
|
4 | let r1 = &mut s;
| ------ first mutable borrow occurs here
5 | let r2 = &mut s;
| ^^^^^^ second mutable borrow occurs here
6 |
7 | println!("{r1}, {r2}");
| ---- first borrow later used here
For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
Lỗi này nói rằng mã này không hợp lệ vì chúng ta không thể mượn s
như một biến
có thể thay đổi nhiều hơn một lần tại một thời điểm. Lần mượn có thể thay đổi
đầu tiên là trong r1
và phải kéo dài cho đến khi nó được sử dụng trong
println!
, nhưng giữa việc tạo tham chiếu có thể thay đổi đó và việc sử dụng
nó, chúng ta đã cố gắng tạo một tham chiếu có thể thay đổi khác trong r2
mà
mượn cùng dữ liệu như r1
.
Hạn chế ngăn nhiều tham chiếu có thể thay đổi đến cùng một dữ liệu tại cùng một thời điểm cho phép thay đổi nhưng theo một cách rất có kiểm soát. Đây là điều mà các lập trình viên Rust mới gặp khó khăn vì hầu hết các ngôn ngữ cho phép bạn thay đổi bất cứ khi nào bạn muốn. Lợi ích của việc có hạn chế này là Rust có thể ngăn chặn đua dữ liệu tại thời điểm biên dịch. Đua dữ liệu (data race) tương tự như một điều kiện đua và xảy ra khi có ba hành vi sau:
- Hai hoặc nhiều con trỏ truy cập cùng một dữ liệu tại cùng một thời điểm.
- Ít nhất một trong các con trỏ đang được sử dụng để ghi vào dữ liệu.
- Không có cơ chế nào đang được sử dụng để đồng bộ hóa việc truy cập vào dữ liệu.
Đua dữ liệu gây ra hành vi không xác định và có thể khó chẩn đoán và sửa chữa khi bạn đang cố gắng theo dõi chúng trong thời gian chạy; Rust ngăn chặn vấn đề này bằng cách từ chối biên dịch mã có đua dữ liệu!
Như mọi khi, chúng ta có thể sử dụng dấu ngoặc nhọn để tạo một phạm vi mới, cho phép nhiều tham chiếu có thể thay đổi, miễn là chúng không đồng thời xuất hiện:
fn main() { let mut s = String::from("hello"); { let r1 = &mut s; } // r1 goes out of scope here, so we can make a new reference with no problems. let r2 = &mut s; }
Rust áp dụng một quy tắc tương tự cho việc kết hợp tham chiếu có thể thay đổi và không thể thay đổi. Mã này dẫn đến lỗi:
fn main() {
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // BIG PROBLEM
println!("{r1}, {r2}, and {r3}");
}
Đây là lỗi:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src/main.rs:6:14
|
4 | let r1 = &s; // no problem
| -- immutable borrow occurs here
5 | let r2 = &s; // no problem
6 | let r3 = &mut s; // BIG PROBLEM
| ^^^^^^ mutable borrow occurs here
7 |
8 | println!("{r1}, {r2}, and {r3}");
| ---- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
Ôi! Chúng ta cũng không thể có một tham chiếu có thể thay đổi trong khi chúng ta có một tham chiếu không thể thay đổi đến cùng một giá trị.
Người dùng của một tham chiếu không thể thay đổi không mong đợi giá trị đột nhiên thay đổi dưới chân họ! Tuy nhiên, nhiều tham chiếu không thể thay đổi được cho phép vì không ai chỉ đọc dữ liệu có khả năng ảnh hưởng đến việc đọc dữ liệu của người khác.
Lưu ý rằng phạm vi của một tham chiếu bắt đầu từ nơi nó được giới thiệu và tiếp
tục thông qua lần cuối cùng tham chiếu đó được sử dụng. Ví dụ, mã này sẽ biên
dịch vì lần sử dụng cuối cùng của các tham chiếu không thể thay đổi là trong
println!
, trước khi tham chiếu có thể thay đổi được giới thiệu:
fn main() { let mut s = String::from("hello"); let r1 = &s; // no problem let r2 = &s; // no problem println!("{r1} and {r2}"); // Variables r1 and r2 will not be used after this point. let r3 = &mut s; // no problem println!("{r3}"); }
Phạm vi của các tham chiếu không thể thay đổi r1
và r2
kết thúc sau
println!
nơi chúng được sử dụng lần cuối, đó là trước khi tham chiếu có thể
thay đổi r3
được tạo. Các phạm vi này không chồng lấn, vì vậy mã này được cho
phép: trình biên dịch có thể biết rằng tham chiếu không còn được sử dụng tại một
điểm trước khi kết thúc phạm vi.
Mặc dù các lỗi mượn có thể gây bực bội đôi khi, hãy nhớ rằng đó là trình biên dịch Rust chỉ ra một lỗi tiềm ẩn sớm (tại thời điểm biên dịch thay vì tại thời điểm chạy) và chỉ cho bạn chính xác nơi vấn đề nằm ở. Sau đó, bạn không phải theo dõi lý do tại sao dữ liệu của bạn không phải là những gì bạn nghĩ nó là.
Tham Chiếu Treo
Trong các ngôn ngữ có con trỏ, rất dễ vô tình tạo ra một con trỏ treo (dangling pointer)—một con trỏ tham chiếu đến một vị trí trong bộ nhớ có thể đã được cấp cho ai đó khác—bằng cách giải phóng một số bộ nhớ trong khi vẫn giữ một con trỏ đến bộ nhớ đó. Ngược lại, trong Rust, trình biên dịch đảm bảo rằng các tham chiếu sẽ không bao giờ là tham chiếu treo: nếu bạn có một tham chiếu đến một số dữ liệu, trình biên dịch sẽ đảm bảo rằng dữ liệu sẽ không ra khỏi phạm vi trước tham chiếu đến dữ liệu đó.
Hãy thử tạo một tham chiếu treo để xem cách Rust ngăn chúng với một lỗi thời điểm biên dịch:
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
Đây là lỗi:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier
--> src/main.rs:5:16
|
5 | fn dangle() -> &String {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
|
5 | fn dangle() -> &'static String {
| +++++++
help: instead, you are more likely to want to return an owned value
|
5 - fn dangle() -> &String {
5 + fn dangle() -> String {
|
error[E0515]: cannot return reference to local variable `s`
--> src/main.rs:8:5
|
8 | &s
| ^^ returns a reference to data owned by the current function
Some errors have detailed explanations: E0106, E0515.
For more information about an error, try `rustc --explain E0106`.
error: could not compile `ownership` (bin "ownership") due to 2 previous errors
Thông báo lỗi này đề cập đến một tính năng mà chúng ta chưa đề cập: thời gian tồn tại (lifetimes). Chúng ta sẽ thảo luận về thời gian tồn tại chi tiết trong Chương 10. Nhưng, nếu bạn bỏ qua các phần về thời gian tồn tại, thông báo có chứa chìa khóa cho lý do tại sao mã này là một vấn đề:
kiểu trả về của hàm này chứa một giá trị đã mượn, nhưng không có giá trị
nào để mượn từ đó
Hãy xem xét kỹ hơn chính xác những gì đang xảy ra ở mỗi giai đoạn của mã
dangle
của chúng ta:
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String { // dangle returns a reference to a String
let s = String::from("hello"); // s is a new String
&s // we return a reference to the String, s
} // Here, s goes out of scope and is dropped, so its memory goes away.
// Danger!
Vì s
được tạo bên trong dangle
, khi mã của dangle
hoàn thành, s
sẽ bị
giải phóng. Nhưng chúng ta đã cố gắng trả về một tham chiếu đến nó. Điều đó có
nghĩa là tham chiếu này sẽ trỏ đến một String
không hợp lệ. Điều đó không tốt!
Rust sẽ không cho phép chúng ta làm điều này.
Giải pháp ở đây là trả về String
trực tiếp:
fn main() { let string = no_dangle(); } fn no_dangle() -> String { let s = String::from("hello"); s }
Điều này hoạt động mà không có vấn đề gì. Quyền sở hữu được chuyển ra ngoài và không có gì bị giải phóng.
Các Quy Tắc của Tham Chiếu
Hãy tổng kết những gì chúng ta đã thảo luận về tham chiếu:
- Tại bất kỳ thời điểm nào, bạn có thể có hoặc là một tham chiếu có thể thay đổi hoặc là bất kỳ số lượng tham chiếu không thể thay đổi nào.
- Tham chiếu phải luôn hợp lệ.
Tiếp theo, chúng ta sẽ xem xét một loại tham chiếu khác: slice.