+6

References và Borrowing trong Rust

Tiếp về vấn đề Ownership trong Rust, nhưng đã nói, sẽ rất mất công khi chúng ta muốn một hàm nhận tham số đầu vào là một biến kiểu String mà lại muốn bảo toàn Ownership cho biến đó, thật may là Rust đã giải quyết vấn đề này bằng References and Borrowing.

Immutable reference

Xét ví dụ sau đây:

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()
}

Ở đây có ký & là biểu thị phép tham chiếu (Reference), ngoài ra trong Rust còn có * là biểu thị cho Dereference (cái từ này không biết dịch là gì, phản tham chiếu hả? Nghe ổn hông).

Minh hoạ các biến của đoạn code này trên StackHeap như sau:

Kiến thức cũ về String thì bộ 3 ptr len capacity của s1 được lưu trên Stack, giá trị "hello" được lưu trên Heap chắc vẫn nhớ ha.

Còn đối với s, hiểu nôm na là trước khi chạy logic của hàm calculate_lenght() ta có một bước s = &s1, nghĩa là s tham chiếu (reference) đến s1, mà để ghi nhớ việc tham chiếu đó thì chúng ta cần lưu lại giá trị con trỏ trỏ đến s1 cho s, mà kích thước cần dùng để lưu một ptr là cố định cho nên s cũng được lưu trên Stack.

Phép tham chiếu không chuyển ownership của giá "hello" từ s1 sang cho s, chỉ đơn giản là s đang tham chiếu đến ss1 đang sở hữu "hello" nên có thể hiểu là s đang mang giá trị "hello".

Do đó, sau khi kết thúc hàm calculate_length(), nghĩa là s đã ra khỏi scope nó, s sẽ bị drop nhưng sẽ không ảnh hưởng gì đến ownership của s1 đối với "hello" nên lệnh println!("The length of '{}' is {}.", s1, len) hoàn toàn hợp lệ.

Khi ta muốn đọc giá trị của s, Compiler sẽ xác định được nó là một tham chiếu nên sẽ truy đến tận cùng giá trị thật đang nằm trên Heap. Do đó, khi gọi pritnln!("{}", s); nó sẽ in ra "hello" chứ không phải in ra thông tin dạng {ptr: xxx, len: 5, capacity: 5}.

Đoạn này có ai thắc mắc là tại sao cùng lưu trên Stack nhưng Compiler lại biết được s là một tham chiếu, còn s1 là một biến đang sở hữu một giá trị trên Heap không? Mình đoán là do khi nhìn thấy cụm ptr len capacity đi với nhau thì biết ngay là một biến đang sở hữu một giá trị, còn khi chỉ nhìn thấy mỗi ptr thì biết nó là tham chiếu.

Mutable reference

Ở trên chỉ là Immutable reference, nghĩa là tham chiếu đến một giá trị và chỉ có quyền đọc giá trị đấy, không có quyền sửa đổi, vậy muốn sửa đổi thì làm thế nào.

Xét ví dụ sau:

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

    change(&s);
}

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

Lỗi liền nha:

$ 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

Như đã nói ở bài trước, các biến trong Rust mặc định immutable (bất biến), lạ ha, biến mà lại bất biến, muốn một biến khả biến thì phải thêm mut.

Đoạn code sau hoàn hợp lệ:

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

    change(&mut s);
    
    println!("{}", s) // -> hello, world
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}
  • Đầu tiên s được khai báo, sowner của giá trị hello
  • Gọi hàm change() với &mut s: lúc này hiểu là some_string = &mut s, giá trị mà some_string mang là hello
  • some_string.push_str(", world"): push thêm ", world" và giá trị mà some_string đang mang, lúc này thành "hello, world"
  • Tuy nhiên giá trị vừa được cập nhật đó vốn luôn thuộc về s, cho nên khi kết thúc hàm thì some_string bị drop nhưng "hello, world" vẫn là giá trị của s

Nguyên tắc khi dùng reference

Có một nguyên tắc duy nhất là: khi đang có một tham chiếu &mut đang tồn tại thì không một tham chiếu nào khác kể cả &mut hay & được phép tồn tại.

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

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

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

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

Lỗi: Khi cả r1r2 đều muốn thay đổi giá trị của s thì s biết nghe đứa nào.

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!("{}, {}, and {}", r1, r2, r3);
}

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

Lỗi: r1r2 đang tham chiếu đến sr3 lại thay đổi giá trị của s thì r1, r2 biết đường nào mà lần

Nhưng nếu sửa thành:

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

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

    println!("{}", r3);
}

thì chương trình hoàn toàn chạy được, do Compiler xác định được r1r2 đã không còn được dùng ở bất cứ đâu kể từ dòng khai báo của r3.

Cũng tương tự, đoạn code sau hoàn toàn hợp lệ:

fn main() {
    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);
}

Dangling References

Buồn ngủ quá mấy bồ ơi, mai viết tiếp, G9!

Dậy rồi nè!

Dangling references được định nghĩa là một tham chiếu đến một giá trị thuộc sở hữu của một biến khác nhưng biến đấy đã ra khỏi scope của nó và bị drop, từ đó dẫn đến việc tham chiếu đến hư vô.

Ví dụ về dangling referecens như sau:

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

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

    &s
}

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

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

Trong hàm dangle(), biến s được khai báo và sở hữu giá trị "hello" lưu trên Heap , nhưng khi kết thúc hàm dangle(), trả về một tham chiếu đến s trong khi s đã bị drop, do đó mã sẽ tạo ra một dangling reference.

Ở đây có đề xuất cách fix là dùng &'static, điều này liên quan đến chủ đề lifetime trong Rust, mình sẽ viết bài sau.

Bye!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí