مبدأ الملكية ownership واضح في معظم الحالات، إذ تسمح لك الملكية بمعرفة أي متغير يملك قيمةً ما، ولكن هناك حالات تكون فيها القيمة الواحدة مملوكةً من أكثر من مالك، فمثلًا في شعبة هيكل البيانات graph data structure قد تؤشر العديد من الأضلع edges إلى العقدة node ذاتها، وبالتالي تمتلك هذه العقدة كل الأضلع التي تشير إليها. لا يجب تحرير العقدة من الذاكرة إلا في حال عدم وجود أي ضلع يشير إليها وبالتالي عدم امتلاكها من قبل أحد.
يمكنك تمكين وجود عدة مالكين صراحةً باستخدام النوع Rc<T>
وهو اختصار لعدّ المرجع reference counting، إذ يُحصي النوع Rc<T>
الخاص برست عدد المراجع التي تشير إلى قيمة محددة لتحديد فيما إذا كانت تلك القيمة قيد الاستخدام أم لا، وإذا لم يكن هناك أي مراجع تشير للقيمة، عندها يمكن تحرير القيمة دون التسبب بجعل أي مرجع غير صالح.
تخيل Rc<T>
مثل تلفاز في غرفة الجلوس، فعندما يدخل شخص الغرفة ليشاهد التلفاز يشغله، كما يمكن لآخرين القدوم للغرفة والمشاهدة أيضَا، وعندما يغادر آخر شخص الغرفة يطفئ التلفاز لأنه لم يعد يُستخدم، ولكن إذا أطفأ أحدهم التلفاز بينما يشاهده الآخرون فسيغضب الذين يشاهدون التلفاز.
نستخدم النوع Rc<T>
عندما نريد وضع بعض البيانات في الكومة heap بحيث يقرؤها عدة أجزاءٌ مختلفة من برنامجنا ولا نستطيع معرفة أي الأجزاء سينتهي من استخدام هذه البيانات أخيرًا عند تصريف البرنامج. نستطيع جعل الجزء الأخير مالك البيانات إذا كنّا نعرفه وبذلك تُطبَّق قواعد الملكية الاعتيادية لرست عند وقت التصريف.
لاحظ أن Rc<T>
موجود فقط للاستخدام في حالات استخدام الخيط الواحد single-threaded، وسنتحدث عن عدّ المراجع في البرامج ذات الخيوط المتعددة multithreaded لاحقًا عندما نتحدث عن التزامن concurrency.
استخدام Rc لمشاركة البيانات
لنعاود النظر إلى قائمة البنية cons list في الشيفرة 5 من مقال المؤشرات الذكية السابق، تذكّر أننا عرفنا القائمة باستخدام Box<T>
إلا أننا سنُنشئ هذه المرة لائحتين يتشاركان ملكية قائمة ثالثة كما يوضح الشكل 3.
الشكل 3: قائمة b
وقائمة c
يتشاركان ملكية قائمة ثالثة a
ننشئ القائمة a
التي تحتوي على 5 وبعدها 10، ومن ثم ننشئ قائمتين؛ قائمة b
تبدأ بالقيمة 3 وقائمة c
تبدأ بالقيمة 4، إذ ستستمر قيم كل من القائمتين b
و c
ضمن القائمة الثالثة a
التي تحتوي على 5 و10، أي ستتشارك القائمتان القائمة الأولى التي تحتوي على 5 و10.
لن تنجح محاولة تنفيذ هذه الحالة باستخدام تعريفنا للنوع List
مع النوع Box<T>
كما هو موضح في الشيفرة 17:
اسم الملف: src/main.rs
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)); }
الشيفرة 17: شيفرة توضّح أنه ليس من المسموح استخدام قائمتين النوع Box<T>
مع مشاركة ملكيتهما لقائمة ثالثة
عندما نصرّف الشيفرة السابقة نحصل على هذا الخطأ:
$ 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 For more information about this error, try `rustc --explain E0382`. error: could not compile `cons-list` due to previous error
تمتلك متغايرات Cons
البيانات التي تحتفظ بها، لذا عندما ننشئ القائمة b
تنتقل القائمة a
إلى القائمة b
وتمتلك b
القائمة a
، ثم عندما نحاول استخدام a
مجددًا عند إنشاء c
لا يُسمح لنا، وذلك لأن القائمة a
نُقلت.
يمكننا تغيير تعريف Cons
بحيث يخزّن المراجع عوضًا عن ذلك، ولكن علينا عندها تعريف معاملات دورة حياة lifetime parameters، إذ سيستطيع بذلك عنصر في القائمة أن يعيش مدّةً تساوي مدة حياة القائمة على الأقل، وهذه هي حالة العناصر الموجودة ضمن القائمة في الشيفرة 17 ولكن هذا لا ينطبق في كل حالة.
عوضاً عن ذلك سنغير تعريف List
لتستخدم Rc<T>
بدلاً من Box<T>
كما هو موضح في الشيفرة 18. يحفظ كل متغاير Cons
قيمةً بالإضافة لمؤشر Rc<T>
يشير إلى List
، عندما ننشئ b
بدلًا من أخذ ملكية a
، سننسخ Rc<List>
التي يحتفظ بها a
وبذلك نزيد عدد المراجع من 1 إلى 2 ونجعل القائمة a
والقائمة b
تتشارك ملكية البيانات في Rc<List>
، كما سننسخ أيضًا a
عندما ننشئ c
، وبذلك يزيد عدد المراجع من 2 إلى 3 مراجع، وفي كل مرة نستدعي Rc::clone
سيزيد عدد المراجع للبيانات داخل Rc<List>
ولن تُحرَّر البيانات إلا إذا لم يكن هناك أي مرجع يشير إليها.
اسم الملف: src/main.rs
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)); }
الشيفرة 18: تعريف List
التي تستخدم Rc<T>
نحتاج لإضافة تعليمة use
وذلك لإضافة Rc<T>
إلى النطاق لأنه غير موجود في البداية. أنشأنا في الدالة main
قائمة تحتوي على 5 و10 وخزّناها في قيمة من نوع Rc<List>
جديدة ضمن a
، ومن ثمّ استدعينا الدالة Rc::clone
بعد أن أنشأنا b
و c
ومررنا مرجعًا مثل وسيط argument يشير إلى Rc<List>
في a
.
يمكننا استدعاء a.clone()
بدلًا من Rc::clone(&a)
، إلا أن الطريقة الاصطلاحية في رست تستخدم Rc::clone
في هذه الحالة. لا ينسخ تنفيذ Rc::clone
نسخةً فعليةً deep copy للبيانات مثل باقي تنفيذات أنواع clone
الأخرى، إذ يزيد استدعاء الدالة Rc::clone
عدد المراجع وهو ما لا يأخذ وقتًا طويلًا، بينما يأخذ النسخ الفعلي للبيانات وقتًا طويلًا، إلا أنه يمكننا التمييز بصريًا بين النسخ الفعلي والنسخ الذي يزيد عدد المراجع باستخدام الدالة Rc::clone
. نحتاج لأخذ موضوع النسخ الفعلية بالحسبان عندما نبحث عن مشاكل متعلقة بأداء الشيفرة البرمجية وذلك بإهمال استدعاءات Rc::clone
.
نسخ قيمة من النوع Rc يزيد عدد المراجع
لنغير مثالنا الموجود في الشيفرة 18 بحيث يمكننا رؤية تغيّر عدد المراجع عندما ننشئ ونحرّر مرجعًا إلى Rc<T>
في a
.
نغيّر من الدالة main
في الشيفرة 19 بحيث تحتوي على نطاق داخلي حول القائمة c
لكي نستطيع ملاحظة تغيّر قيمة عداد المراجع عندما تخرج c
خارج النطاق.
اسم الملف: src/main.rs
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))))); 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)); }
الشيفرة 19: طباعة عدد المراجع
نطبع عدد المراجع في كل نقطة يتغير فيه قيمته ضمن البرنامج، ونحصل على تلك القيمة باستدعاء الدالة Rc::strong_count
. نسمّي الدالة strong_count
بدلًا من count
، وذلك لأن النوع Rc<T>
يحتوي أيضًا على weak_count
، وسنستخدم weak_count
في مقال لاحق عندما نتحدث عن منع دورات المراجع وتحويل Rc<T>
إلى Weak<T>
. تطبع الشيفرة السابقة ما يلي:
$ cargo run Compiling cons-list v0.1.0 (file:///projects/cons-list) Finished dev [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>
من النطاق.
ما لا نستطيع رؤيته في هذا المثال هو خروج a
من النطاق بعد خروج b
في نهاية الدالة main
، ويُضبط عدد المراجع فيما بعد إلى القيمة 0، وعندها تصبح Rc<List>
محرَّرة تمامًا. يسمح استخدام Rc<T>
بأن يكون لقيمة واحدة أكثر من مالك، كما أن عدد المراجع يؤكد أن القيمة لا تزال صالحةً طالما لا يزال هناك مالك.
يسمح Rc<T>
عن طريق المراجع الثابتة immutable مشاركة البيانات بين أقسام متعددة من البرنامج للقراءة فقط. قد تخرق بعض قوانين الاستعارة -التي ناقشناها سابقًا في مقال المراجع References والاستعارة Borrowing والشرائح Slices في لغة رست- في حلقات المرجع Reference Cycles وتسببها بتسريب الذاكرة Memory Leak في لغة رست Rust إذا سمح Rc<T>
بوجود عدة مراجع متغيّرة أيضًا؛ إذ قد تسبب الاستعارات المتعددة المتغيّرة لنفس المكان حالة تعارض وتناقض للبيانات data race، إلا أن القدرة على تغيير البيانات مفيدة جدًا. سنناقش في القسم القادم النمط الداخلي المتغيّر interior mutability pattern، إضافةً إلى النوع RefCell<T>
الذي يمكن استخدامه مع Rc<T>
لتجاوز قيد الثبات هذا.
ترجمة -وبتصرف- لقسم من الفصل Smart Pointers من كتاب The Rust Programming Language.
اقرأ أيضًا
- المقال التالي: المؤشر الذكي Refcell<T> ونمط قابلية التغيير الداخلي interior mutability في لغة رست
- المقال السابق: تنفيذ شيفرة برمجية عند تحرير الذاكرة cleanup باستخدام السمة Drop في لغة رست
- الملكية Ownership في لغة رست
- المؤشرات الذكية Smart Pointers في رست Rust
- التحقق من المراجع References عبر دورات الحياة Lifetimes في لغة رست
- البرمجة بلغة رست كيفية استخدام النوع HashMap لتخزين البيانات في رست Rust
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.