دستور الملكية
Rust
المادة الثانية
في أي لحظة، لأي قيمة: إمّا قرّاءٌ كثرٌ ولا كاتب، أو كاتبٌ واحدٌ ولا أحد غيره.

في نهاية ٠١-ب كتبتَ قائمة أمنيات: "أن أُري الدالةَ القيمةَ دون تمليكها". هذا الإقليم يمنحك الأمنية — ثم يريك لماذا كادت الأمنية نفسها أن تعيد كل كوارث C من الشباك، وما القيد الوحيد الذي أنقذها.


الدرس ١: اشتقاق القانون

اللغز التأسيسي

عُد لأخطاء C من الإقليم ٠١ وأضف لها هذا — أخبث فرد في العائلة، وقد قابلته في C إن استعملت realloc يومًا:

int *arr = malloc(4 * sizeof(int));
int *third = &arr[2];          // مؤشر لعنصر داخل المصفوفة
arr = realloc(arr, 1000000);   // قد ينقل الـ buffer كاملًا لعنوان جديد
*third = 7;                    // ...إلى أين يكتب هذا السطر الآن؟

والصيغة الأعم منه، في أي لغة فيها مراجع — حتى في JS:

for (const x of list) {
    if (bad(x)) list.push(fix(x));   // تعديل بنية أثناء المرور عليها
}

مهمتك التصميمية: افترض أنك أضفت للغة الإقليم ٠١ نوعًا جديدًا: "مرجع مستعار" — مؤشر لا يملك، يسمح بالنظر دون تمليك. اكتب أنت الشرط الذي يجعل المقطعين أعلاه مرفوضين وقت الترجمة. المطلوب صياغة قاعدة واحدة عامة — ليس "امنع realloc" — قاعدة تحكم متى يجوز وجود مرجعين لنفس الشيء. اشتغل عليها قبل المتابعة؛ لاحظ ما البنية المشتركة: في كلا المقطعين هناك قارئ يعتمد على شكل البيانات، وكاتب يغيّر الشكل تحته.

القانون

البديهية الثانية: في أي لحظة، لأي قيمة: إمّا قرّاء كثر ولا كاتب، أو كاتب واحد ولا أحد غيره. مشاركة أو تعديل — لا الاثنين معًا. (Aliasing XOR Mutation)

كل bug ذاكرة تقريبًا — وأنت الآن مؤهل لمراجعة أرشيفك من segfaults هولبرتون والتحقق — هو لحظة اجتمع فيها alias (أكثر من طريق للبيانات) مع mutation (أحد الطرق غيّر البيانات). المؤشر المتدلي بعد realloc: alias قديم + كاتب حرّك البيانات. iterator invalidation: قارئ ماشٍ على البنية + كاتب يعدّلها. القانون لا يمنع الأعراض واحدًا واحدًا — يمنع الاجتماع الذي تتولد منه كلها.

وهنا يتضح أن قرار الإقليم ٠٠ (الثبات افتراضًا) لم يكن ذوقًا: لغة يكون التعديل فيها معلَنًا في كل موضع هي وحدها التي تستطيع أصلًا أن تحاسب على هذا القانون.

الآلية: الترخيصان

fn measure(s: &String) -> usize { s.len() }      // استعارة مشتركة: انظر ولا تلمس
fn shout(s: &mut String) { s.push_str("!!!"); }  // استعارة حصرية: عدّل، وأنت وحدك

let mut msg = String::from("hi");
let n = measure(&msg);      // المالك أعار قراءةً — يستعيدها بعد النداء
shout(&mut msg);            // المالك أعار حصريًا

جرّب الكسر بنفسك (افعلها فعلًا — المطلوب أن تقرأ الرسالتين):

let mut v = vec![1, 2, 3];
let r = &v[0];
v.push(4);            // error[E0502]: cannot borrow `v` as mutable
println!("{r}");      //   because it is also borrowed as immutable

