اذهب إلى المحتوى

المؤشر الذكي Refcell<T>‎ ونمط قابلية التغيير الداخلي interior mutability في لغة رست Rust


Naser Dakhel

تُعد قابلية التغيير الداخلي interior mutability نمط تصميم في رست يسمح لك بتغيير البيانات حتى في حالة وجود مراجع ثابتة immutable تشير لتلك البيانات، ولا تسمح قواعد الاستعارة بهذا الإجراء عادًة. يستخدم النمط شيفرة "unsafe" لتغيير البيانات وذلك داخل هيكل بيانات للتحايل على قواعد رست المعتادة التي تتحكم بقابلية التعديل والاستعارة، إذ تشير الشيفرة غير الآمنة للمصرّف أننا نتحقق من القواعد يدويًا عوضًا عن الاعتماد على المصرّف للتحقق منها لنا، وسنناقش الشيفرة البرمجية غير الآمنة بالتفصيل لاحقًا.

يمكننا استخدام الأنواع التي تستخدم نمط قابلية التغيير الداخلي فقط عندما نتأكد أن قواعد الاستعارة تُتّبع في وقت التنفيذ، على الرغم من أن المصرّف لا يمكنه ضمان ذلك. تُغلّف wrap الشيفرة غير الآمنة "unsafe" ضمن واجهة برمجية API آمنة، بحيث يبقى النوع الخارجي ثابتًا.

لنكتشف هذا المفهوم من خلال النظر إلى نوع <RefCell<T الذي يتبع نمط التغيير الداخلي.

فرض قواعد الاستعارة عند وقت التنفيذ باستخدام <RefCell<T

يمثل النمط <RefCell<T بعكس <Rc<T ملكيةً مفردةً للبيانات التي يحملها. إذًا ما الذي يجعل <RefCell<T مختلفًا عن نمط مثل <Box<T؟ تذكر قواعد الاستعارة التي تعلمناها سابقًا في المقال المراجع References والاستعارة Borrowing والشرائح Slices:

  • يمكنك بأي وقت أن تمتلك إما مرجعًا متغيرًا واحدًا أو أي عدد من المراجع الثابتة ولكن ليس كليهما.
  • يجب على المراجع أن تكون صالحة دومًا.

تُفرض ثوابت قواعد الاستعارة borrowing rules’ invariants عند وقت التصريف مع المراجع و <Box<T، وتُفرض هذه الثوابت مع <RefCell<T في وقت التنفيذ. نحصل على خطأ تصريفي مع المراجع إذا خرقنا هذه القواعد، إلا أنه في حالة <RefCell<T سيُصاب البرنامج بالهلع ويتوقف إذا خرقت القواعد ذاتها.

تتمثل إيجابيات التحقق من قواعد الاستعارة في وقت التصريف في أنه ستُكتشف الأخطاء مبكرًا في عملية التطوير ولا يوجد أي تأثير على الأداء وقت التنفيذ لأن جميع التحليلات قد اكتملت مسبقًا، لهذه الأسباب يُعد التحقق من قواعد الاستعارة في وقت التصريف هو الخيار الأفضل في معظم الحالات وهذا هو السبب في كونه الخيار الافتراضي في رست.

تتمثل إيجابيات ميزة التحقق من قواعد الاستعارة في وقت التنفيذ بدلًا من ذلك، بأنه يُسمح بعد ذلك بسيناريوهات معينة خاصة بالذاكرة الآمنة إذ يجري منعها عادةً من خلال عمليات التحقق في وقت التصريف. يُعد التحليل الساكن static analysis صارمًا كما هو الحال في مصرّف رست. من المستحيل اكتشاف بعض خصائص الشيفرة البرمجية من خلال تحليلها والمثال الأكثر شهرة هو مشكلة التوقف halting problem التي تتجاوز نطاق موضوعنا هنا إلا أنه موضوع يستحق البحث عنه.

قد ترفض رست البرنامج الصحيح إذا لم يكن مصرّف رست متأكدًا من أن الشيفرة البرمجية تتوافق مع قواعد الملكية، وذلك بفضل وجود عملية التحليل، وتوضّح هذه الحالة صرامة العملية، وإذا قبلت رست برنامجًا خاطئًا فلن يتمكن المستخدمون من الوثوق بالضمانات التي تقدمها رست، وعلى الجانب الآخر إذا رفضت رست برنامجًا صحيحًا فسيتسبب ذلك بإزعاج للمبرمج، ولكن لا يمكن أن يحدث أي شيء كارثي. يُعد النوع <RefCell<T مفيدًا عندما تكون متأكدًا من أن الشيفرة الخاصة بك تتبع قواعد الاستعارة ولكن المصرّف غير قادر على فهم وضمان ذلك.

