دستور الملكية
Rust

منذ الإقليم ٠٢ والمترجم يتمتم لك أحيانًا بحرف 'a حين ترفض دالة الترجمة. تعاملتَ معه كطلسم مقترَح تنسخه وتلصقه. هذا الإقليم يعطيك المعنى الكامل — وستكتشف أنه أبسط بكثير مما توحي به سمعته، وأعمق بكثير مما توحي به بساطته.


الدرس ١: لماذا لا يستطيع المترجم أن يستنتج هذا وحده؟

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

جرّب هذا فعليًا الآن:

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}

cargo check يرفض بـ missing lifetime specifier، ويشرح: نوع الإرجاع مرجع مُستعار، لكن التوقيع لا يقول من أي معامل استُعير. هذا يستحق توقفًا حقيقيًا، لأنه يبدو تناقضًا: المترجم استنتج أنواعًا أعقد بكثير من هذا طوال الأقاليم الماضية (استنتج T كاملة في generics!) — فلماذا يعجز هنا عن أمر يبدو تافهًا؟ جسم الدالة واضح تمامًا: يعيد x أو y، وكلاهما معلوم.

مهمتك: لا تقرأ الحل بعد. أجب أولًا: المترجم لا يقرأ فقط جسم هذه الدالة حين يتحقق من الأمان — إنه يتحقق من كل موقع استدعاء لها في كل الكود، وأحيانًا في مكتبات أخرى مُصرَّفة سلفًا لا يملك مصدرها إطلاقًا (فقط توقيعها المُصدَّر). فكّر: لو غيّرتَ جسم longest غدًا (مثلًا لتُعيد y دائمًا بدل الأطول)، وكان المترجم يستنتج قاعدة الأعمار من الجسم، ماذا يحدث لكل الكود الذي استدعاها سابقًا معتمدًا على استنتاج قديم؟ اربط جوابك بما تعلمتَه في الإقليم ٠٠ عن التصريح الإلزامي لأنواع التوقيع (لا استنتاج هناك رغم أن الجسم يستنتج بحرّية) — هل ترى الآن أن هذا سؤال طرحتَه اللغة على نفسها من قبل، ووجدت له نفس الجواب؟

الحل: الأعمار كمعاملات — نفس آلية Generics بالحرف

في الإقليم الماضي كتبتَ fn largest<T: PartialOrd>(list: &[T]) -> &T. <T> معامل عام يُستبدل بنوع عند كل استدعاء. الحل هنا هو نفس الآلية تمامًا، لكن المعامل يُستبدل بمدة زمنية بدل نوع:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

اقرأها كعقد صريح: "أعطني مرجعين حيّين طوال مدة زمنية واحدة اسمّيها 'a (أيًّا كانت — يقررها كل موقع استدعاء بنفسه)، وسأعيد لك مرجعًا لن يتجاوز عمره تلك المدة." هذا كل ما يقوله. عند كل استدعاء، 'a تُستبدل تلقائيًا بأصغر نطاق مشترك بين عمري x وy الفعليين هناك — تمامًا كما T تُستبدل بـ i32 أو &str بحسب موقع الاستدعاء الفعلي.

نقطة يجب أن ترسخ ولا تهتز أبدًا: 'a لا تُطيل عمر أي شيء ولا تُقصّره. لا تُنشئ زمنًا، ولا تتحكم فيه. هي اسمٌ تُعطيه لمنطقة زمنية موجودة أصلًا في الكود (النطاق الذي تعيش فيه القيمة المُستعارة)، حتى يستطيع المترجم مطابقة هذا الاسم عبر حدود الدوال — بالضبط كما x: i32 لا "تصنع" i32 بل تسمّي نوعًا موجودًا. لو أسميتَ نهر النيل "أ" فذلك لا يغيّر طوله؛ يمنحك فقط كلمة تشير بها إليه من غرفة أخرى.

الآن أعد قراءة سؤال "لماذا لا يستنتج المترجم هذا؟": لأن التوقيع، هنا تمامًا كما في الإقليم ٠٠، عقد عام يجب أن يُقرأ ويُفهَم بلا الحاجة لتشغيل أي خوارزمية استنتاج على الجسم — وبلا الحاجة حتى لامتلاك الجسم أصلًا (مكتبة مُصرَّفة تُصدِّر التوقيع فقط). لو غيّرتَ الجسم غدًا فالعقد 'a يبقى صحيحًا ما دام الجسم يحترمه — وهذا بالضبط ما يفحصه المترجم كل مرة.