توقف عند هذه تحديدًا. هذا هو مقطع الـ JS أعلاه، وهذا هو realloc — سطر push قد يعيد تخصيص الـ buffer وينقله، فيصبح r متدليًا. المترجم لم "يتضايق من أسلوبك" — لقد منع use-after-free فعليًا كان سيحدث لو كبرت السعة. من الآن فصاعدًا، كل مرة يرفض فيها borrow checker كودك، مهمتك الأولى ليست إرضاءه — بل إيجاد سيناريو الكارثة الذي رآه ولم تره. هو موجود في الغالبية الساحقة من الحالات.

حدود دقّة القانون: NLL

الترخيص لا يمتد لنهاية النطاق {} بل لآخر استعمال فعلي (اسمها non-lexical lifetimes — المترجم يتتبع التدفق لا الأقواس):

let mut v = vec![1, 2, 3];
let r = &v[0];
println!("{r}");   // آخر استعمال لـ r — الترخيص ينتهي هنا
v.push(4);         // مسموح

قدّم هذه المعلومة في نموذجك الذهني مبكرًا: نصف "صراعات المبتدئين مع الـ borrow checker" التي تقرأ عنها في الإنترنت تعود لنسخ ما قبل 2018 حين كان الترخيص يمتد للقوس.


الدرس ٢: الشرائح — استعارة جزء

اللغز

اكتب — نظريًا، على ورق — توقيع دالة تعيد "أول كلمة" من نص. الخيارات المتاحة لك حتى الآن: تعيد String جديدة (نسخ — لماذا ندفع ثمن heap لشيء موجود أصلًا؟) أو تعيد usize كفهرس نهاية الكلمة (وهذا عقد ضمني هش: فهرس صالح فقط ما دام النص لم يتغير — من يحرس هذا العقد؟). كلاهما رديء. ما الشكل الثالث؟

الآلية

الشريحة (slice): مرجع إلى جزء متصل — مؤشر + طول (fat pointer، ضعف حجم مؤشر C العادي، والسبب بديهي لك):

fn first_word(s: &str) -> &str {
    match s.find(' ') {
        Some(i) => &s[..i],
        None => s,
    }
}

التوقيع كعقد كامل

لاحظ ما صار يقوله توقيع الدالة في هذه اللغة، بلا قراءة جسمها:

التوقيعالعقد
fn f(s: &str)أنظر فقط.
fn f(s: &mut String)سأعدّل في المكان.
fn f(s: String)سأبتلعها — لن تراها بعدي (أو أنني أحتاج تخزينها).
fn f(s: &str) -> &strما أعيده نافذة على ما أعطيتني — لم أخصص شيئًا.

في C كانت هذه المعلومات في التوثيق أو في رأس كاتب الكود. هنا هي في النوع، والكذب فيها لا يُترجَم. هذا ما يعنيه الناس فعليًا حين يقولون "التوقيع في Rust يغني عن التوثيق".

الجدار الغامض

جرّب هذا واقرأ الرسالة:

fn make() -> &str {
    let s = String::from("hi");
    &s
}

اشتقاق الرفض عندك كامل: s تموت بنهاية الدالة (البديهية الأولى)، والمرجع سينجو منها — dangling reference بالكتالوج. لكن اقرأ الرسالة نفسها: المترجم لا يقول "dangling" — يقول شيئًا عن missing lifetime specifier ويقترح &'static str ويتمتم عن 'a. المترجم يفكر بمصطلح لم نشرحه: لكل مرجع شيء اسمه lifetime، وهو يحسبه عنك بصمت منذ الإقليم ٠١. علّق هذا الخيط — إقليم كامل قادم يسحبه.


الألغاز

لغز ٠٢-أ: إعادة كتابة الرقصة. أعد بناء آلة RPN من ٠١-ب، والآن & و&mut متاحتان. أعد تصميم توقيع كل دالة بالسؤال الصحيح: هل تنظر، تعدّل، أم تملك؟ القيد: clone() ما زالت ممنوعة، ودالة واحدة على الأكثر يحق لها تملّك شيء. ثم ضع النسختين جنبًا إلى جنب واكتب في تعليق: أي الأمنيات من قائمتك في ٠١-ب حققتها &، وأيها حققتها &mut؟ (يجب أن تُغلق القائمة كلها.)

