[Rust Book] 4.1. Tham chiếu và Vay Mượn (References and Borrowing)

References and Borrowing

Vấn đề với đoạn code trong Ví dụ 4-5 là chúng ta phải trả về String cho lời gọi hàm để có thể sử dụng String sau khi gọi calculate_length, bởi vì String đã được move vào calculate_length.

Dưới đây là cách để định nghĩa và sử dụng hàm calculate_length, mà có một tham chiếu tới một đối tượng như một tham số thay vì lấy mất ownership của giá trị đó:

Filename: src/main.rs

fn main() {
  let s1 = String::from("hello");

  let len = calculate_length(&s1);

  println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
  s.len()
}

Đầu tiên, chú ý rằng tuple code nằm trong khai báo biến và giá trị hàm trả về đều sẽ biến mất. Thứ hai, 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.

Những dấu và (&) này là những tham chiếu, chúng cho phép bạn tham chiếu tới giá trị mà không chiếm ownership của nó.

&String s pointing at String s1

Hình 4-5: Biểu đồ của &String s trỏ tới String s1

Chú ý: Ngược lại của tham chiếu bằng cách sử dụng &tham chiếu ngược (dereferencing), đạt được bằng toán tử tham chiếu ngược, *. Chúng ta sẽ xem một vài cách dùng của tham chiếu ngược trong Chương 8 và thảo luận chi tiết về tham chiếu ngược trong Chương 15.

Cùng nhìn kỹ hơn vào lời gọi hàm sau đây:

    let s1 = String::from("hello");

    let len = calculate_length(&s1);

Cú pháp &s1 cho phép chúng ta tạo một tham chiếu mà tham chiếu tới giá trị của s1 nhưng không sở hữu nó. Bởi vì nó không sở hữu giá trị nó trỏ tới, giá trị đó sẽ không bị loại bỏ khi tham chiếu ra khỏi scope.

Tương tự, signature của hàm sử dụng & chỉ ra rằng kiểu của tham số s là một tham chiếu.

fn calculate_length(s: &String) -> usize { // s is a reference to a String
    s.len()
} // Here, s goes out of scope. But because it does not have ownership of what
  // it refers to, it is not dropped.

Scope mà biến s hợp lệ tương tự như scope của tham số trong bất kỳ hàm nào, nhưng chúng ta không loại bỏ giá trị mà tham chiếu trỏ tới khi nó ra khỏi scope vì chúng ta không có ownership. Khi các hàm có tham chiếu như tham số thay vì giá trị thực, chúng ta sẽ không cần trả về giá trị để trả lại ownership, bởi vì chúng chưa từng có ownership.

Chúng ta gọi việc có tham chiếu như tham số hàm là borrowing (mượn). Trong thực tế, nếu một người sở hữu thứ gì đó, bạn có thể mượn nó từ họ. Khi bạn xong, bạn phải trả nó lại.

Vậy điều gì xảy ra nếu chúng ta cố chỉnh sửa những thứ chúng ta đang mượn? Hãy thử đoạn code trong ví dụ 4-6. Bật mí: không chạy được!

Filename: src/main.rs

fn main() {
    let s = String::from("hello");

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}

Ví dụ 4-6: Thử thay đổi giá trị đã mượn

Bạn sẽ gặp lỗi sau:

$ 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
  |