وكما في <Rc<T تُستخدم <RefCell<T فقط في السيناريوهات ذات الخيوط المفردة single-threaded وستعطيك خطأً وقت التنفيذ إذا حاولت استخدامه في سياق متعدد الخيوط multithreaded. سنتحدث عن كيفية الحصول على وظيفة <RefCell<T في برنامج متعدد الخيوط لاحقًا.

فيما يلي ملخص لأسباب اختيار<Box<T أو <Rc<T أو <RefCell<T:

  • يمكّن النوع <Rc<T وجود عدّة مالكين لنفس البيانات بينما يوجد للنوعين <RefCell<T و <Box<T مالك وحيد.
  • يسمح النوع <Box<T بوجود استعارات متغيّرة أو ثابتة يُتحقَّق منها وقت التصريف، بينما يسمح النوع <RefCell<T باستعارات متغيّرة أو ثابتة وقت التنفيذ.
  • بما أن النوع <RefCell<T يسمح باستعارات متغيّرة مُتحقق منها في وقت التنفيذ، يمكنك تغيير القيمة داخل <RefCell<T حتى عندما يكون النوع <RefCell<T ثابتًا.

تغيير القيمة داخل قيمة ثابتة هو نمط التغيير الداخلي. لنلقي نظرةً على موقف يكون فيه نمط التغيير الداخلي مفيدًا ونفحص كيف يكون ذلك ممكنًا.

التغيير الداخلي: استعارة متغيرة لقيمة ثابتة

عندما يكون لديك قيمة ثابتة فإنك لا تستطيع استعارتها على أنها متغيرة وفقًا لقواعد الاستعارة، على سبيل المثال، لن تُصرَّف الشيفرة البرمجية التالية:

fn main() {
    let x = 5;
    let y = &mut x;
}

إذا حاولت تصريف الشيفرة السابقة فسيظهر لك الخطأ التالي:

$ cargo run
   Compiling borrowing v0.1.0 (file:///projects/borrowing)
error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable
 --> src/main.rs:3:13
  |
2 |     let x = 5;
  |         - help: consider changing this to be mutable: `mut x`
3 |     let y = &mut x;
  |             ^^^^^^ cannot borrow as mutable

For more information about this error, try `rustc --explain E0596`.
error: could not compile `borrowing` due to previous error

على الرغم من ذلك هناك حالات قد يكون من المفيد فيها لقيمة ما أن تغير نفسها باستخدام توابعها methods الخاصة مع جعلها ثابتة بالنسبة للشيفرات الأخرى، بحيث لا تستطيع الشيفرة البرمجية خارج توابع القيمة تغيير القيمة. يُعد استخدام <RefCell<T إحدى الطرق للحصول على التغيير الداخلي إلا أن النوع <RefCell<T لا يتغلب على كامل قواعد الاستعارة، إذ يسمح مدقق الاستعارة في المصرّف بالتغيير الداخلي ويجري التحقق من قواعد الاستعارة في وقت التنفيذ بدلًا من ذلك. إذا خُرقت القواعد فسوف تحصل على حالة هلع panic!‎ بدلًا من خطأ تصريفي.

لنعمل من خلال مثال عملي يمكّننا من استخدام <RefCell<T لتغيير قيمة ثابتة ومعرفة لماذا يُعد ذلك مفيدًا.

حالة استخدام للتغيير الداخلي: الكائنات الزائفة Mock Objects

يستخدم المبرمج نوعًا بدلًا من آخر في بعض الأحيان أثناء الاختبار من أجل مراقبة سلوك معين والتأكد من تنفيذه بصورةٍ صحيحة، ويُسمى هذا النوع من وضع القيمة المؤقتة placeholder بالاختبار المزدوج test double. فكِّر بهذا الأمر بسياق "دوبلير stunt double" في صناعة الأفلام، إذ يتدخل الشخص ويحل محل الممثل لإنجاز مشهد معين صعب. يُستخدم الاختبار المزدوج لأنواع أخرى عندما نجري الاختبارات. الكائنات الزائفة هي أنواع محددة من الاختبار المزدوج التي تسجل ما يحدث أثناء الاختبار حتى تتمكن من أن تتأكد أن الإجراءات الصحيحة قد أُنجزت.

لا تحتوي رست على كائنات بنفس معنى الكائنات في لغات البرمجة الأخرى، ولا تحتوي رست على قدرة التعامل مع الكائنات الزائفة مُضمَّنة في المكتبة القياسية كما تفعل بعض اللغات الأخرى، ومع ذلك يمكنك بالتأكيد إنشاء هيكل يخدم الهدف من الكائنات الزائفة.

إليك السيناريو الذي سنختبره: سننشئ مكتبةً تتعقب قيمةً مقابل قيمة قصوى وترسل رسائل بناءً على مدى قرب القيمة الحالية من القيمة القصوى، بحيث يمكن استخدام هذه المكتبة لتتبع حصة المستخدم لعدد استدعاءات الواجهة البرمجية المسموح بإجرائها على سبيل المثال.

ستوفر مكتبتنا فقط وظيفة تتبع مدى قرب الحد الأعظمي للقيمة وما يجب أن تكون عليه الرسائل في أي وقت، ومن المتوقع أن توفّر التطبيقات التي تستخدم مكتبتنا آلية إرسال الرسائل، يمكن للتطبيق وضع رسالة في التطبيق، أو إرسال بريد إلكتروني، أو إرسال رسالة نصية، أو شيء آخر، إذ لا تحتاج المكتبة إلى معرفة هذا التفصيل، وكل ما تحتاجه هو شيء ينفِّذ سمة سنوفّرها تدعى Messenger.

تظهر الشيفرة 20 الشيفرة البرمجية الخاصة بالمكتبة:

اسم الملف: src/lib.rs

pub trait Messenger {
    fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}

impl<'a, T> LimitTracker<'a, T>
where
    T: Messenger,
{
    pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }

    pub fn set_value(&mut self, value: usize) {
        self.value = value;

        let percentage_of_max = self.value as f64 / self.max as f64;

        if percentage_of_max >= 1.0 {
            self.messenger.send("Error: You are over your quota!");
        } else if percentage_of_max >= 0.9 {
            self.messenger
                .send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 0.75 {
            self.messenger
                .send("Warning: You've used up over 75% of your quota!");
        }
    }
}

الشيفرة 20: مكتبة لتتبع مدى قرب قيمة من قيمة العظمى وإرسال تحذير عندما تصل القيمة لمقدار معيّن

أحد الأجزاء المهمة من هذه الشيفرة هو أن سمة Messenger لها تابعٌ واحدٌ يدعى send يقبل مرجعًا ثابتًا يشير إلى self بالإضافة إلى نص الرسالة، وهذه السمة هي الواجهة التي يحتاج الكائن الزائف الخاص بنا إلى تنفيذها بحيث يمكن استخدام الكائن الزائف بنفس الطريقة التي يُستخدم بها الكائن الحقيقي؛ والجزء المهم الآخر هو أننا نريد اختبار سلوك التابع set_value على LimitTracker. يمكننا تغيير ما نمرّره لمعامل value، إلا أن set_value لا تُعيد لنا أي شيء لإجراء تأكيدات assertions عليه؛ إذ نريد أن نكون قادرين على القول أنه على المُرسل أن يرسل رسالة معيّنة إذا أنشأنا LimitTracker بشيء يطبّق السمة Messenger وقيمة معينة لـ max، عندما نمرر أرقامًا مختلفة مثل قيمة value.

نحتاج هنا إلى كائن زائف لتتبُّع الرسائل التي يُطلب منه إرسالها عندما نستدعي send، وذلك بدلًا من إرسال بريد إلكتروني أو رسالة نصية. يمكننا إنشاء نسخة جديدة من كائن زائف وإنشاء LimitTracker يستخدم الكائن الزائف واستدعاء التابع set_value على LimitTracker ثم التحقق من أن الكائن الزائف يحتوي على الرسائل التي نتوقعها. تُظهر الشيفرة 21 محاولة تطبيق كائن زائف لفعل ذلك فقط إلا أن مدقق الاستعارة لن يسمح بذلك.

اسم الملف: src/lib.rs

#[cfg(test)]
mod tests {
    use super::*;

    struct MockMessenger {
        sent_messages: Vec<String>,
    }

    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger {
                sent_messages: vec![],
            }
        }
    }

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            self.sent_messages.push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        let mock_messenger = MockMessenger::new();
        let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);

        limit_tracker.set_value(80);

        assert_eq!(mock_messenger.sent_messages.len(), 1);
    }
}