الدرس ٢: بُنى تحمل مراجع

اللغز

جرّب:

struct Highlight {
    text: &str,   // ماذا يقول المترجم؟
}

نفس الرسالة: missing lifetime specifier. لماذا يحتاج struct نفس التصريح الذي احتاجته دالة؟ فكّر بمعيار الإقليم ٠١: كل قيمة Rust — بما فيها struct — لها مالك، وحين يموت المالك تُسقَط القيمة (Drop). لو كانت Highlight تحوي مرجعًا، فعمر Highlight نفسه يجب ألا يتجاوز عمر ما تشير إليه — وإلا حصلتَ على نفس مرجع الإقليم ٠٢ المتدلّي، لكن هذه المرة مخبوءًا داخل حقل بنية بدل متغيّر عارٍ.

الآلية

struct Highlight<'a> {
    text: &'a str,
}

impl<'a> Highlight<'a> {
    fn shout(&self) -> String {
        self.text.to_uppercase()
    }
}

اقرأها: "أي Highlight مُعامَلة بعمر 'a لا يجوز أن تعيش أطول من النص الذي تُشير إليه." المترجم يفرض هذا في كل مكان تُستعمل فيه:

let h;
{
    let s = String::from("hi");
    h = Highlight { text: &s };
}   // s تموت هنا
println!("{}", h.text);   // مرفوض: h تحاول أن تعيش أطول من s

هذا حرفيًا مثال الإقليم ٠٢ (r = &x بعد موت x)، لكن معبَّرًا عنه عبر حقل بنية بدل متغيّر مباشر. الآلية واحدة؛ الغلاف تغيّر فقط.


الدرس ٣: قواعد الحذف (Elision) — لماذا لم تكتب 'a من قبل؟

راجع كل الدوال التي كتبتَها منذ الإقليم ٠٢ وتُرجع مراجع (fn first_word(s: &str) -> &str مثلًا) — عملت بلا كتابة 'a ولو مرة. لماذا لم يطلبها المترجم هناك، وطلبها في longest؟ الجواب: ثلاث قواعد ميكانيكية يطبّقها المترجم قبل أن يطلب منك التصريح الصريح — إن استطاعت حسم الإجابة بلا غموض، لا يزعجك؛ وإن بقي أكثر من احتمال، يتوقف ويطلب منك الحسم:

  1. كل مرجع مُدخَل غير مُسمّى يأخذ عمره الخاص. fn f(x: &str, y: &str) تصير داخليًا fn f<'a, 'b>(x: &'a str, y: &'b str).
  2. إن كان هناك مُدخَل مرجعي واحد بالضبط، يُعطى عمره لكل المخرجات المرجعية المحذوفة. هذا ما أنقذ first_word: معامل مرجعي واحد ⇒ لا غموض ⇒ يُطبَّق آليًا.
  3. إن كان أحد المُدخلات &self أو &mut self، يُعطى عمره لكل المخرجات المحذوفة (لأن الحالة الشائعة: طريقة تُرجع جزءًا من نفسها).

طبّق القواعد يدويًا على longest(x: &str, y: &str) -> &str: القاعدة ١ تُعطي x وy عمرين مختلفين ('a و'b)؛ القاعدة ٢ لا تنطبق (مُدخلان، لا واحد)؛ القاعدة ٣ لا تنطبق (لا self). لا قاعدة حسمت أيّ عمر للمخرج — هذا بالضبط سبب الرفض، ولماذا يجب أن تحسم أنت بنفسك بكتابة 'a صريحة تُوحِّد الثلاثة. الحذف ليس سحرًا ولا استثناءً — إنه خوارزمية معلنة تعمل صامتة حين تكفي، وتتنحى حين لا تكفي.


الدرس ٤: 'static — العمر الأطول والأكثر التباسًا

let s: &'static str = "hi";   // literal، يعيش في .rodata طوال البرنامج

تذكّر الإقليم ٠٢: النصوص الحرفية &str تعيش في قسم .rodata (كما في C) — لا heap، لا مالك يُسقِطها، تُولَد مع البرنامج وتموت معه. هذا العمر الأقصى الممكن، واسمه 'static.

