Tham chiếu và Mượn (References and Borrowing)
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 để vẫn có thể sử dụng String
sau khi gọi calculate_length
, bởi vì String
đã bị chuyển quyền sở hữu vào calculate_length
. Thay vào đó, chúng ta có thể cung cấp một tham chiếu tới giá trị String
. Một tham chiếu giống như một con trỏ ở chỗ nó là một địa chỉ mà chúng ta có thể theo để truy cập dữ liệu được lưu tại địa chỉ đó; dữ liệu đó được sở hữu bởi một biến khác. Khác với con trỏ, một tham chiếu được đảm bảo sẽ trỏ tới một giá trị hợp lệ của một kiểu cụ thể trong suốt vòng đời của tham chiếu đó.
Đây là cách bạn định nghĩa và sử dụng hàm calculate_length
nhận tham chiếu tới một đối tượng làm tham số thay vì lấy quyền sở hữu 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, hãy chú ý rằng toàn bộ 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 nhận &String
thay vì String
. Những dấu &
này đại diện cho tham chiếu, và chúng cho phép bạn tham chiếu tới một giá trị mà không lấy quyền sở hữu nó. Hình 4-6 minh họa khái niệm này.
Hình 4-6: Sơ đồ &String s
trỏ tới String s1
Lưu ý: Đối lập với việc tham chiếu bằng cách sử dụng
&
là giải tham chiếu, được thực hiện với toán tử giải tham chiếu,*
. Chúng ta sẽ thấy một số ví dụ về toán tử này ở Chương 8 và bàn về chi tiết giải tham chiếu ở Chương 15.
Hãy xem kỹ hơn về lời 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 tham chiếu tới giá trị của s1
nhưng không sở hữu nó. Vì tham chiếu không sở hữu giá trị, nên giá trị mà nó trỏ tới sẽ không bị hủy khi tham chiếu ngừng được sử dụng.
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
hợp lệ giống như phạm vi của bất kỳ tham số hàm nào, nhưng giá trị được tham chiếu bởi tham chiếu sẽ không bị hủy khi s
ngừng được sử dụng, bởi vì s
không có quyền sở hữu. Khi các hàm nhận tham chiếu làm tham số thay vì giá trị thực, chúng ta sẽ không cần trả lại giá trị để chuyển lại quyền sở hữu, bởi vì chúng ta chưa từng sở hữu nó.
Hành động tạo một tham chiếu được gọi là mượn. Giống như trong đời thực, nếu một người sở hữu thứ gì đó, bạn có thể mượn nó từ họ. Khi bạn xong việc, bạn phải trả lại. Bạn không sở hữu nó.
Vậy điều gì xảy ra nếu chúng ta cố gắng sửa đổi thứ mà chúng ta đang mượn? Hãy thử đoạn mã trong Listing 4-6. Bật mí: 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
Cũng giống như biến mặc định là bất biến, tham chiếu cũng vậy. Chúng ta không được phép sửa đổi thứ mà chúng ta chỉ có tham chiếu tới.
Tham chiếu có thể thay đổi
Chúng ta có thể sửa đoạn mã từ Listing 4-6 để cho phép sửa đổi giá trị được mượn chỉ với một vài thay đổi nhỏ bằng cách sử dụng tham chiếu có thể thay đổi:
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 đổ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 gọi hàm change
, và cập nhật chữ ký hàm để nhận tham chiếu có thể thay đổi với some_string: &mut String
. Điều này làm rõ 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 tới một giá trị, bạn không thể có bất kỳ tham chiếu nào khác tới giá trị đó. Đoạn mã này cố gắng tạo hai tham chiếu có thể thay đổi tới s
sẽ bị lỗ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 đoạn mã này không hợp lệ vì chúng ta không thể mượn s
dưới dạng có thể thay đổi nhiều hơn một lần cùng lúc. Lần mượn có thể thay đổi đầu tiên là ở r1
và phải kéo dài cho tới khi nó được sử dụng trong println!
, nhưng giữa lúc tạo tham chiếu có thể thay đổi đó và lúc sử dụng nó, chúng ta đã cố gắng tạo một tham chiếu có thể thay đổi khác ở r2
mượn cùng dữ liệu với r1
.
Hạn chế ngăn chặn nhiều tham chiếu có thể thay đổi tới cùng dữ liệu cùng lúc cho phép sửa đổi nhưng theo cách rất kiểm soát. Đây là điều mà người mới học Rust thường gặp khó khăn vì hầu hết các ngôn ngữ cho phép bạn sửa đổi bất cứ khi nào bạn muốn. Lợi ích của hạn chế này là Rust có thể ngăn chặn các lỗi tranh chấp dữ liệu ngay tại thời điểm biên dịch. Một tranh chấp dữ liệu giống như một điều kiện tranh chấp và xảy ra khi ba hành vi sau xuất hiện:
- Hai hoặc nhiều con trỏ truy cập cùng dữ liệu cùng lúc.
- Ít nhất một con trỏ được sử dụng để ghi dữ liệu.
- Không có cơ chế nào được sử dụng để đồng bộ hóa việc truy cập dữ liệu.
Tranh chấp dữ liệu gây ra hành vi không xác định và có thể rất khó phát hiện và sửa khi bạn cố gắng tìm lỗi lúc chạy; Rust ngăn chặn vấn đề này bằng cách từ chối biên dịch mã có tranh chấp dữ liệu!
Như thường lệ, 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, chỉ là không đồng thời:
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 cũng áp dụng quy tắc tương tự khi kết hợp tham chiếu có thể thay đổi và tham chiếu bất biến. Đoạn mã này sẽ gây 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
Chà! Chúng ta cũng không thể có một tham chiếu có thể thay đổi trong khi đang có một tham chiếu bất biến tới cùng giá trị.
Người dùng của tham chiếu bất biến không mong đợi giá trị bị thay đổi bất ngờ! Tuy nhiên, nhiều tham chiếu bất biến được phép vì không ai chỉ đọc dữ liệu có thể ảnh hưởng tới việc đọc 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à kéo dài tới lần cuối cùng tham chiếu đó được sử dụng. Ví dụ, đoạn 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 bất biến là trong println!
, trước khi tham chiếu có thể thay đổi được tạo ra:
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 bất biến r1
và r2
kết thúc sau println!
nơi chúng được sử dụng lần cuối, trước khi tham chiếu có thể thay đổi r3
được tạo ra. Các phạm vi này không giao nhau, nên đoạn mã này được phép: trình biên dịch có thể xác định 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.
Dù đôi khi lỗi mượn có thể gây khó chịu, 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ì lúc chạy) và chỉ rõ chính xác vị trí vấn đề. Nhờ vậy bạn không phải mất công tìm hiểu tại sao dữ liệu của mình không như mong đợi.
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—một con trỏ tham chiếu tới một vị trí bộ nhớ có thể đã được cấp phát cho ai đó khác—bằng cách giải phóng bộ nhớ trong khi vẫn giữ một con trỏ tới bộ nhớ đó. Ngược lại, trong Rust, trình biên dịch đảm bảo rằng tham chiếu sẽ không bao giờ là tham chiếu treo: nếu bạn có một tham chiếu tới dữ liệu nào đó, trình biên dịch sẽ đảm bảo rằng dữ liệu đó sẽ không bị ra khỏi phạm vi trước khi tham chiếu tới dữ liệu đó kết thúc.
Hãy thử tạo một tham chiếu treo để xem Rust ngăn chặn chúng bằng lỗi biên dịch như thế nào:
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 tới một tính năng mà chúng ta chưa học: vòng đời (lifetimes). Chúng ta sẽ bàn về vòng đời chi tiết ở Chương 10. Nhưng nếu bạn bỏ qua phần về vòng đời, thông báo vẫn chứa ý chính về lý do tại sao đoạn mã này có vấn đề:
kiểu trả về của hàm này chứa một giá trị được mượn, nhưng không có giá trị nào để mượn từ đó
Hãy xem kỹ hơn từng bước của đoạn mã dangle
:
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
kết thúc, s
sẽ bị giải phóng. Nhưng chúng ta lại cố trả về một tham chiếu tới nó. Điều đó nghĩa là tham chiếu này sẽ trỏ tới một String
không hợp lệ. Điều này không ổn! Rust sẽ không cho phép chúng ta làm vậy.
Giải pháp ở đây là trả về trực tiếp String
:
fn main() { let string = no_dangle(); } fn no_dangle() -> String { let s = String::from("hello"); s }
Cách này hoạt động mà không gặp 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 lại những gì chúng ta đã học về tham chiếu:
- Tại một thời điểm, bạn chỉ có hoặc một tham chiếu có thể thay đổi hoặc bất kỳ số lượng tham chiếu bất biến nào.
- Tham chiếu luôn phải hợp lệ.
Tiếp theo, chúng ta sẽ tìm hiểu về một loại tham chiếu khác: slice.