الشيفرة 21: محاولة لتطبيق MockMessenger غير المسموح به بواسطة مدقق الاستعارة

تعرِّف شيفرة الاختبار السابقة هيكل MockMessenger يحتوي على حقل sent_messages مع قيم Vec من نوع سلسلة نصية String لتتبع الرسائل المطلوب إرسالها؛ كما نعرّف أيضًا دالة مرتبطة associated function بالاسم new لتسهيل إنشاء قيم MockMessenger الجديدة التي تبدأ بلائحة فارغة من الرسائل، ثم نطبّق السمة Messenger على MockMessenger حتى نستطيع إعطاء ملكية MockMessenger لنسخة LimitTracker. نأخذ الرسالة الممرّرة مثل معامل في تعريف التابع send ونخزّنها في قائمة MockMessenger الخاصة بالحقل sent_messages.

نختبر في الاختبار ما يحدث عندما يُطلب من LimitTracker تعيين value لشيء يزيد عن 75 بالمائة من max، إذ نُنشئ أولًا نسخةً جديدةً من MockMessenger تبدأ بقائمة فارغة من الرسائل، ثم نُنشئ LimitTracker جديد ونمرّر له مرجعًا يشير إلى MockMessenger الجديد وقيمة max هي 100. نستدعي التابع set_value على LimitTracker بقيمة 80 والتي تزيد عن 75 بالمئة من 100 ثم نتأكد أن قائمة الرسائل التي يتتبعها MockMessenger يجب أن تحتوي الآن على رسالة واحدة.

ومع ذلك هناك مشكلة واحدة في هذا الاختبار كما هو موضح هنا:

$ cargo test
   Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
  --> src/lib.rs:58:13
   |
2  |     fn send(&self, msg: &str);
   |             ----- help: consider changing that to be a mutable reference: `&mut self`
...
58 |             self.sent_messages.push(String::from(message));
   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable

