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

วงจรการอ้างอิงสามารถทำให้เกิดหน่วยความจำรั่วไหลได้

การรับประกันความปลอดภัยของหน่วยความจำใน Rust ทำให้เกิดกรณีโดยไม่ตั้งใจได้ยาก แต่ก็ใช่ว่าจะเกิดขึ้นไม่ได้เลยในการสร้างหน่วยความจำที่ไม่ถูกล้างทำความสะอาด (เรียกว่า หน่วยความจำรั่วไหล หรือ memory leak) การป้องกันหน่วยความจำรั่วไหลโดยสมบูรณ์ไม่ได้เป็นหนึ่งในการรับประกันของ Rust ซึ่งหมายความว่าหน่วยความจำรั่วไหลนั้นปลอดภัยต่อหน่วยความจำในมุมมองของ Rust เราสามารถเห็นว่า Rust อนุญาตให้เกิดหน่วยความจำรั่วไหลได้โดยใช้ Rc<T> และ RefCell<T>: มันเป็นไปได้ที่จะสร้างการอ้างอิงที่รายการต่าง ๆ อ้างอิงถึงกันและกันในลักษณะเป็นวงจร สิ่งนี้ทำให้เกิดหน่วยความจำรั่วไหลเนื่องจากจำนวนการอ้างอิงของแต่ละรายการในวงจรจะไม่มีวันลดลงถึง 0 และค่าต่าง ๆ จะไม่มีวันถูกล้างทำความสะอาด (dropped)

การสร้างวงจรการอ้างอิง

มาดูกันว่าวงจรการอ้างอิงอาจเกิดขึ้นได้อย่างไรและจะป้องกันได้อย่างไร โดยเริ่มจากนิยามของ enum List และเมธอด tail ในโค้ดตัวอย่างที่ 15-25

use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
enum List {
    Cons(i32, RefCell<Rc<List>>),
    Nil,
}

impl List {
    fn tail(&self) -> Option<&RefCell<Rc<List>>> {
        match self {
            Cons(_, item) => Some(item),
            Nil => None,
        }
    }
}

fn main() {}

เรากำลังใช้รูปแบบอื่นของนิยาม List จากโค้ดตัวอย่างที่ 15-5 องค์ประกอบที่สองในตัวแปรย่อย Cons ตอนนี้คือ RefCell<Rc<List>> ซึ่งหมายความว่าแทนที่จะมีความสามารถในการแก้ไขค่า i32 เหมือนที่เราทำในโค้ดตัวอย่างที่ 15-24 เราต้องการแก้ไขค่า List ที่ตัวแปรย่อย Cons ชี้ไป เรายังได้เพิ่มเมธอด tail เพื่อให้เราสะดวกในการเข้าถึงสมาชิกตัวที่สองหากเรามีตัวแปรย่อย Cons

ในโค้ดตัวอย่างที่ 15-26 เรากำลังเพิ่มฟังก์ชัน main ที่ใช้นิยามในโค้ดตัวอย่างที่ 15-25 โค้ดนี้สร้างลิสต์ใน a และลิสต์ใน b ที่ชี้ไปยังลิสต์ใน a จากนั้น มันแก้ไขลิสต์ใน a ให้ชี้ไปยัง b ทำให้เกิดวงจรการอ้างอิง มีคำสั่ง println! ระหว่างทางเพื่อแสดงให้เห็นว่าจำนวนการอ้างอิงเป็นเท่าใด ณ จุดต่าง ๆ ในกระบวนการนี้

use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
enum List {
    Cons(i32, RefCell<Rc<List>>),
    Nil,
}

impl List {
    fn tail(&self) -> Option<&RefCell<Rc<List>>> {
        match self {
            Cons(_, item) => Some(item),
            Nil => None,
        }
    }
}

// ANCHOR: here
fn main() {
    let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));

    println!("a initial rc count = {}", Rc::strong_count(&a));
    println!("a next item = {:?}", a.tail());

    let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));

    println!("a rc count after b creation = {}", Rc::strong_count(&a));
    println!("b initial rc count = {}", Rc::strong_count(&b));
    println!("b next item = {:?}", b.tail());

    if let Some(link) = a.tail() {
        *link.borrow_mut() = Rc::clone(&b);
    }

    println!("b rc count after changing a = {}", Rc::strong_count(&b));
    println!("a rc count after changing a = {}", Rc::strong_count(&a));

    // Uncomment the next line to see that we have a cycle;
    // it will overflow the stack.
    // println!("a next item = {:?}", a.tail());
}
// ANCHOR_END: here

