Rc<T> — Smart Pointer แบบนับ Reference
ในกรณีส่วนใหญ่ ownership ชัดเจน — คุณรู้แน่ชัดว่าตัวแปรไหน own ค่าที่ ให้ อย่างไรก็ตาม มีกรณีที่ค่าเดียวอาจมีหลาย owner ตัวอย่างเช่น ใน โครงสร้างข้อมูล graph หลาย edge อาจชี้ไปยัง node เดียวกัน และ node นั้นในเชิงแนวคิดถูก own โดย edge ทั้งหมดที่ชี้ไปที่มัน node ไม่ควร ถูก cleanup ยกเว้นมันไม่มี edge ใดที่ชี้ที่มัน และดังนั้นไม่มี owner
คุณต้องเปิดใช้ multiple ownership ชัดเจนโดยใช้ type Rust Rc<T>
ซึ่งย่อมาจาก reference counting type Rc<T> ตามจำนวน
reference ของค่าเพื่อตัดสินว่าค่ายังถูกใช้อยู่ไหม ถ้ามี reference
ศูนย์ของค่า ค่า cleanup ได้โดยไม่มี reference ใดที่กลายเป็นไม่ valid
จินตนาการ Rc<T> เป็น TV ในห้องครอบครัว เมื่อคนหนึ่งเข้ามาดู TV
พวกเขาเปิดมัน คนอื่นเข้ามาในห้องและดู TV ได้ เมื่อคนสุดท้ายออกจาก
ห้อง พวกเขาปิด TV เพราะมันไม่ถูกใช้อีก ถ้าใครปิด TV ในขณะที่คนอื่น
ยังดูอยู่ จะมีการประท้วงจากคนดู TV ที่เหลือ!
เราใช้ type Rc<T> เมื่อเราต้องการ allocate ข้อมูลบน heap ให้หลาย
ส่วนของโปรแกรมของเราอ่าน และเราตัดสินที่ compile time ไม่ได้ว่า
ส่วนไหนจะเสร็จใช้ข้อมูลล่าสุด ถ้าเรารู้ส่วนไหนจะเสร็จล่าสุด เราทำให้
ส่วนนั้นเป็น owner ของข้อมูลก็ได้ และกฎ ownership ปกติที่บังคับใช้
ที่ compile time จะมีผล
สังเกตว่า Rc<T> ใช้เฉพาะใน scenario เธรดเดียว เมื่อเราพูดถึง
concurrency ในบทที่ 16 เราจะครอบคลุมวิธีทำ reference counting ใน
โปรแกรม multithreaded
แชร์ข้อมูล
มากลับไปยังตัวอย่าง cons list ของเราใน Listing 15-5 จำได้ว่าเรานิยาม
มันโดยใช้ Box<T> คราวนี้ เราจะสร้างสอง list ที่ทั้งคู่แชร์ ownership
ของ list ที่สาม ในเชิงแนวคิด นี่ดูคล้ายกับ Figure 15-3
Figure 15-3: สอง list, b และ c แชร์
ownership ของ list ที่สาม a
เราจะสร้าง list a ที่บรรจุ 5 แล้ว 10 จากนั้น เราจะสร้างอีก
สอง list — b ที่เริ่มด้วย 3 และ c ที่เริ่มด้วย 4 ทั้ง list
b และ c จะต่อไปยัง list a แรกที่บรรจุ 5 และ 10 อีกแง่
หนึ่ง ทั้งสอง list จะแชร์ list แรกที่บรรจุ 5 และ 10
การพยายาม implement scenario นี้โดยใช้นิยามของ List กับ Box<T>
ของเราจะไม่ทำงาน ดังที่แสดงใน 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));
}
Box<T> ที่พยายามแชร์ ownership ของ list ที่สามเมื่อเราคอมไพล์โค้ดนี้ เราได้ error นี้:
$ 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
|
note: if `List` implemented `Clone`, you could clone the value
--> src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^ consider implementing `Clone` for this type
...
10 | let b = Cons(3, Box::new(a));
| - you could clone this value
For more information about this error, try `rustc --explain E0382`.
error: could not compile `cons-list` (bin "cons-list") due to 1 previous error
variant Cons own ข้อมูลที่พวกมันเก็บ ดังนั้นเมื่อเราสร้าง list
b a ถูกย้ายเข้า b และ b own a จากนั้น เมื่อเราพยายามใช้
a อีกครั้งเมื่อสร้าง c เราไม่ได้รับอนุญาตเพราะ a ถูกย้ายไป
เราเปลี่ยนนิยามของ Cons ให้เก็บ reference แทนได้ แต่จากนั้นเรา
จะต้องระบุ parameter lifetime โดยระบุ parameter lifetime เราจะระบุ
ว่าทุก element ใน list จะอยู่อย่างน้อยตราบเท่าที่ list ทั้งหมด นี่
เป็นกรณีสำหรับ element และ list ใน Listing 15-17 แต่ไม่ในทุก scenario
แทน เราจะเปลี่ยนนิยามของ List ของเราให้ใช้ Rc<T> ในที่ของ
Box<T> ดังที่แสดงใน Listing 15-18 แต่ละ variant Cons ตอนนี้จะ
เก็บค่าและ Rc<T> ที่ชี้ไปยัง List เมื่อเราสร้าง b แทนการรับ
ownership ของ a เราจะ clone Rc<List> ที่ a กำลังเก็บ ดังนั้น
เพิ่มจำนวน reference จากหนึ่งเป็นสอง และให้ a และ b แชร์
ownership ของข้อมูลใน Rc<List> นั้น เรายังจะ clone a เมื่อสร้าง
c เพิ่มจำนวน reference จากสองเป็นสาม ทุกครั้งที่เราเรียก
Rc::clone reference count ของข้อมูลภายใน Rc<List> จะเพิ่ม และ
ข้อมูลจะไม่ถูก cleanup ยกเว้นมี reference ศูนย์ของมัน
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));
}
List ที่ใช้ Rc<T>เราต้องเพิ่ม statement use เพื่อนำ Rc<T> เข้า scope เพราะมันไม่
อยู่ใน prelude ใน main เราสร้าง list ที่เก็บ 5 และ 10 และเก็บ
มันใน Rc<List> ใหม่ใน a จากนั้น เมื่อเราสร้าง b และ c เรา
เรียกฟังก์ชัน Rc::clone และส่ง reference ของ Rc<List> ใน a
เป็นอาร์กิวเมนต์
เราเรียก a.clone() แทน Rc::clone(&a) ได้ แต่ธรรมเนียมของ Rust
คือใช้ Rc::clone ในกรณีนี้ implementation ของ Rc::clone ไม่ทำ
deep copy ของข้อมูลทั้งหมดเหมือนที่ implementation clone ของ
type ส่วนใหญ่ทำ การเรียก Rc::clone เพียงเพิ่ม reference count
ซึ่งไม่ใช้เวลามาก Deep copy ของข้อมูลใช้เวลามาก โดยใช้ Rc::clone
สำหรับ reference counting เราแยกความแตกต่างด้วยสายตาระหว่างประเภท
clone แบบ deep-copy และประเภท clone ที่เพิ่ม reference count เมื่อ
มองหาปัญหา performance ในโค้ด เราต้องพิจารณาเฉพาะ clone แบบ
deep-copy และไม่สนใจการเรียก Rc::clone ได้
Clone เพื่อเพิ่ม Reference Count
มาเปลี่ยนตัวอย่างที่ใช้งานของเราใน Listing 15-18 เพื่อให้เราเห็น
reference count เปลี่ยนเมื่อเราสร้างและ drop reference ของ
Rc<List> ใน a
ใน Listing 15-19 เราจะเปลี่ยน main ให้มี scope ภายในรอบ list c
จากนั้น เราเห็นว่า reference count เปลี่ยนยังไงเมื่อ c ออกจาก
scope ได้
enum List {
Cons(i32, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::rc::Rc;
// --snip--
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));
}
ที่แต่ละจุดในโปรแกรมที่ reference count เปลี่ยน เรา print reference
count ที่เราได้โดยเรียกฟังก์ชัน Rc::strong_count ฟังก์ชันนี้ถูก
ตั้งชื่อ strong_count ไม่ใช่ count เพราะ type Rc<T> ยังมี
weak_count — เราจะเห็นว่า weak_count ใช้ทำอะไรใน
“ป้องกัน Reference Cycle โดยใช้ Weak<T>”
โค้ดนี้ print ต่อไปนี้:
$ 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
เราเห็นว่า Rc<List> ใน a มี reference count เริ่มต้นที่ 1 จากนั้น
แต่ละครั้งที่เราเรียก clone count ขึ้น 1 เมื่อ c ออกจาก scope
count ลง 1 เราไม่ต้องเรียกฟังก์ชันเพื่อลด reference count เหมือนที่
เราต้องเรียก Rc::clone เพื่อเพิ่ม reference count — implementation
ของ trait Drop ลด reference count อัตโนมัติเมื่อค่า Rc<T> ออกจาก
scope
สิ่งที่เราเห็นไม่ได้ในตัวอย่างนี้คือเมื่อ b แล้ว a ออกจาก scope
ที่ท้ายสุดของ main count เป็น 0 และ Rc<List> ถูก cleanup
สมบูรณ์ การใช้ Rc<T> อนุญาตให้ค่าเดียวมีหลาย owner และ count
รับประกันว่าค่ายัง valid ตราบใดที่ owner ใดยังมีอยู่
ผ่าน immutable reference Rc<T> อนุญาตให้คุณแชร์ข้อมูลระหว่างหลาย
ส่วนของโปรแกรมของคุณสำหรับอ่านเท่านั้น ถ้า Rc<T> อนุญาตให้คุณมี
หลาย mutable reference ด้วย คุณอาจละเมิดกฎ borrowing หนึ่งที่พูดถึง
ในบทที่ 4 — multiple mutable borrow ไปยังที่เดียวก่อให้เกิด data
race และความไม่สอดคล้องได้ แต่การ mutate ข้อมูลได้มีประโยชน์มาก!
ในส่วนถัดไป เราจะพูดถึง pattern interior mutability และ type
RefCell<T> ที่คุณใช้ร่วมกับ Rc<T> เพื่อทำงานกับข้อจำกัด
immutability นี้ได้