References và Borrowing trong Rust
Bài đăng này đã không được cập nhật trong 2 năm
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 Stack
và Heap
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 s
mà s1
đ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,s
làowner
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ủas
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ả r1
và r2
đề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: r1
và r2
đang tham chiếu đến s
mà r3
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 r1
và r2
đã 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