เราสร้างอินสแตนซ์ Rc<List> ที่ถือครองค่า List ในตัวแปร a โดยมีลิสต์เริ่มต้นเป็น 5, Nil จากนั้นเราสร้างอินสแตนซ์ Rc<List> ที่ถือครองค่า List อีกค่าหนึ่งในตัวแปร b ที่บรรจุค่า 10 และชี้ไปยังลิสต์ใน a

เราแก้ไข a เพื่อให้มันชี้ไปยัง b แทนที่จะเป็น Nil ซึ่งทำให้เกิดวงจร เราทำสิ่งนั้นโดยใช้เมธอด tail เพื่อรับการอ้างอิงไปยัง RefCell<Rc<List>> ใน a ซึ่งเราใส่ไว้ในตัวแปร link จากนั้น เราใช้เมธอด borrow_mut บน RefCell<Rc<List>> เพื่อเปลี่ยนค่าภายในจาก Rc<List> ที่ถือครองค่า Nil ไปเป็น Rc<List> ใน b

เมื่อเรารันโค้ดนี้ โดยใส่คอมเมนต์ปิดคำสั่ง println! บรรทัดสุดท้ายไว้ชั่วคราว เราจะได้ผลลัพธ์นี้:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.53s
     Running `target/debug/cons-list`
a initial rc count = 1
a next item = Some(RefCell { value: Nil })
a rc count after b creation = 2
b initial rc count = 1
b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
b rc count after changing a = 2
a rc count after changing a = 2

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

สี่เหลี่ยมผืนผ้าป้ายชื่อ 'a' ที่ชี้ไปยังสี่เหลี่ยมผืนผ้าที่บรรจุจำนวนเต็ม 5 สี่เหลี่ยมผืนผ้าป้ายชื่อ 'b' ที่ชี้ไปยังสี่เหลี่ยมผืนผ้าที่บรรจุจำนวนเต็ม 10 สี่เหลี่ยมผืนผ้าที่บรรจุ 5 ชี้ไปยังสี่เหลี่ยมผืนผ้าที่บรรจุ 10 และสี่เหลี่ยมผืนผ้าที่บรรจุ 10 ชี้กลับไปยังสี่เหลี่ยมผืนผ้าที่บรรจุ 5 ซึ่งทำให้เกิดวงจร

รูปภาพที่ 15-4: วงจรการอ้างอิงของลิสต์ a และ b ที่ชี้หากันและกัน

หากคุณเปิดคอมเมนต์บรรทัด println! สุดท้ายออกแล้วรันโปรแกรม Rust จะพยายามพิมพ์วงจรนี้โดยที่ a ชี้ไปยัง b ชี้ไปยัง a ไปเรื่อย ๆ จนกระทั่งล้นสแต็ก (stack overflow)

เมื่อเปรียบเทียบกับโปรแกรมในโลกแห่งความเป็นจริง ผลลัพธ์ของการสร้างวงจรการอ้างอิงในตัวอย่างนี้ไม่ได้ร้ายแรงมากนัก: ทันทีหลังจากที่เราสร้างวงจรการอ้างอิง โปรแกรมก็จบลง อย่างไรก็ตาม หากโปรแกรมที่ซับซ้อนกว่านี้จัดสรรหน่วยความจำจำนวนมากในลักษณะวงจรและถือครองไว้เป็นเวลานาน โปรแกรมจะใช้หน่วยความจำมากกว่าที่จำเป็น และอาจทำให้ระบบทำงานหนักจนหน่วยความจำที่มีอยู่หมดลงได้

การสร้างวงจรการอ้างอิงไม่ได้ทำได้ง่าย ๆ แต่ก็ใช่ว่าจะเป็นไปไม่ได้ หากคุณมีค่า RefCell<T> ที่บรรจุค่า Rc<T> หรือการรวมกันแบบซ้อนที่คล้ายกันของชนิดข้อมูลที่มีการแก้ไขภายในและการนับจำนวนการอ้างอิง คุณต้องตรวจสอบให้แน่ใจว่าคุณไม่ได้สร้างวงจร คุณไม่สามารถพึ่งพา Rust ในการตรวจจับพวกมันได้ การสร้างวงจรการอ้างอิงจะเป็นข้อผิดพลาดทางตรรกะ (logic bug) ในโปรแกรมของคุณที่คุณควรใช้การทดสอบอัตโนมัติ การตรวจสอบโค้ด (code review) และแนวปฏิบัติการพัฒนาซอฟต์แวร์อื่น ๆ เพื่อลดโอกาสเกิดให้น้อยที่สุด

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

