Rc<T>
, Con Trỏ Thông Minh Đếm Tham Chiếu
Trong đa số trường hợp, quyền sở hữu (ownership) rất rõ ràng: bạn biết chính xác biến nào sở hữu một giá trị cụ thể. Tuy nhiên, có những trường hợp một giá trị có thể có nhiều chủ sở hữu. Ví dụ, trong cấu trúc dữ liệu đồ thị, nhiều cạnh có thể trỏ đến cùng một nút, và nút đó về mặt khái niệm được sở hữu bởi tất cả các cạnh trỏ đến nó. Một nút không nên được dọn dẹp trừ khi nó không có bất kỳ cạnh nào trỏ đến và do đó không có chủ sở hữu nào.
Bạn phải kích hoạt quyền sở hữu đa chủ một cách rõ ràng bằng cách sử dụng kiểu
Rust Rc<T>
, viết tắt của reference counting (đếm tham chiếu). Kiểu Rc<T>
theo dõi số lượng tham chiếu đến một giá trị để xác định liệu giá trị đó có còn
đang được sử dụng hay không. Nếu không có tham chiếu nào đến một giá trị, giá
trị có thể được dọn dẹp mà không làm mất hiệu lực bất kỳ tham chiếu nào.
Hãy tưởng tượng Rc<T>
như một chiếc TV trong phòng khách gia đình. Khi một
người vào xem TV, họ bật nó lên. Những người khác có thể vào phòng và xem TV.
Khi người cuối cùng rời khỏi phòng, họ tắt TV vì nó không còn được sử dụng nữa.
Nếu ai đó tắt TV trong khi những người khác vẫn đang xem, sẽ có sự phản đối từ
những người xem TV còn lại!
Chúng ta sử dụng kiểu Rc<T>
khi muốn cấp phát dữ liệu trên heap cho nhiều phần
của chương trình đọc và chúng ta không thể xác định tại thời điểm biên dịch phần
nào sẽ kết thúc việc sử dụng dữ liệu cuối cùng. Nếu chúng ta biết phần nào sẽ
kết thúc cuối cùng, chúng ta có thể đơn giản làm cho phần đó trở thành chủ sở
hữu của dữ liệu, và các quy tắc sở hữu thông thường được thực thi tại thời điểm
biên dịch sẽ có hiệu lực.
Lưu ý rằng Rc<T>
chỉ dùng trong các tình huống đơn luồng. Khi chúng ta thảo
luận về đồng thời trong Chương 16, chúng ta sẽ đề cập đến cách thực hiện đếm
tham chiếu trong các chương trình đa luồng.
Sử dụng Rc<T>
để Chia Sẻ Dữ Liệu
Hãy quay lại ví dụ về danh sách cons trong Listing 15-5. Nhớ rằng chúng ta đã
định nghĩa nó bằng Box<T>
. Lần này, chúng ta sẽ tạo hai danh sách đều chia sẻ
quyền sở hữu của một danh sách thứ ba. Về mặt khái niệm, điều này trông giống
như Hình 15-3.
Hình 15-3: Hai danh sách, b
và c
, chia sẻ quyền sở hữu
của danh sách thứ ba, a
Chúng ta sẽ tạo danh sách a
chứa 5
và sau đó là 10
. Sau đó, chúng ta sẽ
tạo thêm hai danh sách nữa: b
bắt đầu bằng 3
và c
bắt đầu bằng 4
. Cả hai
danh sách b
và c
sẽ tiếp tục với danh sách a
đầu tiên chứa 5
và 10
.
Nói cách khác, cả hai danh sách sẽ chia sẻ danh sách đầu tiên chứa 5
và 10
.
Việc cố gắng triển khai kịch bản này bằng định nghĩa List
với Box<T>
sẽ
không hoạt động, như được hiển thị trong Listing 15-17:
enum List {
Cons(i32, Box<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
let b = Cons(3, Box::new(a));
let c = Cons(4, Box::new(a));
}
Khi chúng ta biên dịch mã này, chúng ta nhận được lỗi này:
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0382]: use of moved value: `a`
--> src/main.rs:11:30
|
9 | let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
| - move occurs because `a` has type `List`, which does not implement the `Copy` trait
10 | let b = Cons(3, Box::new(a));
| - value moved here
11 | let c = Cons(4, Box::new(a));
| ^ value used here after move
For more information about this error, try `rustc --explain E0382`.
error: could not compile `cons-list` (bin "cons-list") due to 1 previous error
Các biến thể Cons
sở hữu dữ liệu mà chúng nắm giữ, vì vậy khi chúng ta tạo
danh sách b
, a
được chuyển vào b
và b
sở hữu a
. Sau đó, khi chúng ta
cố gắng sử dụng a
lần nữa khi tạo c
, chúng ta không được phép vì a
đã bị
chuyển đi.
Chúng ta có thể thay đổi định nghĩa của Cons
để nắm giữ tham chiếu thay thế,
nhưng sau đó chúng ta sẽ phải chỉ định các tham số thời gian sống. Bằng cách chỉ
định tham số thời gian sống, chúng ta sẽ chỉ định rằng mọi phần tử trong danh
sách sẽ tồn tại ít nhất lâu bằng toàn bộ danh sách. Đây là trường hợp cho các
phần tử và danh sách trong Listing 15-17, nhưng không phải trong mọi tình huống.
Thay vào đó, chúng ta sẽ thay đổi định nghĩa của List
để sử dụng Rc<T>
thay
vì Box<T>
, như được hiển thị trong Listing 15-18. Mỗi biến thể Cons
bây giờ
sẽ chứa một giá trị và một Rc<T>
trỏ đến một List
. Khi chúng ta tạo b
,
thay vì lấy quyền sở hữu của a
, chúng ta sẽ sao chép Rc<List>
mà a
đang
nắm giữ, do đó tăng số lượng tham chiếu từ một lên hai và cho phép a
và b
chia sẻ quyền sở hữu dữ liệu trong Rc<List>
đó. Chúng ta cũng sẽ sao chép a
khi tạo c
, tăng số lượng tham chiếu từ hai lên ba. Mỗi lần chúng ta gọi
Rc::clone
, số lượng tham chiếu đến dữ liệu trong Rc<List>
sẽ tăng lên, và dữ
liệu sẽ không được dọn dẹp trừ khi không có tham chiếu nào đến nó.
enum List { Cons(i32, Rc<List>), Nil, } use crate::List::{Cons, Nil}; use std::rc::Rc; fn main() { let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); let b = Cons(3, Rc::clone(&a)); let c = Cons(4, Rc::clone(&a)); }
Chúng ta cần thêm câu lệnh use
để đưa Rc<T>
vào phạm vi vì nó không có trong
prelude. Trong main
, chúng ta tạo danh sách chứa 5 và 10 và lưu trữ nó trong
một Rc<List>
mới trong a
. Sau đó, khi chúng ta tạo b
và c
, chúng ta gọi
hàm Rc::clone
và truyền một tham chiếu đến Rc<List>
trong a
làm đối số.
Chúng ta có thể đã gọi a.clone()
thay vì Rc::clone(&a)
, nhưng quy ước của
Rust là sử dụng Rc::clone
trong trường hợp này. Việc triển khai Rc::clone
không tạo một bản sao sâu của tất cả dữ liệu như hầu hết các triển khai clone
của các kiểu khác. Lệnh gọi đến Rc::clone
chỉ tăng số lượng tham chiếu, điều
này không tốn nhiều thời gian. Việc sao chép sâu dữ liệu có thể tốn nhiều thời
gian. Bằng cách sử dụng Rc::clone
cho việc đếm tham chiếu, chúng ta có thể
phân biệt trực quan giữa các loại bản sao sâu và các loại bản sao tăng số lượng
tham chiếu. Khi tìm kiếm vấn đề về hiệu suất trong mã, chúng ta chỉ cần xem xét
các bản sao sâu và có thể bỏ qua các lệnh gọi đến Rc::clone
.
Sao Chép một Rc<T>
Tăng Số Lượng Tham Chiếu
Hãy thay đổi ví dụ đang hoạt động của chúng ta trong Listing 15-18 để chúng ta
có thể thấy số lượng tham chiếu thay đổi khi chúng ta tạo và loại bỏ các tham
chiếu đến Rc<List>
trong a
.
Trong Listing 15-19, chúng ta sẽ thay đổi main
để nó có một phạm vi bên trong
xung quanh danh sách c
; sau đó chúng ta có thể thấy số lượng tham chiếu thay
đổi như thế nào khi c
ra khỏi phạm vi.
enum List { Cons(i32, Rc<List>), Nil, } use crate::List::{Cons, Nil}; use std::rc::Rc; fn main() { let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); println!("count after creating a = {}", Rc::strong_count(&a)); let b = Cons(3, Rc::clone(&a)); println!("count after creating b = {}", Rc::strong_count(&a)); { let c = Cons(4, Rc::clone(&a)); println!("count after creating c = {}", Rc::strong_count(&a)); } println!("count after c goes out of scope = {}", Rc::strong_count(&a)); }
Tại mỗi điểm trong chương trình khi số lượng tham chiếu thay đổi, chúng ta in ra
số lượng tham chiếu, mà chúng ta nhận được bằng cách gọi hàm Rc::strong_count
.
Hàm này được đặt tên là strong_count
thay vì count
vì kiểu Rc<T>
cũng có
weak_count
; chúng ta sẽ xem weak_count
được sử dụng để làm gì trong "Ngăn
chặn Chu kỳ Tham chiếu Bằng cách Sử dụng
Weak<T>
".
Mã này in ra như sau:
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.45s
Running `target/debug/cons-list`
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2
Chúng ta có thể thấy rằng Rc<List>
trong a
có số lượng tham chiếu ban đầu là
1; sau đó mỗi lần chúng ta gọi clone
, số lượng tăng lên 1. Khi c
ra khỏi
phạm vi, số lượng giảm đi 1. Chúng ta không phải gọi một hàm để giảm số lượng
tham chiếu như chúng ta phải gọi Rc::clone
để tăng số lượng tham chiếu: việc
triển khai của trait Drop
tự động giảm số lượng tham chiếu khi một giá trị
Rc<T>
ra khỏi phạm vi.
Điều chúng ta không thể thấy trong ví dụ này là khi b
và sau đó a
ra khỏi
phạm vi ở cuối main
, số lượng lúc đó là 0, và Rc<List>
được dọn dẹp hoàn
toàn. Sử dụng Rc<T>
cho phép một giá trị có nhiều chủ sở hữu, và số lượng đảm
bảo rằng giá trị vẫn hợp lệ miễn là bất kỳ chủ sở hữu nào vẫn còn tồn tại.
Thông qua các tham chiếu bất biến, Rc<T>
cho phép bạn chia sẻ dữ liệu giữa
nhiều phần của chương trình để chỉ đọc. Nếu Rc<T>
cho phép bạn có nhiều tham
chiếu thay đổi cũng vậy, bạn có thể vi phạm một trong các quy tắc mượn đã thảo
luận trong Chương 4: nhiều lần mượn thay đổi đến cùng một nơi có thể gây ra các
cuộc đua dữ liệu và sự không nhất quán. Nhưng khả năng thay đổi dữ liệu rất hữu
ích! Trong phần tiếp theo, chúng ta sẽ thảo luận về mẫu khả biến nội bộ và kiểu
RefCell<T>
mà bạn có thể sử dụng kết hợp với Rc<T>
để làm việc với hạn chế
bất biến này.