For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit-tracker` due to previous error
warning: build failed, waiting for other jobs to finish...

لا يمكننا تعديل MockMessenger لتتبع الرسائل لأن التابع send يأخذ مرجعًا ثابتًا يشير إلى self، كما لا يمكننا أيضًا تنفيذ الاقتراح في نص الخطأ الذي يدلنا على استخدام ‎&mut self بدلًا من ذلك لأن بصمة التابع send لن تتطابق مع بصمة تعريف السمة Messenger (ننصحك بقراءة رسالة الخطأ بنفسك).

هذه هي الحالة التي يُمكن أن يساعد فيها نمط التغيير الداخلي، إذ سنخزن sent_messages داخل <RefCell<T، ومن ثم سيتمكن التابع send من تعديل sent_messages لتخزين الرسائل التي رأيناها، وتظهر الشيفرة 22 كيف يبدو التغيير:

اسم الملف: src/lib.rs

#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::RefCell;

    struct MockMessenger {
        sent_messages: RefCell<Vec<String>>,
    }

    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger {
                sent_messages: RefCell::new(vec![]),
            }
        }
    }

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            self.sent_messages.borrow_mut().push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        // --snip--

        assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
    }
}

الشيفرة 22: استخدام <RefCell<T لتغيير قيمة داخلية بينما تكون القيمة الخارجية ثابتة

أصبح حقل sent_messages الآن من النوع <<RefCell<Vec<String بدلًا من <Vec<String. نُنشئ في الدالة new نسخةً جديدةً من <<RefCell<Vec<String حول الشعاع الفارغ.

لا يزال المعامل الأول يمثّل استعارة self ثابتة تتطابق مع تعريف السمة، ولتطبيق التابع send نستدعي borrow_mut على <<RefCell<Vec<String في self.sent_messages للحصول على مرجع متغيّر للقيمة داخل <<RefCell<Vec<String التي تمثّل الشعاع، ومن ثم يمكننا أن نستدعي push على المرجع المتغيّر الذي يشير إلى الشعاع لتتبع الرسائل المرسلة أثناء الاختبار.

التغيير الأخير الذي يتعين علينا إجراؤه هو ضمن التأكيد assertion لمعرفة عدد العناصر الموجودة في الشعاع الداخلي، ولتحقيق ذلك نستدعي borrow على <<RefCell<Vec<String للحصول على مرجع ثابت يشير إلى الشعاع.

الآن بعد أن رأيت كيفية استخدام المؤشر <RefCell<T لنتعمق في كيفية عمله.

تتبع عمليات الاستعارة وقت التنفيذ عن طريق <RefCell<T

نستخدم عند إنشاء مراجع متغيّرة وثابتة الصيغة & و & mut على التوالي، بينما نستخدم في <RefCell<T التابعين borrow و borrow_mut اللذين يعدّان جزءًا من واجهة برمجية آمنة تنتمي إلى <RefCell<T. يُعيد التابع borrow نوع المؤشر الذكي <Ref<T ويُعيد التابع borrow_mut نوع المؤشر الذكي <RefMut<T، وينفّذ كلا النوعين السمة Deref لذلك يمكننا معاملتهما على أنهما مراجع نمطيّة regular references.

يتعقّب المؤشر <RefCell<T عدد المؤشرات الذكية النشطة حاليًا من النوعين <Ref<T و <RefMut<T، وفي كل مرة نستدعي فيها التابع borrow يزيد المؤشر <RefCell<T من عدد عمليات الاستعارة النشطة الثابتة، وعندما تخرج قيمة من النوع <Ref<T عن النطاق، ينخفض عدد الاستعارات الثابتة بمقدار واحد. يتيح لنا المؤشر <RefCell<T الحصول على العديد من الاستعارات الثابتة أو استعارة واحدة متغيّرة في أي وقت تمامًا مثل قواعد الاستعارة وقت التصريف.

إذا حاولنا انتهاك هذه القواعد، سيهلع تنفيذ <RefCell<T وقت التنفيذ بدلًا من الحصول على خطأ تصريفي كما اعتدنا حصوله مع المراجع. تظهر الشيفرة 23 تعديلًا في تطبيق الدالة send من الشيفرة 22، ونحاول هنا إنشاء استعارتين نشطتين متغيّرتين لنفس النطاق عمدًا لتوضيح أن <RefCell<T سيمنعنا من فعل ذلك وقت التنفيذ.

اسم الملف: src/lib.rs

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            let mut one_borrow = self.sent_messages.borrow_mut();
            let mut two_borrow = self.sent_messages.borrow_mut();

            one_borrow.push(String::from(message));
            two_borrow.push(String::from(message));
        }
    }

[الشيفرة 23: إنشاء مرجعين متغيّرين في النطاق ذاته لملاحظة هلع <RefCell<T>]

نُنشئ متغير اسمه one_borrow للمؤشر الذكي <RefMut<T الذي أُعيد من borrow_mut، ثم نُنشئ استعارةً متغيّرة أخرى بنفس الطريقة في المتغير two_borrow، مما يؤدي إلى إنشاء مرجعين متغيّرين في النطاق ذاته وهو أمر غير مسموح به.

عندما ننفذ الاختبارات لمكتبتنا، تُصرَّف الشيفرة البرمجية الموجودة في الشيفرة 23 دون أي أخطاء إلا أن الاختبار سيفشل:

$ cargo test
   Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
    Finished test [unoptimized + debuginfo] target(s) in 0.91s
     Running unittests src/lib.rs (target/debug/deps/limit_tracker-e599811fa246dbde)

running 1 test
test tests::it_sends_an_over_75_percent_warning_message ... FAILED

failures:

---- tests::it_sends_an_over_75_percent_warning_message stdout ----
thread 'main' panicked at 'already borrowed: BorrowMutError', src/lib.rs:60:53
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::it_sends_an_over_75_percent_warning_message

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

لاحظ أن الشيفرة هلعت مع الرسالة already borrowed: BorrowMutError، وهذه هي الطريقة التي يتعامل بها المؤشر <RefCell<T مع انتهاكات قواعد الاستعارة عند وقت التنفيذ.

اختيار اكتشاف أخطاء الاستعارة وقت التنفيذ بدلًا من وقت التصريف كما فعلنا هنا يعني أنك من المحتمل أن تجد أخطاءً في الشيفرة الخاصة بك لاحقًا في عملية التطوير، وربما حتى عند نشر الشيفرة البرمجية الخاصة بك ووصولها لمرحلة الإنتاج. قد تتكبّد شيفرتك البرمجية أيضًا خسارةً صغيرةً في الأداء عند وقت التنفيذ وذلك نتيجة لتتبع الاستعارات عند وقت التشغيل بدلًا من وقت التصريف، ومع ذلك فإن استخدام <RefCell<T يجعل من الممكن كتابة كائن زائف يمكنه تعديل نفسه لتتبع الرسائل التي شاهدها أثناء استخدامه في سياق يسمح فقط بالقيم الثابتة. يمكنك استخدام <RefCell<T على الرغم من المقايضات للحصول على وظائف أكثر مما توفره المراجع العادية.

وجود عدة مالكين للبيانات المتغيرة عن طريق استخدام كل من <Rc <T و <RefCell<T

هناك طريقة شائعة لاستخدام <RefCell<T بالاشتراك مع <Rc<T، تذكر أن <Rc<T يتيح لك وجود عدة مالكين لبعض البيانات إلا أنه يمنحك وصولًا ثابتًا إلى تلك البيانات. إذا كان لديك <Rc<T يحتوي على <RefCell<T فيمكنك الحصول على قيمة يمكن أن يكون لها عدة مالكين ويمكنك تغييرها.

على سبيل المثال تذكر مثال قائمة البنية في الشيفرة 18 في المقالة السابقة، إذ استخدمنا <Rc<T للسماح لقوائم متعددة بمشاركة ملكية قائمة أخرى، ونظرًا لأن <Rc<T يحتوي على قيم ثابتة فقط، فلا يمكننا تغيير أي من القيم الموجودة في القائمة بمجرد إنشائها. دعنا نضيف <RefCell<T لاكتساب القدرة على تغيير القيم في القوائم. تُظهر الشيفرة 24 أنه يمكننا تعديل القيم المخزّنة في جميع القوائم وذلك باستخدام <RefCell<T داخل تعريف Cons:

اسم الملف: src/main.rs

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

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

fn main() {
    let value = Rc::new(RefCell::new(5));

    let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));

    let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));
    let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));

    *value.borrow_mut() += 10;

    println!("a after = {:?}", a);
    println!("b after = {:?}", b);
    println!("c after = {:?}", c);
}

الشيفرة 24: استخدام <<Rc<RefCell<i32 لإنشاء List يمكن تغييرها

نُنشئ قيمة بحيث تكون نسخةً من النوع <<Rc<RefCell<i32 ونخزنها في متغير باسم value حتى نتمكن من الوصول إليها مباشرةً لاحقًا، ثم نُنشئ List في a مع متغاير Cons يحمل value، نحتاج هنا إلى استنساخ value بحيث يكون لكل من a و value ملكية للقيمة الداخلية 5 بدلًا من نقل الملكية من value إلى a أو استعارة a من value.

نغلّف القائمة a داخل <Rc<T عند إنشاء القائمتين a و b بحيث يمكن لكلّ من القائمتين الرجوع إلى a وهو ما فعلناه في الشيفرة 18 سابقًا.

نريد إضافة القيمة 10 إلى value بعد إنشاء القوائم في a و b و c، ونحقّق ذلك عن طريق استدعاء borrow_mut على value التي تستخدم ميزة التحصيل التلقائي automatic differencing التي ناقشناها سابقًا في المقال استخدام التوابع methods ضمن الهياكل structs ضمن فقرة بعنوان (أين العامل ‎->‎؟) لتحصيل <Rc<T إلى قيمة <RefCell<T الداخلية. يُعيد التابع borrow_mut المؤشر الذكي <RefMut<T، ومن ثم نستخدم عامل التحصيل عليه ونغير القيمة الداخلية.

عندما ننفذ a و b و c، يمكننا أن نرى أن لجميعهم القيمة المعدلة 15 بدلًا من 5:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished dev [unoptimized + debuginfo] target(s) in 0.63s
     Running `target/debug/cons-list`
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))

يا لهذه الطريقة الجميلة، فقد أصبح لدينا قيمة List ثابتة خارجيًا باستخدام <RefCell<T>، إلا أنه يمكننا استخدام التوابع الموجودة في <RefCell<T التي توفر الوصول إلى قابلية التغيير الداخلية حتى نتمكن من تعديل بياناتنا عندما نحتاج إلى ذلك. تحمينا عمليات التحقق وقت التنفيذ لقواعد الاستعارة من حالات تسابق البيانات data race وفي بعض الأحيان يكون الأمر مستحقًا لمقايضة القليل من السرعة مقابل هذه المرونة في هياكل البيانات التي لدينا. لاحظ أن <RefCell<T لا يعمل مع الشيفرة متعددة الخيوط، ويعدّ <Mutex<T الإصدار الآمن من سلسلة <RefCell<T، وسنناقش <Mutex<T لاحقًا.

ترجمة -وبتصرف- لقسم من الفصل Smart Pointers من كتاب The Rust Programming Language.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...