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.

Ba bảng: bảng cho s chỉ chứa một con trỏ đến bảng
cho s1. Bảng cho s1 chứa dữ liệu stack cho s1 và trỏ đến
dữ liệu chuỗi trên heap.

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 &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 r1r2 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!

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.