การป้องกันวงจรการอ้างอิงด้วยการใช้ Weak<T>

จนถึงตอนนี้ เราได้สาธิตว่าการเรียก Rc::clone จะเพิ่ม strong_count ของอินสแตนซ์ Rc<T> และอินสแตนซ์ Rc<T> จะถูกล้างทำความสะอาดเฉพาะเมื่อ strong_count เป็น 0 เท่านั้น คุณยังสามารถสร้างการอ้างอิงแบบอ่อน (weak reference) ไปยังค่าภายในอินสแตนซ์ Rc<T> ได้โดยการเรียก Rc::downgrade และส่งการอ้างอิงไปยัง Rc<T> การอ้างอิงแบบเข้มแข็ง (Strong references) คือวิธีที่คุณสามารถแบ่งปันความเป็นเจ้าของของอินสแตนซ์ Rc<T> การอ้างอิงแบบอ่อน (Weak references) จะไม่แสดงความสัมพันธ์ของการเป็นเจ้าของ และจำนวนของพวกมันไม่ส่งผลกระทบต่อเวลาที่อินสแตนซ์ Rc<T> จะถูกล้างทำความสะอาด พวกมันจะไม่ทำให้เกิดวงจรการอ้างอิง เพราะวงจรใด ๆ ที่เกี่ยวข้องกับการอ้างอิงแบบอ่อนบางอันจะถูกทำลายเมื่อจำนวนการอ้างอิงแบบเข้มแข็งของค่าที่เกี่ยวข้องเป็น 0

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

เนื่องจากค่าที่ Weak<T> อ้างอิงถึงอาจจะถูกปล่อย (dropped) ไปแล้ว ในการทำสิ่งใดก็ตามกับค่าที่ Weak<T> ชี้ไป คุณต้องแน่ใจว่าค่านั้นยังคงมีอยู่ ให้ทำสิ่งนี้โดยเรียกใช้เมธอด upgrade บนอินสแตนซ์ Weak<T> ซึ่งจะคืนค่าเป็น Option<Rc<T>> คุณจะได้ผลลัพธ์เป็น Some หากค่า Rc<T> ยังไม่ถูกปล่อย และได้ผลลัพธ์เป็น None หากค่า Rc<T> ถูกปล่อยไปแล้ว เนื่องจาก upgrade คืนค่าเป็น Option<Rc<T>> Rust จะรับประกันว่ากรณี Some และกรณี None จะได้รับการจัดการ และจะไม่มีพอยน์เตอร์ที่ไม่ถูกต้อง

ตัวอย่างเช่น แทนที่จะใช้ลิสต์ที่สมาชิกแต่ละตัวรู้จักเฉพาะสมาชิกถัดไป เราจะสร้างต้นไม้ (tree) ที่สมาชิกแต่ละตัวรู้จักโหนดลูกของมัน และ โหนดแม่ของมัน

การสร้างโครงสร้างข้อมูลแบบต้นไม้

เพื่อเริ่มต้น เราจะสร้างต้นไม้ด้วยโหนดที่รู้จักโหนดลูกของมัน เราจะสร้าง struct ชื่อ Node ที่ถือครองค่า i32 ของตัวมันเอง รวมถึงการอ้างอิงไปยังค่า Node ลูกของมัน:

ชื่อไฟล์: src/main.rs

use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
struct Node {
    value: i32,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        children: RefCell::new(vec![]),
    });

    let branch = Rc::new(Node {
        value: 5,
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });
}

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

ถัดไป เราจะใช้นิยาม struct ของเราและสร้างหนึ่งอินสแตนซ์ Node ชื่อ leaf ที่มีค่า 3 และไม่มีโหนดลูก และอีกหนึ่งอินสแตนซ์ชื่อ branch ที่มีค่า 5 และมี leaf เป็นหนึ่งในโหนดลูกของมัน ดังแสดงในโค้ดตัวอย่างที่ 15-27

use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
struct Node {
    value: i32,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        children: RefCell::new(vec![]),
    });

    let branch = Rc::new(Node {
        value: 5,
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });
}