7 | fn change(some_string: &String) {
  |                        ------- help: consider changing this to be a mutable reference: `&mut String`
8 |     some_string.push_str(", world");
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable

For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership` due to previous error

Chỉ là giống như biến, mặc định chúng là immutable, tham chiếu cũng vậy. Chúng ta không được phép thay đổi thứ mà chúng ta tham chiếu tới.

Mutable References

Chúng ta có thể sửa lỗi ở ví dụ 4-6 chỉ với một thay đổi nhỏ, sử dụng mutable reference:

Filename: src/main.rs

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 phải đặt smut. Sau đó chúng ta phải tạo một tham chiếu mutable với &mut s và chấp nhận một tham chiếu mutable trong hàm với some_string: &mut String.

Nhưng những tham chiếu mutable có một hạn chế lớn: bạn chỉ có thể có một tham chiếu mutable tới một phần cụ thể của dữ liệu trong một scope cụ thể. Đoạn code sau đây sẽ không chạy được:

Filename: src/main.rs

    let mut s = String::from("hello");

    let r1 = &mut s;
    let r2 = &mut s;

    println!("{}, {}", r1, r2);

Lỗi sẽ như dưới đây:

$ 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` due to previous error

Hạn chế này cho phép mutation nhưng bị quản lý rất chặt chẽ. Nó cũng là thứ mà hay khiến những Rustacean gặp rắc rối, bởi vờ hầu hết các ngôn ngữ cho phép bạn mutate bất cứ khi nào bạn thích.

Lợi ích của hạn chế này là Rust có thể ngăn ngừa data race từ lúc biên dịch. Một data race tương tự như một race condition và xảy ra khi ba hành vi này xảy ra:

  • Hai hay nhiều con trỏ truy cập tới cùng 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 được sử dụng để đồng bộ truy cập tới dữ liệu.

Data race gây ra những hành vi không đoán trước, và nó có thể sẽ khó tìm ra nguyên nhân khi bạn cố theo dấu chúng khi chạy; Rust ngăn ngừa được vấn đề này bởi vì nó thậm chí sẽ không biên dịch code với data race!

Như thường lệ, chúng ta có thể sử dụng cặp dấu ngoặc nhọn để tạo một scope mới, cho phép nhiều tham chiếu mutable, chỉ là không đồng thời (simultaneous):

    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;

Có một quy tắc tương tự cho việc kết hợp tham chiếu mutable và immutable. Đoạn code này trả ra kết quả là một lỗi:

    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    let r3 = &mut s; // BIG PROBLEM

    println!("{}, {}, and {}", r1, r2, r3);
$ 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!("{}, {}, and {}", r1, r2, r3);
  |                                -- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` due to previous error

Whew! Vậy là chúng ta cũng không thể có một tham chiếu mutable trong khi chúng ta đang có một tham chiếu immutable. Người dùng của một tham chiếu immutable không mong đợi việc những giá trị đột ngột thay đổi phía dưới chúng! Tuy nhiên, nhiều tham chiếu immutable lại được phép vì không ai mà chỉ đọc dữ liệu lại có khả năng ảnh hưởng đến việc đọc dữ liệu của người khác.

Lưu ý rằng scope của một tham chiếu bắt đầu từ nơi nó được giới thiệu và tiếp tục chạy qua đến lần cuối tham chiếu đó được sử dụng. Ví dụ, đoạn code này sẽ biên dịch được vì lần sử dụng cuối của những tham chiếu immutable xảy ra trước khi tham chiếu mutable được giới thiệu:

    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    println!("{} and {}", r1, r2);
    // variables r1 and r2 will not be used after this point

    let r3 = &mut s; // no problem
    println!("{}", r3);

Những scope của các tham chiếu immutable r1r2 kết thúc sau println!, nơi mà lần cuối chúng được sử dụng, trước khi tham chiếu mutable r3 được tạo. Những scope này không đè lên nhau, do đó đoạn code này được cho phép.

Thậm chí mặc dù lỗi borrow có thể khá phiền phức, hãy nhớ rằng đó là trình biên dịch của Rust đang chỉ ra những bug tiềm tàng sớm (tại thời điểm biên dịch thay vì lúc chạy) và cho bạn thấy chính xác vấn đề ở đâu. Sau đó bạn không cần phải theo dấu tại sao dữ liệu của bạn lại không đúng như cái mà bạn nghĩ.

Dangling References

Trong những ngôn ngữ với con trỏ, rất dễ để nhầm lẫn tạo ra một dangling pointer, một con trỏ tham chiếu tới một địa chỉ trong bộ nhớ mà đã được đưa cho ai đó khác, bằng việc giải phóng một vài bộ nhớ trong khi vẫn duy trì một con trỏ tới bộ nhớ đó. Trong Rust, ngược lại, trình biên dịch bảo đảm rằng tham chiếu sẽ không bao giờ là dangling reference: nếu bạn có một tham chiếu thới dữ liệu nào đó, trình biên dịch sẽ chắc chắn rằng dữ liệu đó sẽ không ra khỏi scope trước tham chiếu của nó.

Hãy cùng thử tạo một dangling reference, thứ mà Rust sẽ ngăn ngừa nó với một lỗi biên dịch:

Filename: src/main.rs

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    let s = String::from("hello");

    &s
}
$ 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
  |
5 | fn dangle() -> &'static String {
  |                ~~~~~~~~

For more information about this error, try `rustc --explain E0106`.
error: could not compile `ownership` due to previous error

Lỗi này đề cập đến một tính năng mà chúng ta vẫn chưa bàn đến: lifetimes. Chúng ta sẽ thảo luận chi tiết về lifetimes trong Chương 10. Nhưng nếu bạn bỏ qua phần về lifetimes, đoạn lỗi có chứa lý do chính tại sao đoạn code này lại có vấn đề:

this function's return type contains a borrowed value, but there is no value
for it to be borrowed from.

Hãy cùng nhìn sâu hơn chính xác thì điều gì đang xảy ra ở mỗi giai đoạn của đoạn dangle code:

Filename: src/main.rs

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. Its memory goes away.
  // Danger!

Bởi vì s được tạo bên trong dangle, khi đoạn code của dangle được hoàn thành, s sẽ bị hủy cấp phát (deallocated). Nhưng chúng ta đã cố để trả về một tham chiếu tới nó. Điều đó nghĩa là tham chiếu sẽ trỏ tới một String không hợp lệ. Rust sẽ không cho phép chúng ta làm điều đó.

Giải pháp ở đây là trực tiếp trả về String:

fn no_dangle() -> String {
    let s = String::from("hello");

    s
}

Đoạn code sẽ hoạt động mà không có vấn đề gì. Ownership được chuyển ra ngoài và không có thứ gì bị hủy cấp phát.

Những quy tắc của Tham chiếu

Hãy cùng tóm tắt lại 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 cho trước, bạn có thể có hoặc một tham chiếu mutable hoặc nhiều tham chiếu immutable.
  • Tham chiếu phải luôn hợp lệ.

Tiếp theo, chúng ta sẽ cùng xem một loại khác của tham chiếu: slices.

Source

https://doc.rust-lang.org/stable/book/ch04-02-references-and-borrowing.html

Bài liên quan

comments powered by Disqus