Trong Rust, con trỏ có thể được phân loại theo cách quản lý ownership. Dựa vào yếu tố đó ta có thể chia làm 2 loại:
- Owning Pointers: Là loại con trỏ mà khi bị hủy, dữ liệu mà nó trỏ đến cũng sẽ được giải phóng. Tiêu biểu như
String
,Box<T>
,Rc
,Arc
,Vec
... - Non-Owning Pointers: Là loại con trỏ không sở hữu dữ liệu trỏ đến. Chúng chỉ là các tham chiếu và không chịu trách nhiệm về việc quản lý vòng đời của dữ liệu đó.
Rust gọi các con trỏ Non-Owning Pointers với cái tên ngắn gọn hơn là References, chủ đề mà chúng ta sẽ cùng tìm hiểu ngày hôm nay.
Tham chiếu tới giá trị (References to Values)
use std::collections::HashMap;
type Table = HashMap<String, Vec<String>>; fn show(table: Table) { for (artist, works) in table { println!("works by {}:", artist); for work in works { println!(" {}", work); } }
} fn main() { let mut table = Table::new(); table.insert( "Gesualdo".to_string(), vec![ "many madrigals".to_string(), "Tenebrae Responsoria".to_string(), ], ); table.insert( "Caravaggio".to_string(), vec![ "The Musicians".to_string(), "The Calling of St. Matthew".to_string(), ], ); table.insert( "Cellini".to_string(), vec![ "Perseus with the head of Medusa".to_string(), "a salt cellar".to_string(), ], ); show(table); // chuyển quyền sở hữu table cho hàm show assert_eq!(table["Gesualdo"][0], "many madrigals"); // Báo lỗi: borrow of moved value: `table`
}
Hàm show
chỉ có nhiệm vụ khá đơn giản in ra màn hình, với cách gọi như trên thì chúng ta sẽ không thể tương tác tiếp với table
sau khi gọi hàm show
=> vô cùng bất tiện 😤
Chúng ta sẽ sử dụng tham chiếu (references) để giải quyết vấn đề trên.
fn show(table: &Table) { // thay đổi kiểu dữ liệu của truyền vào thành dạng tham chiếu for (artist, works) in table { println!("works by {}:", artist); for work in works { println!(" {}", work); } }
} fn main() { // ... show(&table); // Truyền tham chiếu assert_eq!(table["Gesualdo"][0], "many madrigals");
}
Tham chiếu lại được chia thành 2 kiểu:
- Shared Reference (&T): Là một tham chiếu bất biến (immutable) đến dữ liệu. Nó cho phép bạn đọc dữ liệu nhưng không thể thay đổi nó.
- Mutable Reference (&mut T): Là một tham chiếu có thể thay đổi dữ liệu. Chúng ta có quyền thay đổi giá trị mà nó trỏ đến.
// ví dụ cho Mutable Reference
fn sort_works(table: &mut Table) { for (_artist, works) in table { works.sort(); }
} sort_works(&mut table);
Ví dụ khi sử dụng tham chiếu
Tham chiếu Rust so với tham chiếu của C/C++
Trong C++, tham chiếu được tạo một cách ngầm định thông qua ép kiểu, và việc giải tham chiếu (dereferenced) cũng được thực hiện một cách ngầm định.
Trong Rust, tham chiếu được tạo ra một cách tường minh bằng toán tử &
, và việc dereferenced được sử dụng một cách tường minh bằng toán tử *
:
// C++
int x = 10;
int &r = x; // Ép kiểu x thành dạng tham chiếu
assert(r == 10); // ngầm định lấy ra giá trị mà tham chiếu trỏ đến
r = 20; // thay giá trị của x thông qua r
// Rust
let x = 10;
let r = &x; // Khởi tạo biến r là 1 tham chiếu đến x assert!(*r == 10); // Lấy giá trị mà r tham chiếu trỏ tới bằng ký tự *
Nếu muốn sửa giá trị qua tham chiếu thì phải thêm mut
, chặt chẽ hơn nhiều so với C/C++
let mut y = 32;
let m = &mut y; // biến m là tham chiếu dạng mut tới y
*m += 32; // tăng giá trị của y thêm 32 đơn vị
assert!(*m == 64);
Tuy nhiên, với toán tử .
thì Rust sẽ ngầm định lấy ra giá trị tham chiếu tới. Cùng xem qua các ví dụ dưới đây
fn main() { struct Anime { name: &'static str, bechdel_pass: bool, }; let aria = Anime { name: "Aria: The Animation", bechdel_pass: true, }; let anime_ref = &aria; // lấy giá trị name ra với tham chiếu anime_ref assert_eq!((*anime_ref).name, "Aria: The Animation"); // Cũng là lấy ra giá trị name, nhưng ngắn gọn hơn assert_eq!(anime_ref.name, "Aria: The Animation");
}
let mut v = vec![1973, 1968];
(&mut v).sort(); // tương đương cách trên nhưng ngắn gọn hơn
v.sort();
Tham chiếu trỏ tới tham chiếu
struct Point { x: i32, y: i32 } let point = Point { x: 1000, y: 729 }; let r: &Point = &point;
let rr: &&Point = &r;
let rrr: &&&Point = &rr; // Mặc dù trỏ qua nhiều tham chiếu trung gian
// nhưng khi dùng toán tử . thì Rust sẽ hỗ trợ truy tới giá trị đích luôn
assert_eq!(rrr.y, 729);
So sánh tham chiếu
fn main() { let x = 10; let y = 10; let rx = &x; let ry = &y; let rrx = ℞ let rry = &ry; // So sánh giá trị mà tham chiếu trỏ tới println!("{}", rrx <= rry); // true (10 <= 10) println!("{}", rrx == rry); // true (10 == 10) println!("{}", rx == ry); // true (10 == 10) // so sánh 2 con trỏ có cùng trỏ đến 1 vùng nhớ không ? println!("{}", std::ptr::eq(rx, ry)); // false
}
Con Trỏ Null ?
Trong Rust, khái niệm về con trỏ NULL
không tồn tại. Thay vào đó, ngôn ngữ sử dụng kiểu dữ liệu Option<T>
để xử lý các trường hợp mà một giá trị có thể không tồn tại. Điều này giúp loại bỏ hoàn toàn các vấn đề liên quan đến con trỏ NULL, như lỗi truy cập bộ nhớ không xác định hoặc con trỏ lơ lửng (dangling pointers) rất hay gặp trong các ngôn ngữ lập trình khác như C/C++.
Bằng cách không cho phép giá trị NULL
, Rust buộc lập trình viên phải xử lý tất cả các trường hợp có thể xảy ra, từ đó cải thiện tính ổn định và độ tin cậy của ứng dụng.
fn find_value(list: &Vec<i32>, target: i32) -> Option<&i32> { for &item in list { if item == target { return Some(&item); // Trả về giá trị nếu tìm thấy } } None // Không tìm thấy
} fn main() { let numbers = vec![10, 20, 30, 40]; let target_value = 25; match find_value(&numbers, target_value) { Some(result) => println!("Found value: {}", result), None => println!("Value not found."), }
}
Tài liệu tham khảo
https://www.oreilly.com/library/view/programming-rust-2nd/9781492052586/