เราทำสำเนา Rc<Node> ใน leaf และเก็บไว้ใน branch หมายความว่า Node ใน leaf ตอนนี้มีเจ้าของสองคน: leaf และ branch เราสามารถเข้าถึงจาก branch ไปยัง leaf ผ่านทาง branch.children แต่ไม่มีวิธีเข้าถึงจาก leaf ไปยัง branch เหตุผลก็คือ leaf ไม่มี การอ้างอิงไปยัง branch และไม่รู้ว่าพวกมันเกี่ยวข้องกัน เราต้องการให้ leaf รู้ว่า branch เป็นโหนดแม่ของมัน เราจะทำเช่นนั้นในขั้นตอนถัดไป

การเพิ่มการอ้างอิงจากโหนดลูกไปยังโหนดแม่

เพื่อให้โหนดลูกรับรู้ถึงโหนดแม่ของมัน เราจำเป็นต้องเพิ่มฟิลด์ parent ลงในการนิยาม struct Node ของเรา ปัญหาคือการตัดสินใจว่าชนิดของ parent ควรเป็นอะไร เรารู้ว่ามันไม่สามารถบรรจุ Rc<T> ได้ เพราะนั่นจะทำให้เกิดวงจรการอ้างอิงที่ leaf.parent ชี้ไปยัง branch และ branch.children ชี้ไปยัง leaf ซึ่งจะทำให้ค่า strong_count ของพวกมันไม่มีวันเป็น 0

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

ดังนั้น แทนที่จะใช้ Rc<T> เราจะทำให้ชนิดของ parent ใช้ Weak<T> โดยเฉพาะคือ RefCell<Weak<Node>> ตอนนี้ นิยาม struct Node ของเราดูเป็นดังนี้:

ชื่อไฟล์: src/main.rs

use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());

    let branch = Rc::new(Node {
        value: 5,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });

    *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}

โหนดจะสามารถอ้างอิงถึงโหนดแม่ของมันได้แต่ไม่ได้เป็นเจ้าของโหนดแม่ ในโค้ดตัวอย่างที่ 15-28 เราอัปเดต main ให้ใช้นิยามใหม่นี้เพื่อให้โหนด leaf มีวิธีอ้างอิงถึงโหนดแม่ของมันคือ branch

use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());

    let branch = Rc::new(Node {
        value: 5,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });

    *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}

การสร้างโหนด leaf ดูคล้ายกับโค้ดตัวอย่างที่ 15-27 ยกเว้นฟิลด์ parent: leaf เริ่มต้นโดยไม่มีโหนดแม่ ดังนั้นเราจึงสร้างอินสแตนซ์การอ้างอิง Weak<Node> ใหม่ที่ว่างเปล่า

ณ จุดนี้ เมื่อเราลองรับการอ้างอิงไปยังโหนดแม่ของ leaf โดยใช้เมธอด upgrade เราจะได้ค่า None เราเห็นสิ่งนี้ในผลลัพธ์จากคำสั่ง println! บรรทัดแรก:

leaf parent = None

เมื่อเราสร้างโหนด branch มันจะมี การอ้างอิง Weak<Node> ใหม่ในฟิลด์ parent ด้วยเช่นกันเพราะ branch ไม่มีโหนดแม่ เรายังคงมี leaf เป็นหนึ่งในโหนดลูกของ branch เมื่อเรามีอินสแตนซ์ Node ใน branch แล้ว เราสามารถแก้ไข leaf เพื่อให้มันมี การอ้างอิง Weak<Node> ไปยังโหนดแม่ของมันได้ เราใช้เมธอด borrow_mut บน RefCell<Weak<Node>> ในฟิลด์ parent ของ leaf แล้วใช้ฟังก์ชัน Rc::downgrade เพื่อสร้างการอ้างอิง Weak<Node> ไปยัง branch จาก Rc<Node> ใน branch

เมื่อเราพิมพ์โหนดแม่ของ leaf อีกครั้ง ในครั้งนี้เราจะได้ตัวแปรย่อย Some ที่ถือครอง branch: ตอนนี้ leaf สามารถเข้าถึงโหนดแม่ของมันได้แล้ว! เมื่อเราพิมพ์ leaf เรายังหลีกเลี่ยงวงจรที่จบลงด้วยสแต็กล้นเหมือนที่เรามีในโค้ดตัวอย่างที่ 15-26; การอ้างอิง Weak<Node> จะถูกพิมพ์เป็น (Weak):

leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) },
children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) },
children: RefCell { value: [] } }] } })

การไม่มีผลลัพธ์ที่เป็นอนันต์แสดงว่าโค้ดนี้ไม่ได้สร้างวงจรการอ้างอิง เรายังสามารถบอกได้โดยดูที่ค่าที่เราได้จากการเรียกใช้ Rc::strong_count และ Rc::weak_count

