לדלג לתוכן

2.8 - בעלות משותפת ושינוי פנימי - הרצאה

שני חלקים של התוכנית צריכים את אותו אובייקט

עד עכשיו חיפשנו בעלים אחד לכל נתון. לפעמים יש כמה רכיבים שחייהם אינם מסודרים ביחס פשוט של הורה וילד, וכל אחד צריך להשאיר אובייקט משותף חי. הטיפוס Rc מספק בעלות משותפת בתוך תהליכון אחד באמצעות מונה הפניות חזקות.

use std::rc::Rc;

fn main() {
    let name = Rc::new(String::from("shared"));
    let another = Rc::clone(&name);
    println!("count={}", Rc::strong_count(&name));
    println!("{} {}", name, another);
    drop(another);
    println!("count={}", Rc::strong_count(&name));
}

הפעולה clone כאן אינה משכפלת את הטקסט. היא יוצרת בעלות נוספת על אותה הקצאה ומעדכנת מונה. כאשר הבעלים החזק האחרון נעלם, הערך הפנימי מושמד. בפרק התהליכונים נפגוש את Arc, אבל אין להשתמש ב־Rc כתחליף לסנכרון בין תהליכונים.

שיתוף בעלות אינו הרשאת שינוי

אם כמה בעלים יכולים לקרוא את אותו אובייקט, אי אפשר פשוט להחזיר לכל אחד מהם &mut T. ההבטחה של הפניה בלעדית הייתה נשברת. כדי לאפשר שינוי דרך ממשק משותף נדרש טיפוס שמגדיר כיצד השינוי מתבצע ואיך נשמרים כללי הגישה.

הטיפוס Cell מאפשר להחליף ערך בלי להחזיר הפניה רגילה לתוכו. עבור ערך Copy אפשר לקבל עותק ולכתוב ערך חדש:

use std::cell::Cell;

struct Counter {
    value: Cell<u32>,
}

impl Counter {
    fn increment(&self) {
        let next = self.value.get().checked_add(1).expect("counter overflow");
        self.value.set(next);
    }
}

fn main() {
    let counter = Counter { value: Cell::new(0) };
    counter.increment();
    counter.increment();
    assert_eq!(counter.value.get(), 2);
}

המתודה מקבלת &self, אך השינוי מותר דרך החוזה של Cell. לא נוצרה הפניה משותפת רגילה לערך פנימי שאנחנו משנים מאחוריה. ההגבלה על סוג הפעולות שהטיפוס חושף היא חלק מן הסיבה שאפשר להשתמש בו בבטחה.

בדיקת השאלות יכולה להתבצע בזמן ריצה

כאשר צריך להשאיל את הנתון הפנימי, RefCell מנהלת בדיקה בזמן ריצה. הפעולה borrow מחזירה guard לקריאה, ו־borrow_mut מחזירה guard לשינוי בלעדי. כל עוד guard חי, התא שומר את מצב ההשאלה המתאים.

use std::cell::RefCell;

fn main() {
    let values = RefCell::new(vec![1, 2]);
    {
        let mut access = values.borrow_mut();
        access.push(3);
    }
    let read = values.borrow();
    assert_eq!(&*read, &[1, 2, 3]);
    assert!(values.try_borrow_mut().is_err());
}

הניסיון האחרון נכשל באופן מבוקר משום שההשאלה המשותפת עדיין קיימת. הקריאה borrow_mut במקום try_borrow_mut הייתה גורמת לפאניקה. המערכת לא ביטלה את כלל הבעלות; היא העבירה חלק מבדיקתו לזמן הריצה דרך API שמחזיק את ההרשאה בתוך guard.

הטיפוס UnsafeCell הוא הבסיס שמאפשר שינוי פנימי בהפניה משותפת מבחינת חוזי הזיכרון. הוא אינו מספק לבדו את כללי הגישה של RefCell ואינו מתיר ליצור הפניות בלעדיות חופפות. בפרק unsafe נבדוק מה צריך להוכיח כאשר בונים מעטפת כזאת בעצמנו.

מחזור חזק יכול להשאיר נתונים בחיים

אם שני ערכי Rc מחזיקים זה בזה באמצעות בעלות חזקה, אף מונה אינו מגיע לאפס כאשר הקורא החיצוני עוזב. זו דליפת משאב, אף שהיא אינה בהכרח הפרת בטיחות זיכרון. גרף בטוח מבחינת מצביעים יכול עדיין להחזיק אחסון שלא משתחרר לעולם.

לשם כך קיימת הפניה חלשה, Weak. היא מאפשרת לנסות לקבל בעלות חזקה כאשר הערך עדיין חי, בלי להשאיר אותו חי בעצמה. נבנה עץ שבו ההורה מחזיק את ילדיו, והילד רק מכיר את ההורה:

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

struct Node {
    name: String,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn node(name: &str) -> Rc<Node> {
    Rc::new(Node {
        name: name.into(),
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(Vec::new()),
    })
}

fn main() {
    let root = node("root");
    let child = node("child");
    *child.parent.borrow_mut() = Rc::downgrade(&root);
    root.children.borrow_mut().push(Rc::clone(&child));
    assert_eq!(child.parent.borrow().upgrade().unwrap().name, "root");
    let observed = Rc::downgrade(&root);
    drop(root);
    assert!(observed.upgrade().is_none());
    assert!(child.parent.borrow().upgrade().is_none());
}

הילד נשאר חי כי המשתנה child עדיין בעלים שלו, אבל ההורה יכול להיעלם. הקריאה upgrade מחזירה Option ולכן מאלצת אותנו להתמודד עם האפשרות הזאת. קשר הורה חלש אינו מתאים לכל גרף; הוא מתאים כאן משום שהחלטנו שהילד אינו אמור להאריך את חיי ההורה.

בוחרים מודל ולא תרופה לכל אבחון

השילוב Rc עם RefCell שימושי, אבל יכול להסתיר תכנון שמקשה להבין מי משנה מה. לפני שמשתמשים בו, בדקו אם מספיק להעביר בעלות, להשתמש במזהים או להפריד שלבי עבודה. אם הבעלות אכן משותפת, כתבו אילו קשתות בגרף חזקות, אילו חלשות ובאיזה תחום מותר להחזיק guard.

בתרגול נבנה רישום callbacks קטן. הסכנה שתעניין אותנו אינה רק פאניקה בגלל borrow כפול, אלא גם קריאה חוזרת שמנסה לשנות את אותו אוסף בזמן שאנחנו מחזיקים אליו הרשאה. החוזה של הפעולה צריך להתייחס גם לכניסה חוזרת, אפילו כשהכול מתבצע בתהליכון אחד.