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> พอยน์เตอร์อัจฉริยะแบบนับจำนวนการอ้างอิง

ในกรณีส่วนใหญ่ ความเป็นเจ้าของจะชัดเจน: คุณรู้ได้อย่างแม่นยำว่าตัวแปรใดเป็นเจ้าของค่าที่กำหนด อย่างไรก็ตาม มีบางกรณีที่ค่าเดียวอาจจะมีเจ้าของได้หลายคน ตัวอย่างเช่น ในโครงสร้างข้อมูลกราฟ (graph data structures) เส้นเชื่อม (edges) หลายเส้นอาจจะชี้ไปยังโหนด (node) เดียวกัน และโหนดนั้นตามแนวคิดแล้วก็ตกเป็นเจ้าของโดยเส้นเชื่อมทั้งหมดที่ชี้มายังโหนดนั้น โหนดไม่ควรถูกล้างทำความสะอาดเว้นแต่ว่าจะไม่มีเส้นเชื่อมใดชี้มาที่มันอีก และดังนั้นจึงไม่มีเจ้าของเหลืออยู่อีกต่อไป

คุณต้องเปิดใช้งานความเป็นเจ้าของแบบหลายคนอย่างชัดเจนโดยใช้ชนิดข้อมูล Rust ที่เรียกว่า Rc<T> ซึ่งเป็นตัวย่อมาจาก การนับจำนวนการอ้างอิง (reference counting) ชนิดข้อมูล Rc<T> จะคอยติดตามจำนวนการอ้างอิงไปยังค่าหนึ่ง ๆ เพื่อพิจารณาว่าค่านั้นยังถูกใช้งานอยู่หรือไม่ หากมีจำนวนการอ้างอิงไปยังค่านั้นเป็นศูนย์ ค่านั้นจะสามารถถูกล้างทำความสะอาดได้โดยไม่มีการอ้างอิงใด ๆ กลายเป็นค่าที่ไม่ถูกต้อง

ลองจินตนาการถึง Rc<T> เสมือนทีวีในห้องนั่งเล่นของครอบครัว เมื่อมีคนหนึ่งเดินเข้ามาดูทีวี พวกเขาจะเปิดทีวี ผู้อื่นสามารถเข้ามาในห้องและดูทีวีด้วยกันได้ เมื่อคนสุดท้ายออกจากห้องไป พวกเขาจะปิดทีวีเพราะไม่มีใครใช้งานแล้ว หากมีคนปิดทีวีในขณะที่ผู้อื่นยังคงดูอยู่ ก็คงจะเกิดการโวยวายจากผู้ชมทีวีที่เหลืออย่างแน่นอน!

เราใช้ชนิดข้อมูล Rc<T> เมื่อเราต้องการจัดสรรข้อมูลบางอย่างไว้บนฮีปเพื่อให้หลาย ๆ ส่วนของโปรแกรมของเราสามารถอ่านค่าได้ และเราไม่สามารถพิจารณาในขณะคอมไพล์ได้ว่าส่วนใดจะใช้งานข้อมูลเสร็จสิ้นเป็นส่วนสุดท้าย หากเรารู้ว่าส่วนใดจะเสร็จสิ้นเป็นส่วนสุดท้าย เราก็สามารถกำหนดให้ส่วนนั้นเป็นเจ้าของข้อมูลได้ และกฎความเป็นเจ้าของทั่วไปที่บังคับใช้ในขณะคอมไพล์ก็จะมีผลทำหน้าที่ไป

โปรดทราบว่า Rc<T> มีไว้สำหรับการใช้งานในสถานการณ์เธรดเดียว (single-threaded scenarios) เท่านั้น เมื่อเราพูดถึงการทำงานพร้อมกัน (concurrency) ในบทที่ 16 เราจะครอบคลุมวิธีนับจำนวนการอ้างอิงในโปรแกรมแบบหลายเธรด (multithreaded programs)

การแบ่งปันข้อมูล

กลับมาที่ตัวอย่าง cons list ของเราในโค้ดตัวอย่างที่ 15-5 โปรดระลึกว่าเราได้นิยามมันขึ้นโดยใช้ Box<T> ในครั้งนี้ เราจะสร้างสองลิสต์ที่ทั้งคู่แบ่งปันความเป็นเจ้าของของลิสต์ที่สามร่วมกัน ในทางแนวคิด สิ่งนี้ดูคล้ายกับรูปภาพที่ 15-3

ลิงก์ลิสต์ป้ายชื่อ 'a' ชี้ไปยังสามองค์ประกอบ องค์ประกอบแรกบรรจุจำนวนเต็ม 5 และชี้ไปยังองค์ประกอบที่สอง องค์ประกอบที่สองบรรจุจำนวนเต็ม 10 และชี้ไปยังองค์ประกอบที่สาม องค์ประกอบที่สามบรรจุค่า 'Nil' ที่แสดงถึงการสิ้นสุดของลิสต์ โดยไม่ชี้ไปที่ใด ลิงก์ลิสต์ป้ายชื่อ 'b' ชี้ไปยังองค์ประกอบที่บรรจุจำนวนเต็ม 3 และชี้ไปยังองค์ประกอบแรกของลิสต์ 'a' ลิงก์ลิสต์ป้ายชื่อ 'c' ชี้ไปยังองค์ประกอบที่บรรจุจำนวนเต็ม 4 และชี้ไปยังองค์ประกอบแรกของลิสต์ 'a' เช่นกัน เพื่อให้ส่วนหางของลิสต์ 'b' และ 'c' เป็นลิสต์ 'a' ทั้งคู่

