Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rc<T>، المؤشر الذكي لعد المراجع (Rc<T>, the Reference-Counted Smart Pointer)

في أغلب الحالات، تكون الملكية (ownership) واضحة: أنت تعرف بالضبط أي متغير يمتلك قيمة معينة. ومع ذلك، هناك حالات قد يكون فيها للقيمة الواحدة عدة ملاك. على سبيل المثال، في هياكل بيانات الرسم البياني (graph data structures)، قد تشير عدة حواف (edges) إلى نفس العقدة (node)، وتكون تلك العقدة مملوكة مفاهيميًا لجميع الحواف التي تشير إليها. لا ينبغي تنظيف العقدة ما لم تكن هناك أي حواف تشير إليها وبالتالي ليس لها ملاك.

يجب عليك تمكين الملكية المتعددة (multiple ownership) صراحةً باستخدام نوع Rust المسمى Rc<T>، وهو اختصار لـ عد المراجع (reference counting). يتتبع النوع Rc<T> عدد المراجع لقيمة ما لتحديد ما إذا كانت القيمة لا تزال قيد الاستخدام أم لا. إذا كان هناك صفر من المراجع لقيمة ما، فيمكن تنظيف القيمة دون أن تصبح أي مراجع غير صالحة.

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

نستخدم النوع Rc<T> عندما نريد تخصيص بعض البيانات على الكومة (heap) لأجزاء متعددة من برنامجنا لقراءتها ولا يمكننا تحديد وقت الترجمة (compile time) أي جزء سينتهي من استخدام البيانات آخراً. إذا كنا نعرف أي جزء سينتهي آخراً، فيمكننا ببساطة جعل هذا الجزء هو مالك البيانات، وستدخل قواعد الملكية العادية المفروضة في وقت الترجمة حيز التنفيذ.

لاحظ أن Rc<T> مخصص للاستخدام فقط في سيناريوهات الخيط الواحد (single-threaded). عندما نناقش التزامن (concurrency) في الفصل 16، سنغطي كيفية إجراء عد المراجع في البرامج متعددة الخيوط (multithreaded).

مشاركة البيانات (Sharing Data)

دعنا نعود إلى مثال قائمة cons في القائمة 15-5. تذكر أننا عرفناها باستخدام Box<T>. هذه المرة، سننشئ قائمتين تشتركان في ملكية قائمة ثالثة. مفاهيميًا، يبدو هذا مشابهًا للشكل 15-3.

A linked list with the label 'a' pointing to three elements. The first element contains the integer 5 and points to the second element. The second element contains the integer 10 and points to the third element. The third element contains the value 'Nil' that signifies the end of the list; it does not point anywhere. A linked list with the label 'b' points to an element that contains the integer 3 and points to the first element of list 'a'. A linked list with the label 'c' points to an element that contains the integer 4 and also points to the first element of list 'a' so that the tails of lists 'b' and 'c' are both list 'a'.

الشكل 15-3: قائمتان، b و c تشتركان في ملكية قائمة ثالثة، a

سننشئ القائمة a التي تحتوي على 5 ثم 10. بعد ذلك، سننشئ قائمتين إضافيتين: b التي تبدأ بـ 3 و c التي تبدأ بـ 4. ستستمر كل من القائمتين b و c بعد ذلك إلى القائمة a الأولى التي تحتوي على 5 و 10. بمعنى آخر، ستشترك كلتا القائمتين في القائمة الأولى التي تحتوي على 5 و 10.

محاولة تنفيذ هذا السيناريو باستخدام تعريفنا لـ List مع Box<T> لن تنجح، كما هو موضح في القائمة 15-17.

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));
}

عندما نترجم هذا الكود، نحصل على هذا الخطأ:

$ 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
   |
note: if `List` implemented `Clone`, you could clone the value
  --> src/main.rs:1:1
   |
 1 | enum List {
   | ^^^^^^^^^ consider implementing `Clone` for this type
...
10 |     let b = Cons(3, Box::new(a));
   |                              - you could clone this value

For more information about this error, try `rustc --explain E0382`.
error: could not compile `cons-list` (bin "cons-list") due to 1 previous error

تمتلك متغيرات Cons البيانات التي تحملها، لذا عندما ننشئ القائمة b يتم نقل a إلى b وتمتلك b القائمة a. بعد ذلك، عندما نحاول استخدام a مرة أخرى عند إنشاء c لا يُسمح لنا بذلك لأن a قد تم نقلها.

يمكننا تغيير تعريف Cons ليحمل مراجع (references) بدلاً من ذلك، ولكن سيتعين علينا حينها تحديد معاملات مدة الحياة (lifetime parameters). من خلال تحديد معاملات مدة الحياة، سنحدد أن كل عنصر في القائمة سيعيش على الأقل طالما تعيش القائمة بأكملها. هذا هو الحال بالنسبة للعناصر والقوائم في القائمة 15-17، ولكن ليس في كل سيناريو.

بدلاً من ذلك، سنغير تعريفنا لـ List لاستخدام Rc<T> بدلاً من Box<T> كما هو موضح في القائمة 15-18. سيحمل كل متغير Cons الآن قيمة و Rc<T> يشير إلى List. عندما ننشئ b بدلاً من أخذ ملكية a سنقوم باستنساخ (clone) الـ Rc<List> الذي يحمله a وبذلك نزيد عدد المراجع من واحد إلى اثنين ونسمح لـ a و b بمشاركة ملكية البيانات في ذلك الـ Rc<List>. سنقوم أيضًا باستنساخ a عند إنشاء c مما يزيد عدد المراجع من اثنين إلى ثلاثة. في كل مرة نستدعي فيها Rc::clone سيزداد عدد المراجع للبيانات داخل Rc<List> ولن يتم تنظيف البيانات ما لم يكن هناك صفر من المراجع إليها.

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));
}

