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

المؤشر Rc<T>‎ الذكي واستخدامه للإشارة إلى عدد المراجع في لغة رست Rust


Naser Dakhel

مبدأ الملكية 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.

15-03.png

الشكل 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.

اقرأ أيضًا


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

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

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



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

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

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

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   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.


×
×
  • أضف...