รูปภาพที่ 15-3: สองลิสต์ คือ b และ c แบ่งปันความเป็นเจ้าของของลิสต์ที่สาม คือ a

เราจะสร้างลิสต์ a ที่บรรจุ 5 แล้วตามด้วย 10 จากนั้นเราจะสร้างอีกสองลิสต์: b ที่เริ่มต้นด้วย 3 และ c ที่เริ่มต้นด้วย 4 ลิสต์ทั้ง b และ c จะไปต่อที่ลิสต์ a แรกที่บรรจุ 5 และ 10 หรือกล่าวอีกนัยหนึ่ง ลิสต์ทั้งสองจะแบ่งปันลิสต์แรกที่บรรจุ 5 และ 10 ร่วมกัน

การพยายามประยุกต์ใช้สถานการณ์นี้โดยใช้นิยาม List ของเราด้วย Box<T> จะไม่ทำงาน ดังแสดงในโค้ดตัวอย่างที่ 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));
}

เมื่อเราคอมไพล์โค้ดนี้ เราจะได้ข้อผิดพลาดนี้:

$ 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

ตัวแปรย่อย Cons เป็นเจ้าของข้อมูลที่มันถือครอง ดังนั้นเมื่อเราสร้างลิสต์ b ตัวแปร a จึงถูกย้าย (moved) เข้าไปใน b และ b จึงกลายเป็นเจ้าของ a จากนั้น เมื่อเราพยายามใช้ a อีกครั้งตอนสร้าง c เราจึงไม่ได้รับอนุญาตเนื่องจาก a ถูกย้ายไปแล้ว

เราอาจเปลี่ยนนิยามของ Cons ให้ถือครองการอ้างอิงแทนได้ แต่นั่นหมายความว่าเราจะต้องระบุพารามิเตอร์ช่วงชีวิต (lifetime parameters) การระบุพารามิเตอร์ช่วงชีวิตจะเป็นการระบุว่าสมาชิกทุกตัวในลิสต์จะคงมีชีวิตอยู่อย่างน้อยนานเท่ากับลิสต์ทั้งหมด ซึ่งเป็นกรณีสำหรับสมาชิกและลิสต์ในโค้ดตัวอย่างที่ 15-17 แต่ไม่ใช่ในทุกสถานการณ์

แทนที่จะทำเช่นนั้น เราจะเปลี่ยนนิยามของ List ให้ใช้ Rc<T> แทนที่ Box<T> ดังแสดงในโค้ดตัวอย่างที่ 15-18 แต่ละตัวแปรย่อย Cons ตอนนี้จะถือครองค่าหนึ่งค่าและ Rc<T> ที่ชี้ไปยัง List เมื่อเราสร้าง b แทนที่จะรับความเป็นเจ้าของของ a เราจะทำสำเนา (clone) Rc<List> ที่ a ถือครองไว้ ซึ่งจะเพิ่มจำนวนการอ้างอิงจากหนึ่งเป็นสอง และยอมให้ a และ b แบ่งปันความเป็นเจ้าของของข้อมูลใน Rc<List> นั้นร่วมกัน เราจะทำสำเนา a เช่นกันตอนสร้าง c ซึ่งจะเพิ่มจำนวนการอ้างอิงจากสองเป็นสาม ทุกครั้งที่เราเรียกใช้ Rc::clone จำนวนการอ้างอิงไปยังข้อมูลภายใน Rc<List> จะเพิ่มขึ้น และข้อมูลจะไม่ถูกล้างทำความสะอาดเว้นแต่จะเหลือจำนวนการอ้างอิงเป็นศูนย์

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));
}

เราจำเป็นต้องเพิ่มคำสั่ง use เพื่อนำ Rc<T> เข้ามาในขอบเขตเพราะมันไม่ได้อยู่ในพรีลูด (prelude) ใน main เราสร้างลิสต์ที่บรรจุ 5 และ 10 แล้วเก็บไว้ใน Rc<List> ใหม่ใน a จากนั้น เมื่อเราสร้าง b และ c เราจะเรียกฟังก์ชัน Rc::clone และส่งการอ้างอิงไปยัง Rc<List> ใน a เป็นอาร์กิวเมนต์