การมองภาพการเปลี่ยนแปลงของ strong_count และ weak_count

มาดูกันว่าค่า strong_count และ weak_count ของอินสแตนซ์ Rc<Node> เปลี่ยนแปลงไปอย่างไร โดยการสร้างขอบเขตภายในใหม่และย้ายการสร้าง branch เข้าไปในขอบเขตนั้น โดยทำเช่นนั้น เราจะเห็นว่าเกิดอะไรขึ้นเมื่อ branch ถูกสร้างขึ้น แล้วตามด้วยถูกปล่อยเมื่อมันหลุดออกจากขอบเขต การปรับเปลี่ยนแสดงในโค้ดตัวอย่างที่ 15-29

use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    println!(
        "leaf strong = {}, weak = {}",
        Rc::strong_count(&leaf),
        Rc::weak_count(&leaf),
    );

    {
        let branch = Rc::new(Node {
            value: 5,
            parent: RefCell::new(Weak::new()),
            children: RefCell::new(vec![Rc::clone(&leaf)]),
        });

        *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

        println!(
            "branch strong = {}, weak = {}",
            Rc::strong_count(&branch),
            Rc::weak_count(&branch),
        );

        println!(
            "leaf strong = {}, weak = {}",
            Rc::strong_count(&leaf),
            Rc::weak_count(&leaf),
        );
    }

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
    println!(
        "leaf strong = {}, weak = {}",
        Rc::strong_count(&leaf),
        Rc::weak_count(&leaf),
    );
}

หลังจาก leaf ถูกสร้างขึ้น Rc<Node> ของมันจะมีจำนวนแบบเข้มแข็ง (strong count) เป็น 1 และจำนวนแบบอ่อน (weak count) เป็น 0 ในขอบเขตภายใน เราสร้าง branch และเชื่อมโยงมันกับ leaf ซึ่ง ณ จุดนั้นเมื่อเราพิมพ์จำนวน Rc<Node> ใน branch จะมีจำนวนแบบเข้มแข็งเป็น 1 และจำนวนแบบอ่อนเป็น 1 (สำหรับ leaf.parent ที่ชี้ไปยัง branch ด้วย Weak<Node>) เมื่อเราพิมพ์จำนวนใน leaf เราจะเห็นว่ามันจะมีจำนวนแบบเข้มแข็งเป็น 2 เพราะ branch ตอนนี้มีสำเนาของ Rc<Node> ของ leaf เก็บไว้ใน branch.children แต่จะยังคงมีจำนวนแบบอ่อนเป็น 0

เมื่อขอบเขตภายในสิ้นสุดลง branch จะหลุดออกจากขอบเขต และจำนวนแบบเข้มแข็งของ Rc<Node> จะลดลงเหลือ 0 ดังนั้น Node ของมันจึงถูกปล่อย จำนวนแบบอ่อนเป็น 1 จาก leaf.parent ไม่มีผลต่อการที่ Node จะถูกปล่อยหรือไม่ ดังนั้นเราจึงไม่มีหน่วยความจำรั่วไหล!

หากเราลองเข้าถึงโหนดแม่ของ leaf หลังจากสิ้นสุดขอบเขต เราจะได้ None อีกครั้ง ณ จุดสิ้นสุดของโปรแกรม Rc<Node> ใน leaf จะมีจำนวนแบบเข้มแข็งเป็น 1 และจำนวนแบบอ่อนเป็น 0 เพราะตัวแปร leaf ตอนนี้เป็น การอ้างอิงเดียวไปยัง Rc<Node> อีกครั้ง

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

สรุป

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

นอกจากนี้ยังพูดถึงเทรต Deref และ Drop ซึ่งช่วยเปิดใช้งานฟังก์ชันการทำงานมากมายของพอยน์เตอร์อัจฉริยะ เราได้สำรวจวงจรการอ้างอิงที่สามารถทำให้เกิดหน่วยความจำรั่วไหลและวิธีป้องกันโดยใช้ Weak<T>

หากบทนี้กระตุ้นความสนใจของคุณและคุณต้องการประยุกต์ใช้พอยน์เตอร์อัจฉริยะของคุณเอง ลองไปดูที่ “The Rustonomicon” สำหรับข้อมูลที่มีประโยชน์เพิ่มเติม

ถัดไป เราจะพูดถึงการทำงานพร้อมกัน (concurrency) ใน Rust คุณยังจะได้เรียนรู้เกี่ยวกับพอยน์เตอร์อัจฉริยะใหม่อีกด้วย