لغز ٠٢-ب: محكمة الكوارث. كل مقطع أدناه يرفضه المترجم. مهمتك لكل واحد ثلاثة أسطر مكتوبة: (١) أي بديهية انتهكت وكيف، (٢) سيناريو الكارثة الملموسة في C لو مُرِّر الكود — سمِّ الـ bug باسمه (use-after-free؟ invalidation؟ …)، (٣) أصغر إصلاح يحافظ على نية الكود. ممنوع تشغيل المترجم قبل كتابة الثلاثة:

// (١)
let mut s = String::from("hi");
let a = &s;
let b = &mut s;
println!("{a}");

// (٢)
let mut v = vec![1, 2, 3];
for x in &v {
    if *x == 2 { v.push(99); }
}

// (٣)
let r;
{
    let x = 5;
    r = &x;
}
println!("{r}");

// (٤)
let mut s = String::from("hello world");
let w = first_word(&s);   // من الدرس ٢
s.clear();
println!("{w}");

// (٥)
fn swap_first(a: &mut Vec<i32>) {
    let x = &a[0];
    a[0] = a[1];        // هل هذا فعلًا خطر؟ فكّر قبل أن تجيب —
    println!("{x}");    // ثم فكّر: كيف يعرف المترجم حدود ما يعرفه؟
}

الخامس هو الجوهرة: قد تقتنع أنه "آمن فعليًا". صياغة لماذا يرفضه المترجم رغم ذلك — أي ما الذي يستطيع المترجم إثباته وما لا يستطيع، ولماذا اختار جهة الرفض عند الشك — هي نصف نضجك في هذه اللغة.

لغز ٠٢-ج: المُقسِّم اليدوي. ابنِ fn split_ws(s: &str) -> Vec<&str> تقسم نصًا على المسافات. القيود: ممنوع split() وكل قريباتها الجاهزة؛ اشتغل بـ char_indices() و&s[a..b] فقط؛ وممنوع تخصيص أي String — الناتج نوافذ محضة على المُدخل. (ستصطدم بمطبّ فهرسة لطيف إن جربت نصًّا عربيًّا — لا تحله الآن، فقط دوّنه: إنه بذرة الإقليم ٦.) تحقق أن Vec<&str> الراجع يجمّد الأصل: أضف s.clear()... مهلًا — s أصلًا &str غير قابلة للتعديل. جرّب بدلها التجميد على String يملكها المُنادي.


الخلاصة

اكتمل الدستور. بديهيتان فقط:

  1. لكل قيمة مالك واحد (الإقليم ٠١) — تحل أسئلة من يحرّر ومتى.
  2. مشاركة XOR تعديل (هذا الإقليم) — تحل أسئلة من يرى ماذا أثناء التغيّر.

كل ما تبقى في اللغة — كل ما ستقابله من هنا حتى نهاية المنهج — إجابات على سؤال واحد متكرر: "كيف نعيش حياة برمجية كاملة تحت هذين القانونين؟" حين تبدو لك ميزة قادمة غريبة، أرجعها إليهما وستجدها مبرهنة.

بذرة الغموض

لاحظ ما تستطيع دوالنا إعادته حتى الآن: قيم مملوكة، أو مراجع لأشياء أعطانا إياها المُنادي. لكن ماذا عن دالة تريد أن تقول "أعد لي إما قيمة أو لا شيء"؟ أو "إما نتيجة أو خطأ"؟ في C كنت تهرّب هذا في NULL و-1 وأعراف بائسة في التوثيق. الإقليم القادم يعطيك ما كان ينقص C منذ ١٩٧٢ — وأنت تعرف نصفه من TS دون أن تدري.