เราอาจเรียกใช้ a.clone() แทนที่จะเป็น Rc::clone(&a) ได้ แต่ธรรมเนียมของ Rust คือการใช้ Rc::clone ในกรณีนี้ การประยุกต์ใช้ Rc::clone ไม่ได้สร้างสำเนาเชิงลึก (deep copy) ของข้อมูลทั้งหมดเหมือนการประยุกต์ใช้ clone ของชนิดข้อมูลส่วนใหญ่ การเรียกใช้ Rc::clone จะเพียงแค่เพิ่มจำนวนการอ้างอิงขึ้น ซึ่งไม่ได้ใช้เวลามาก การทำสำเนาข้อมูลเชิงลึกสามารถใช้เวลาได้มาก การใช้ Rc::clone สำหรับการนับจำนวนการอ้างอิงช่วยให้เราสามารถแยกแยะความแตกต่างทางสายตาระหว่างชนิดการทำสำเนาเชิงลึกกับการทำสำเนาที่เพิ่มจำนวนการอ้างอิงได้ เมื่อมองหาปัญหาด้านประสิทธิภาพในโค้ด เราจึงต้องพิจารณาเฉพาะการทำสำเนาเชิงลึกเท่านั้นและสามารถมองข้ามการเรียกใช้ Rc::clone ได้

การทำสำเนา (Clone) เพื่อเพิ่มจำนวนการอ้างอิง

มาเปลี่ยนตัวอย่างการทำงานของเราในโค้ดตัวอย่างที่ 15-18 เพื่อให้เราเห็นจำนวนการอ้างอิงที่เปลี่ยนแปลงไปขณะที่เราสร้างและปล่อยการอ้างอิงไปยัง Rc<List> ใน a

ในโค้ดตัวอย่างที่ 15-19 เราจะเปลี่ยน main ให้มีขอบเขตภายในล้อมรอบลิสต์ c จากนั้น เราสามารถเห็นได้ว่าจำนวนการอ้างอิงเปลี่ยนแปลงไปอย่างไรเมื่อ c หลุดออกจากขอบเขต

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));
}

ณ แต่ละจุดในโปรแกรมที่จำนวนการอ้างอิงเปลี่ยนแปลง เราจะพิมพ์จำนวนการอ้างอิง ซึ่งเราได้จากการเรียกใช้ฟังก์ชัน Rc::strong_count ฟังก์ชันนี้ถูกตั้งชื่อว่า strong_count แทนที่จะเป็น count เพราะชนิดข้อมูล Rc<T> มี weak_count ด้วยเช่นกัน เราจะเห็นว่า weak_count ถูกใช้ทำอะไรในหัวข้อ “การป้องกันวงจรการอ้างอิงด้วยการใช้ Weak<T>

โค้ดนี้พิมพ์ผลลัพธ์ต่อไปนี้:

$ 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 มีจำนวนการอ้างอิงเริ่มต้นเป็น 1 จากนั้น แต่ละครั้งที่เราเรียกใช้ clone จำนวนจะเพิ่มขึ้นทีละ 1 เมื่อ c หลุดออกจากขอบเขต จำนวนจะลดลงทีละ 1 เราไม่ต้องเรียกใช้ฟังก์ชันเพื่อลดจำนวนการอ้างอิงเหมือนที่เราต้องเรียกใช้ Rc::clone เพื่อเพิ่มจำนวนการอ้างอิง: การประยุกต์ใช้เทรต Drop จะลดจำนวนการอ้างอิงลงโดยอัตโนมัติเมื่อค่า Rc<T> หลุดออกจากขอบเขต

สิ่งที่เราไม่เห็นในตัวอย่างนี้คือเมื่อ b แล้วตามด้วย a หลุดออกจากขอบเขต ณ จุดสิ้นสุดของ main จำนวนการอ้างอิงจะเป็น 0 และ Rc<List> จะถูกล้างทำความสะอาดอย่างสมบูรณ์ การใช้ Rc<T> ยินยอมให้ค่าเดียวมีเจ้าของได้หลายคน และตัวนับจะช่วยให้มั่นใจว่าค่านั้นยังคงถูกต้องตราบเท่าที่ยังมีเจ้าของคนใดคนหนึ่งดำรงอยู่

ผ่านทางการอ้างอิงแบบแก้ไขไม่ได้ Rc<T> ช่วยให้คุณสามารถแบ่งปันข้อมูลระหว่างหลาย ๆ ส่วนของโปรแกรมสำหรับการอ่านเท่านั้น หาก Rc<T> ยินยอมให้คุณมีการอ้างอิงแบบแก้ไขได้หลายอันด้วย คุณอาจละเมิดกฎการยืมกฎข้อหนึ่งที่พูดถึงในบทที่ 4: การยืมแบบแก้ไขได้หลายตำแหน่งไปยังจุดเดียวกันอาจทำให้เกิดสภาวะแข่งขันของข้อมูล (data races) และความไม่สอดคล้องกันได้ แต่การสามารถแก้ไขข้อมูลได้ก็มีประโยชน์อย่างมาก! ในส่วนถัดไป เราจะพูดถึงรูปแบบการแก้ไขภายใน (interior mutability pattern) และชนิดข้อมูล RefCell<T> ที่คุณสามารถใช้ร่วมกับ Rc<T> เพื่อทำงานกับข้อจำกัดของการแก้ไขไม่ได้นี้