فخّ اصطلاحي يستحق التحذير المبكر: T: 'static كقيد على generic لا تعني بالضرورة "يعيش للأبد" — تعني "لا يحوي T أي مرجع بعمر أقصر من 'static" (أي: إما لا مراجع فيه إطلاقًا — الحالة الشائعة لأي نوع مملوك بالكامل كـ String، أو i32 — أو كل مراجعه بالفعل 'static). سترى هذا القيد كثيرًا حين تصل الخيوط (الإقليم ٠٨): إرسال قيمة لخيط آخر يتطلب ألا تحمل مرجعًا قد يموت أصله بينما الخيط الآخر ما زال يستعمله.


حكمة عملية (قبل الألغاز)

حين تجد نفسك تصارع المترجم حول الأعمار لدقائق طويلة في كود تطبيقي عادي (لا في تمرين مصمَّم كهذا)، هذه غالبًا إشارة تصميم، لا معركة يجب كسبها بالقوة: هل هذا الموضع يحتاج فعلًا أن يستعير، أم أن تمليك قيمة (وربما clone() واحدة مدروسة) أبسط وأوضح نيّة؟ رقصة الملكية من الإقليم ٠١ لم تُلغَ — الاستعارة اختصار لها حين يصح استعمالها، لا بديل دائم عنها. أهل اللغة يوازنون بين الاثنين بذوق مكتسب، لا بتعصب لأحدهما.


الألغاز

لغز ٠٥-أ: المعامل غير المشمول. اكتب:

fn announce_and_return_first<'a>(x: &'a str, y: &'a str, ann: ???) -> &'a str {
    println!("Attention: {ann}");
    if x.len() > y.len() { x } else { y }
}

ann معامل ثالث — نص أيضًا، يُطبَع فقط ولا يُعاد إطلاقًا. هل يحتاج نفس عمر 'a؟ صمّم التوقيع الصحيح بأقل قيود ممكنة (القيد الزائد كذبٌ في العقد بقدر القيد الناقص)، ثم تحقق: هل يمكنك استدعاء الدالة بحيث يموت ann قبل أن يموت x أو y، وما زالت تُترجَم؟ إن نجحت فتوقيعك صحيح.

لغز ٠٥-ب: المُفكِّك المُستعار. ابنِ struct Parser<'a> { input: &'a str, pos: usize } بطريقة fn next_token(&mut self) -> Option<&'a str> تُعيد الكلمة التالية (مفصولة بمسافة) من input، أو None عند النهاية، بلا تخصيص أي String — كل ما تُعيده نوافذ &'a str على input الأصلي (ادمج ما بنيتَه في ٠٢-ج). القيد الحاسم: عمر ما تُعيده مربوط بـ 'a (عمر النص الأصلي) لا بعمر &mut self — لماذا هذا الفرق جوهري؟ (جرّب الخطأ أولًا: اكتب fn next_token(&'a mut self) -> Option<&'a str> واقرأ لماذا يرفضها المترجم حين تحاول استدعاءها مرتين متتاليتين — هذا فخّ اصطلاحي شهير يستحق أن تصطدم به بنفسك مرة واحدة.) اختبرها بجملة إنجليزية وبجملة عربية معًا، ودوّن أي سلوك غريب تراه مع العربية — لا تُصلحه، فقط راقبه (بذرة).

لغز ٠٥-ج: القاضي بلا تشغيل. لكل توقيع أدناه: هل يُترجَم بلا تعديل، أم يحتاج 'a صريحة، أم مرفوض بنيويًا مهما كتبتَ من أعمار؟ اكتب حكمك وسببه (بالرجوع لقواعد الحذف الثلاث بالاسم) قبل تجربته:

fn f1(x: &str) -> &str { x }
fn f2(x: &str, y: &str) -> &str { x }
fn f3() -> &str { "hi" }
fn f4(x: &str, y: &str) -> (&str, &str) { (x, y) }
struct S<'a> { r: &'a str }
impl<'a> S<'a> {
    fn f5(&self, other: &str) -> &str { self.r }
}
fn f6<'a>(x: &'a str) -> &'static str { "always this" }

الخلاصة

بذرة الغموض

المُفكِّك الذي بنيتَه في ٠٥-ب يشبه شيئًا أعمق مبنيًّا في قلب اللغة نفسها. والمطبّ العربي الذي طلبنا منك أن تراقبه بلا إصلاح؟ كلاهما يُحلّان في الإقليم القادم — الذي يكشف أيضًا سرًّا عن self أُخفي عنك عمدًا: هناك ثلاثة أزياء لشيء واحد كنتَ تستعمله طوال الوقت دون أن تعرف اسمه.