Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

A linked list with the label 'a' pointing to three elements. The first element contains the integer 5 and points to the second element. Th
e second element contains the integer 10 and points to the third element. The third element contains the value 'Nil' that signifies the end of the l
ist; it does not point anywhere. A linked list with the label 'b' points to an element that contains the integer 3 and points to the first element o
f list 'a'. A linked list with the label 'c' points to an element that contains the integer 4 and also points to the first element of list 'a' so th
at the tails of lists 'b' and 'c' are both list 'a'.

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

Filename: src/main.rs
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));
}
Listing 15-17: สาธิตว่าเราไม่ได้รับอนุญาตให้มีสอง list ที่ใช้ 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 ศูนย์ของมัน

Filename: src/main.rs
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));
}
Listing 15-18: นิยามของ 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 ได้

Filename: src/main.rs
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));
}
Listing 15-19: Print reference count

ที่แต่ละจุดในโปรแกรมที่ 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 นี้ได้