تُعد قابلية التغيير الداخلي 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.
اقرأ أيضًا
- المقال التالي: حلقات المرجع Reference Cycles وتسببها بتسريب الذاكرة Memory Leak في لغة رست
-
المقال السابق: المؤشر
Rc<T>
الذكي واستخدامه للإشارة إلى عدد المراجع في لغة رست Rust - المؤشرات الذكية Smart Pointers في رست Rust
- الملكية Ownership في لغة رست
- البرمجة بلغة رست التحقق من المراجع References عبر دورات الحياة Lifetimes في لغة رست
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.