كل بنية بنيتَها حتى الآن تحترم البديهيتين بصرامة هندسية: مالك واحد، وتعديل حصري أو قراءة مشتركة لا كلاهما. لكن بعض الأشكال الحقيقية تتمرد على هذا: عقدة شجرة يشير إليها أبوها وإخوتها كمرجع أخوي، أو ذاكرة تخزين مؤقت (cache) يريد كل من يحمل مرجعًا لها أن يحدّثها. هذا الإقليم لا يكسر البديهيتين — يريك كيف تتفاوضان.
الدرس ١: Box<T> — التمليك، فقط في عنوان آخر
الرجوع لبذرة الإقليم ٠٣
في ٠٣-ب، اقترح المترجم Box<T> لحلّ حجم enum العودي "اللانهائي" منطقيًا. الآن السبب الكامل: Box<T> هي أبسط مؤشر ذكي في اللغة — قيمة على الـ heap، مالكها واحد بالضبط (لا تليين هنا بعد — Box تحترم البديهية الأولى حرفيًا، فقط تنقل مكان الإقامة من الـ stack إلى الـ heap). حجم Box<T> ثابت دائمًا (مؤشر واحد) بغضّ النظر عن حجم T — ولهذا يحلّ مشكلة الحجم العودي: enum Expr { Add(Box<Expr>, Box<Expr>), ... } معروفة الحجم لأن كل فرع يحمل مؤشرًا لا قيمة مضمَّنة.
استعمالات Box الثلاثة التي قابلتَها فعلًا دون تسميتها:
- أنواع عودية (٠٣-ب) — حجم غير معروف بلا مؤشر.
dyn Trait(٠٤، ٠٦) —Box<dyn Shape>: حجم كائن الـ trait غير معروف وقت الترجمة (أنواع مختلفة، أحجام مختلفة)؛Boxيعطيه حجمًا ثابتًا (مؤشر) كي يعيش فيVecأو يُعاد من دالة.- نقل بيانات ضخمة بأرخص كلفة — نقل
Box<[u8; 1_000_000]>ينقل مؤشرًا واحدًا (٨ بايتات)، لا مليون بايت.
لماذا *my_box يعمل كأنه T مباشرة؟
let b = Box::new(5);
println!("{}", *b + 1); // إلغاء الإشارة اليدوي
let s = Box::new(String::from("hi"));
println!("{}", s.len()); // بلا * إطلاقًا — كيف؟
Box<T> تطبّق trait Deref. الاستدعاء s.len() يعمل عبر الإلغاء التلقائي التسلسلي (deref coercion): لا توجد len() على Box<String>، فيحاول المترجم *s (تصبح String)؛ لا توجد أيضًا مباشرة الطريقة المطلوبة؟ يحاول Deref مجددًا حتى يصل &str — هذا بالضبط ما يجعل &String يمرّ حيث تُطلب &str (البذرة المؤجَّلة من الإقليم ٠٢). ابنِ هذا يدويًا لتفهمه من جذوره لا كسحر:
struct MyBox<T>(T);
impl<T> std::ops::Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T { &self.0 }
}
my_box_val الآن تُترجَم فعليًا إلى (my_box_val.deref()). هذا كل ما في الأمر — لا خصوصية للمترجم، trait عادي يطبّقه أي نوع، بما فيها أنواعك.
الدرس ٢: Rc<T> — القانون الأول يتفاوض
اللغز
جرّب بناء شجرة حيث لكل عقدة أب واحد، لكن الأب نفسه تشير إليه أكثر من عقدة (أو: كائن إعدادات مشترك، يريد ثلاثة مكوّنات مستقلة الاحتفاظ بنسخة منه معًا). بالبديهية الأولى الصارمة: مالك واحد. فمن يكون؟ لو نقلتَ الملكية لأول من طلبها، البقية فقدت الوصول (تذكّر إحباط رقصة الملكية في ٠١). استعارة (&T) لا تكفي أيضًا — الاستعارة تحتاج مالكًا حيًّا تستعير منه، ومتغيّرًا محليًا يضمن عمرًا كافيًا (الإقليم ٠٥)؛ هنا لا يوجد "مالك أصلي" واحد أطول عمرًا من البقية بطبيعة المشكلة. صمّم آلية ملكية "أكثر من واحد بأمان" بنفسك قبل القراءة: ما أرخص معلومة يمكن أن يتشاركها عدة أطراف لمعرفة "هل أنا آخر من يستعمل هذا؟"
الحل: عدّاد مراجع
use std::rc::Rc;
let a = Rc::new(String::from("shared"));
let b = Rc::clone(&a); // لا تُنسَخ السلسلة — يُنسَخ المؤشر، ويُزاد عدّاد
let c = a.clone(); // مكافئ تمامًا (Rc::clone اصطلاحي، أوضح نية)
println!("{}", Rc::strong_count(&a)); // 3
Rc::clone لا يستنسخ T — ينسخ مؤشرًا صغيرًا ويزيد عدّادًا مشتركًا بمقدار ١. حين يُسقَط (drop) كل Rc، يُنقَص العدّاد؛ فقط حين يصل صفرًا تُسقَط القيمة الفعلية. هذا هو "التفاوض": البديهية الأولى لم تُكسَر — تأمّل من هو "المالك" فعليًا هنا: ليس أي Rc فردي، بل المجموعة الحيّة منها ككل تملك القيمة، وآخر عضو فيها يغادر هو من يُنفّذ التحرير الفعلي. مالك واحد، فقط تعريف "الواحد" توسّع من فرد إلى مجموعة تعرف حجمها بنفسها.
القيد: Rc<T> تعطيك &T فقط (لن تحصل أبدًا على &mut T منها مباشرة) — لماذا؟ لو سمحت بالتعديل مباشرة، وهناك نسخ أخرى من Rc تقرأ الآن، لخرقتَ البديهية الثانية فورًا. Rc تحل مشكلة من يملك، لكنها تبقي مشكلة من يعدّل بلا حل — وهنا يظهر البطل التالي.
الدرس ٣: RefCell<T> — القانون الثاني يتفاوض
اللغز
تريد بيانات يشاركها عدة Rc، لكن أحدهم أحيانًا يحتاج تعديلها. البديهية الثانية تمنع ذلك وقت الترجمة بصرامة — والمترجم، مهما كانت منطقتك برمجيًا صحيحة (مثلًا: تعلم يقينًا أن التعديل لن يحدث إلا حين لا أحد غيرك يقرأ، بحكم منطق البرنامج لا بحكم الأنواع)، لا يستطيع إثبات ما لا يراه في الأنواع. صمّم آلية تسمح بهذا التعديل، مع الاحتفاظ بضمان "لا قراءة وكتابة متزامنتين" — لكن اسمح للفحص أن ينتقل من مكان لآخر (لا من "موجود" إلى "معدوم").
الحل: فحص وقت التشغيل بدل وقت الترجمة
use std::cell::RefCell;
let cell = RefCell::new(5);
*cell.borrow_mut() += 1; // استعارة حصرية، مؤقتة
println!("{}", cell.borrow()); // استعارة قراءة، مؤقتة
let r1 = cell.borrow();
let r2 = cell.borrow_mut(); // PANIC وقت التشغيل: already borrowed
RefCell<T> تحمل — إلى جانب T — علمًا داخليًا يتتبع عدد الاستعارات النشطة حاليًا، تمامًا كما يفعل borrow checker، لكن بيانات حقيقية تُفحص كل نداء بدل تحليل ساكن وقت الترجمة. .borrow() تعيد Ref<T> (تتصرف كـ &T)، .borrow_mut() تعيد RefMut<T> (تتصرف كـ &mut T) — وكلاهما، عند إسقاطهما (نهاية نطاقهما)، تُنقص العلم. خرق البديهية الثانية فعليًا (استعارتان حصريتان، أو حصرية مع مشتركة، في آنٍ) يُفزِّع البرنامج (panic) بدل أن يُرفَض ترجمته.
فرّق هذا بدقة عن unsafe (الإقليم ٠٩) — فخّ مفاهيمي شائع: RefCell كود آمن بالكامل (safe Rust) — الفحص لم يُحذَف، فقط تأخّر توقيته من الترجمة إلى التشغيل، وعقوبة الخطأ panic منضبط، لا فساد ذاكرة. unsafe لاحقًا يزيل الفحص كليًا، والعقوبة undefined behavior. الفرق جوهري: هذا تفاوض داخل حدود الأمان؛ ذاك خروج منها بوعي.
Rc<RefCell<T>> — الثنائي الذي يظهر في كل كود Rust بمراجع مشتركة قابلة للتعديل
let shared = Rc::new(RefCell::new(vec![1, 2, 3]));
let handle = Rc::clone(&shared);
handle.borrow_mut().push(4);
println!("{:?}", shared.borrow()); // [1, 2, 3, 4] — التعديل مرئي عبر كل المقابض
هذا أقرب ما تصل إليه في Rust الآمنة لسلوك "مرجع كائن عادي" في Java/Python/ JS — لكنك الآن ترى بالضبط الآلة التي تلك اللغات تخفيها عنك (وتدفع ثمنها دومًا، بلا خيار): عدّاد مراجع + فحص استعارة وقت تشغيل، بدل أن تكون هذه هي الحالة الافتراضية المجانية الوحيدة.
الوفاء بتحفّظ الإقليم ٠١: تسريب Rc الدائري
تذكّر ادعاءنا في ٠١: "الخروج من النطاق يحرّر القيمة دومًا"، مع تحفّظ صغير مؤجَّل. هذا موعده: لو أنشأتَ دائرة من Rc — عقدة أ تملك Rc لعقدة ب، وب تملك Rc يعود لأ — لن يصل عدّاد أيٍّ منهما للصفر أبدًا، حتى لو خرج كل شيء من النطاق الخارجي. تسريب حقيقي، دائم، لكنه "آمن": لا فساد ذاكرة، فقط هدر — ولهذا لا يُصنَّف كخرق لضمان الأمان في Rust (الضمان عن الفساد، لا عن التسريب؛ التسريب مسموح رسميًا بالمواصفة).
الحل الاصطلاحي: Weak<T> — مرجع لا يُحتسَب في عداد الملكية (strong_ count)، بل في عدّاد منفصل (weak_count). يُستعمَل نموذجيًا للاتجاه العكسي في العلاقات الهرمية (الابن يملك أباه بـ Rc ممنوع منطقيًا — هذا ما يصنع الدائرة؛ الابن يشير لأبيه بـ Weak بدلًا من ذلك):
use std::rc::{Rc, Weak};
let weak_ref: Weak<T> = Rc::downgrade(&some_rc);
match weak_ref.upgrade() { // Option<Rc<T>> — قد يكون القريب مات فعلًا!
Some(rc) => { /* استعمله */ }
None => { /* لم يعد موجودًا */ }
}
لاحظ upgrade() تُعيد Option — لأن Weak لا تضمن بقاء القيمة حيّة (لا تحسب في العدّاد، فقد ينخفض للصفر ويُسقَط الأصل رغم وجود Weak تشير إليه). هنا Option تعمل بمعناها الأصلي تمامًا: "قد تكون غائبة، وسأجبرك على الفحص."
معلم البناء: بنية بيانات "مستحيلة"
ابنِ شجرة صغيرة (شبيهة بشجرة DOM مصغّرة، أو نظام ملفات وهمي: مجلدات تحوي ملفات ومجلدات فرعية) بخاصيتين تجعلانها مستحيلة بالملكية الأحادية الصرفة:
المتطلبات:
- كل عقدة تحوي قيمة (نصًّا مثلًا)، و
Vecمن الأبناء، ومرجعًا للأب. - عقدة
Aيجب أن تسمح بـ: (١) إضافة ابن جديد لها من خارج الشجرة عبر مقبض محفوظ سابقًا، (٢) تعديل قيمة عقدة عميقة داخل الشجرة عبر مقبض خارجي محفوظ لها (لا عبر إعادة المشي من الجذر)، (٣) المشي من أي عقدة صعودًا حتى الجذر عبر مراجع الأب. - بنية العقدة تلميحية (صمّم التفاصيل، لكن هذا هو الشكل الجوهري):
children: Vec<Rc<RefCell<Node>>>،parent: RefCell<Weak<Node>>(أوWeak<RefCell<Node>>بحسب تصميمك — فكّر أيّهما أصح ولماذا). - اكتب دالة تطبع مسار عقدة من الجذر حتى نفسها (تسلق بالمراجع الأبوية، وتعالج حالة
upgrade()التي تُعيدNoneبشكل صريح لا بـunwrapأعمى).
لماذا "مستحيلة": جرّب أولًا — بجدّية، لا شكليًا — بناءها بـ Box<Node> فقط للأبناء ومرجع &Node عادي للأب. ستصطدم بجدار: مرجع الأب يحتاج عمرًا (الإقليم ٠٥) لا يمكن التعبير عنه بلا تجميد الشجرة كلها عن أي تعديل لاحق (البديهية الثانية ترفض تعديل عقدة بينما مرجع أبوي حيّ يستعير منها من زاوية أخرى). هذا الجدار هو بالضبط الدافع الذي يجعل Rc<RefCell<Weak<...>>>> ضرورة لا ترفًا. اكتب فقرة قصيرة توثّق أين بالضبط اصطدمتَ بالجدار قبل أن تنتقل للحل — هذا التوثيق أهم من الحل نفسه لترسيخ الفهم.
الألغاز
لغز ٠٧-أ: عدّاد يتحدث. أنشئ Rc::new(String::from("x"))، واطبع Rc::strong_count بعد كل حدث: عند الإنشاء، بعد clone() مرتين، داخل {} نطاق يحوي clone() ثالثًا (واطبع العدّاد قبل وبعد نهاية ذلك النطاق)، وبعد drop() يدوي صريح لأحد النسخ. توقّع الأرقام كتابةً قبل التشغيل.
لغز ٠٧-ب: الفزّاعة المضبوطة. اكتب دالة تأخذ &RefCell<Vec<i32>> وتحاول — عمدًا — الحصول على borrow_mut() مرتين متداخلتين (الثانية قبل إسقاط الأولى) لإثارة الـ panic بيدك ومشاهدة رسالته كاملة. ثم أصلحها بحيث ينتهي نطاق الاستعارة الأولى (عبر {} صريحة أو بإسناد النتيجة ثم استعمالها فورًا) قبل طلب الثانية. اكتب سطرًا: كيف تُفرّق هذه الرسالة (already borrowed: BorrowMutError) عن رسالة borrow checker وقت الترجمة التي رأيتها في الإقليم ٠٢ — من حيث متى عرفتَ بالمشكلة (قبل التشغيل أم أثناءه)؟
لغز ٠٧-ج: الدائرة المتعمَّدة. اصنع عقدتين تشيران لبعضهما بـ Rc<RefCell<...>> (كلٌّ يحمل حقلًا يشير للآخر). أضف impl Drop بسيطة تطبع رسالة (كما في ٠١-ج) لكلتا العقدتين. شغّل برنامجًا يُنشئهما ثم يُسقطهما (تخرج من النطاق) — راقب: هل تُطبَع رسالتا الـ Drop؟ ثم أعد التصميم بحيث يشير أحدهما للآخر بـ Weak بدل Rc، وكرّر التجربة. قارن الطباعتين واشرح الفرق بمصطلح "آخر مالك يغادر" من الدرس ٢.
الخلاصة
- العقدة الجديدة:
Box<T>(تمليك في عنوان آخر، أساس الأنواع العودية وdyn)،Rc<T>(البديهية الأولى تتفاوض: الملكية تنتقل من فرد لمجموعة تعرف حجمها)،RefCell<T>(البديهية الثانية تتفاوض: الفحص ينتقل من الترجمة للتشغيل، والعقوبة panic منضبط لا فساد)، وWeak<T>(كسر الدوائر). لا قانون انكسر — كلاهما توسّع بأدوات صريحة، مرئية في النوع. - الوصلة للخلف: تحفّظ الإقليم ٠١ عن التسريب وُفِّي هنا بالكامل. وبذرة
&Stringتمرّ حيث تُطلب&strمن الإقليم ٠٢ انحلّت عبرDeref. - الوصلة للأمام: كل ما بنيتَه في هذا الإقليم —
Rc،RefCell— مصمَّم لخيط تنفيذ واحد. عدّادRcنفسه ليس محميًا من التعديل المتزامن؛ لو زاده خيطان في نفس اللحظة تمامًا فقد يُخطئ العدّ (سباق بيانات كلاسيكي، من نفس عائلة أخطاء C10K التي عرفتها في fullstack).
والآن المفاجأة: البديهيتان اللتان صُمِّمتا بالكامل من أجل ذاكرة خيط واحد — تبيّن لاحقًا، بالصدفة الهندسية الأجمل في تاريخ اللغة، أنهما تحلّان مشكلة أكبر بكثير كانت تؤرّق كل لغة نظام قبل Rust: سباقات البيانات بين الخيوط. الإقليم القادم يريك كيف، ولماذا كانت هذه "الصدفة" ليست صدفة إطلاقًا.