نحتاج إلى إضافة عبارة use لجلب Rc<T> إلى النطاق (scope) لأنه ليس في التمهيد (prelude). في main ننشئ القائمة التي تحمل 5 و 10 ونخزنها في Rc<List> جديد في a. بعد ذلك، عندما ننشئ b و c نستدعي دالة Rc::clone ونمرر مرجعًا إلى Rc<List> في a كمعامل.

كان بإمكاننا استدعاء a.clone() بدلاً من Rc::clone(&a) ولكن عرف Rust هو استخدام Rc::clone في هذه الحالة. لا يقوم تنفيذ Rc::clone بعمل نسخة عميقة (deep copy) لجميع البيانات كما تفعل تنفيذات clone لمعظم الأنواع. استدعاء Rc::clone يزيد فقط من عداد المراجع، وهو ما لا يستغرق الكثير من الوقت. يمكن أن تستغرق النسخ العميقة للبيانات الكثير من الوقت. باستخدام Rc::clone لعد المراجع، يمكننا التمييز بصريًا بين أنواع النسخ العميقة من الاستنساخ وأنواع الاستنساخ التي تزيد من عداد المراجع. عند البحث عن مشاكل الأداء في الكود، نحتاج فقط إلى مراعاة استنساخ النسخ العميق ويمكننا تجاهل استدعاءات Rc::clone.

الاستنساخ لزيادة عداد المراجع (Cloning to Increase the Reference Count)

دعنا نغير مثالنا العملي في القائمة 15-18 حتى نتمكن من رؤية عدادات المراجع تتغير بينما ننشئ ونسقط المراجع إلى Rc<List> في a.

في القائمة 15-19، سنغير main بحيث يكون لها نطاق داخلي حول القائمة c؛ ثم يمكننا رؤية كيف يتغير عداد المراجع عندما تخرج c عن النطاق.

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

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

// --snip--

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));
}

في كل نقطة في البرنامج حيث يتغير عداد المراجع، نقوم بطباعة عداد المراجع، والذي نحصل عليه من خلال استدعاء دالة Rc::strong_count. تمت تسمية هذه الدالة strong_count بدلاً من count لأن النوع Rc<T> يحتوي أيضًا على weak_count؛ سنرى فيمَ يُستخدم weak_count في “منع دورات المراجع باستخدام Weak<T>.

يطبع هذا الكود ما يلي:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished `dev` profile [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 (Drop trait) بتقليل عداد المراجع تلقائيًا عندما تخرج قيمة Rc<T> عن النطاق.

ما لا يمكننا رؤيته في هذا المثال هو أنه عندما تخرج b ثم a عن النطاق في نهاية main يكون العداد 0، ويتم تنظيف Rc<List> تمامًا. يسمح استخدام Rc<T> لقيمة واحدة بامتلاك عدة ملاك، ويضمن العداد بقاء القيمة صالحة طالما بقي أي من الملاك موجودًا.

عبر المراجع غير القابلة للتغيير (immutable references)، يسمح لك Rc<T> بمشاركة البيانات بين أجزاء متعددة من برنامجك للقراءة فقط. إذا كان Rc<T> يسمح لك بامتلاك مراجع متعددة قابلة للتغيير (mutable references) أيضًا، فقد تنتهك إحدى قواعد الاستعارة (borrowing rules) التي نوقشت في الفصل 4: يمكن أن تؤدي الاستعارات المتعددة القابلة للتغيير لنفس المكان إلى سباقات البيانات (data races) وعدم الاتساق. لكن القدرة على تغيير البيانات مفيدة جدًا! في القسم التالي، سنناقش نمط القابلية للتغيير الداخلية (interior mutability pattern) والنوع RefCell<T> الذي يمكنك استخدامه بالاقتران مع Rc<T> للعمل مع قيد عدم القابلية للتغيير هذا.