في C كنتَ أنت الـ garbage collector. كل malloc كتبتَه كان يفتح حسابًا في رأسك: من يحرّر هذا؟ متى؟ هل حرّره غيري قبلي؟ هذا الإقليم لا يعلّمك ميزة — يعلّمك كيف حوّلت Rust ذلك الحساب الذهني إلى نظام إثبات يديره المترجم.
الدرس ١: صمّم النظام بنفسك
اللغز التأسيسي (لا تتجاوزه — هو قلب الإقليم)
أمامك أخطاء الذاكرة الثلاثة التي طاردتك في Holberton:
// (١) double free
char *a = malloc(10);
char *b = a;
free(a);
free(b);
// (٢) use-after-free
char *p = malloc(10);
free(p);
p[0] = 'x';
// (٣) leak
char *q = malloc(10);
q = malloc(20); // العشرة الأولى ضاعت للأبد
مهمتك: قبل أن تعرف حلّ Rust، صمّمه أنت. المطلوب مجموعة قواعد يفحصها مترجم (وقت الترجمة فقط — ممنوع أي عدّاد أو فحص وقت التشغيل، وممنوع GC) بحيث يصبح كتابة الأخطاء الثلاثة مستحيلًا نحويًا، مع بقاء البرامج المفيدة ممكنة. اقضِ وقتًا حقيقيًا هنا — ساعة على الأقل، بالورقة والقلم. جرّب قاعدتك على المقاطع الثلاثة وعلى برنامج عادي (مرّر buffer لدالة، أعده، خزّنه في struct). ستكتشف بنفسك أن معظم القواعد الساذجة إما تمنع البرامج المفيدة أو تترك ثغرة. المطلوب أن تشعر بضيق مساحة الحلول — عندها فقط سيبدو حل Rust حتميًا لا اعتباطيًا.
الحل الذي استقرّت عليه اللغة
لاحظ البنية المشتركة في الأخطاء الثلاثة: كلها أسئلة ملكية. من يملك حق التحرير؟ (١) اثنان ظنّا أنهما يملكانه. (٢) واحد استعمل ما لم يعد يملكه. (٣) لا أحد بقي يملكه. فكان الحل:
البديهية الأولى: لكل قيمة مالك واحد بالضبط في كل لحظة. حين يخرج المالك من نطاقه (scope)، تُسقَط القيمة (drop) ويُحرَّر موردها — آليًا، حتميًا، في سطر معروف وقت الترجمة.
اشتقّ منها بنفسك قبل المتابعة: لماذا تقتل هذه الجملةُ الواحدة الأخطاءَ الثلاثة دفعة واحدة؟
- double free: التحرير مسؤولية المالك، والمالك واحد ⇒ تحرير واحد.
- use-after-free: الاستعمال حقّ المالك، ومن فقد الملكية فقد الحق ⇒ يرفضه المترجم قبل الولادة.
- leak: القيمة لا تضيع "بلا مالك" — خروج المالك من النطاق يحرّرها. (احتفظ بتحفّظ صغير على هذه تحديدًا؛ في الإقليم ٧ مفاجأة صادمة عنها.)
الآلية: النقل (move)
الملكية تنتقل بالإسناد وبالتمرير للدوال وبالإرجاع منها:
let s1 = String::from("hello"); // s1 يملك buffer على الـ heap
let s2 = s1; // الملكية انتقلت. s1 ميّت الآن — ليس "نسخة"
println!("{s1}"); // error[E0382]: borrow of moved value: `s1`
بعقلية C: الإسناد نسخ الـ struct السطحي {ptr, len, capacity} — ما كان سيصنع في C مؤشّرين للـ buffer نفسه (قنبلة الخطأ ١). Rust تنسخ البتات نفسها لكنها تشطب المصدر من دفاتر المترجم. لا شيء حدث وقت التشغيل — الشطب قيدٌ محاسبي بحت، وهذه أول مرة ترى فيها نمط اللغة المتكرر: الدفع وقت الترجمة بدل وقت التشغيل.
Copy مقابل Move — لماذا i32 لا تتحرك؟
let a = 5;
let b = a;
println!("{a}"); // يعمل. لماذا لم "يمت" a؟
اشتق المعيار بنفسك: متى يكون النسخ السطحي آمنًا ورخيصًا معًا؟ حين لا تشير القيمة لأي مورد خارجها — الأعداد، bool، char، والمؤشرات المشتركة &T (سترى لماذا في الإقليم ٢). هذه الأنواع تحمل وسم Copy: الإسناد ينسخ ولا ينقل. أما ما يملك موردًا (String, Vec) فالنسخ السطحي كارثة (الخطأ ١ حرفيًا) ⇒ ينتقل. والنسخ العميق لا يحدث أبدًا بصمت — تطلبه صراحة:
let s2 = s1.clone(); // نسخ حقيقي للـ buffer. التكلفة مرئية في الكود
قارن بلغاتك السابقة: C++ تنسخ بعمق بصمت (copy constructor)، وJS/TS تشارك المرجع بصمت. قرار Rust: لا صمت — الرخيص يحدث تلقائيًا، والمكلف يُكتَب.
Drop والحتمية
{
let f = std::fs::File::open("data.txt").unwrap();
// ...
} // هنا بالضبط يُغلق الملف. ليس "حين يمر الـ GC" — هنا
هذا نمط RAII الذي لمسته في C++ إن مررت به، وكنت تصنعه يدويًا في C (goto cleanup). الفرق عن الـ GC جوهري: الموارد غير الذاكرية (ملفات، sockets، أقفال) تُحرَّر في لحظة معلومة، لأن "متى" جزء من دلالة البرنامج لا تفصيلة تنظيف. الترتيب حتمي أيضًا: عكس ترتيب التعريف داخل النطاق.
الرقصة المؤلمة (عمدًا)
الدوال تخضع للبديهية: التمرير بالقيمة = نقل ملكية.
fn shout(s: String) -> String {
let s = s.to_uppercase();
s
}
let msg = String::from("hi");
let msg = shout(msg); // لولا الإرجاع والربط الجديد، لمات msg داخل shout
كل دالة تريد أن "تنظر" في قيمة عليها أن تبتلعها ثم تتقيّأها. سمِّ هذا رقصة الملكية. ستتعبك في ألغاز هذا الإقليم — وهذا التعب مقصود ومصمَّم: إياك أن تخفف ألمه بـ clone() في كل سطر (هذا هرب من الدرس). عش الألم كاملًا؛ هو الذي سيجعلك تشتق بنفسك حلّ الإقليم القادم.
الألغاز
لغز ٠١-أ: المتنبئ الثاني. لكل سطر مرقّم: هل يمر أم يفشل؟ إن فشل فبأي خطأ ولماذا، وإن مرّ فمن يملك ماذا بعده؟ ورقة وقلم قبل التشغيل:
fn take(v: Vec<i32>) -> usize { v.len() }
fn main() {
let v = vec![1, 2, 3];
let n = take(v); // ١
let m = take(v); // ٢
let x = 7;
let y = x;
let z = x + y; // ٣
let s = String::from("a");
let t = s.clone();
let u = s; // ٤
println!("{n} {z} {t} {u}"); // ٥
}
لغز ٠١-ب: آلة RPN الراقصة. ابنِ آلة حاسبة عكسية (Reverse Polish Notation) تقرأ تعبيرًا مثل "3 4 + 2 *" وتطبع 14. البنية إلزامية:
- الحالة كلها في
Vec<f64>(المكدس). - كل عملية دالة مستقلة:
push_num,apply_op… كل دالة تأخذ المكدس ملكيةً كاملةVec<f64>وتعيده ملكيةً كاملة. - القيود: ممنوع
&نهائيًا (لم نتعلمه)، ممنوعclone()نهائيًا، وممنوع دمج كل المنطق فيmain.
ستشعر أن اللغة تعذّبك. دوّن — حرفيًا، في تعليق أعلى الملف — قائمة بكل لحظة قلت فيها "ليتني أستطيع فقط أن أُري الدالة القيمة دون تمليكها". هذه القائمة هي مواصفات الإقليم القادم، وقد كتبتَها أنت.
لغز ٠١-ج: المورد المحسوس. اصنع نوعًا يجعل الـ Drop مرئيًا:
struct Noisy(&'static str);
impl Drop for Noisy {
fn drop(&mut self) { println!("dropping {}", self.0); }
}
(خذ الـ syntax كما هو — impl تُشرح في الإقليم ٣، والـ trait في ٤.) اكتب main تحوي: ثلاثة Noisy في نطاقات متداخلة، واحد يُنقل لدالة ولا يعود، واحد يُنقل ثم يُعاد، وواحد داخل if لا يتنفذ. قبل التشغيل اكتب ترتيب أسطر dropping ... المتوقع كاملًا. ثم شغّل. أي انحراف عن توقعك = ثغرة في نموذجك الذهني — طاردها حتى تغلقها، فحتمية الـ Drop هي ما ستبني عليه اللغة أنصاف أقاليمها القادمة.
الخلاصة
- العقدة الجديدة: الملكية الأحادية + النقل +
Copyالصريحة + الـ Drop الحتمي = إدارة ذاكرة C بلا أخطاء C، والفاتورة تُدفع وقت الترجمة. - الوصلة للخلف: هذا هو الانضباط الذي كنت تحمله في رأسك فوق
malloc— حرفيًا هو، وقد صار قابلًا للإثبات. وقرار "الثبات افتراضًا" من الإقليم ٠٠ أول لبنة في قابلية الإثبات هذه. - الوصلة للأمام: قائمة "ليتني أستطيع أن أُري دون تمليك" التي كتبتَها في ٠١-ب.
الحل اسمه الاستعارة (borrowing) — لكن الاستعارة بلا قيود تعيد أخطاء C كلها من الشباك. القيد الذي يمنع ذلك جملةٌ واحدة من أربع كلمات، وهي أهم أربع كلمات في اللغة كلها. الإقليم القادم مكرّس لاشتقاقها.