Non-lexical lifetimes في Rust: دليل مبسط لتحسين الكود

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

ما هي non-lexical lifetimes؟ تعرف على أهم تحسين في Rust

\n\n

كل من كتب بلغة Rust لبضعة أسابيع فقط، واجه موقفًا: الكود منطقي تمامًا وآمن، لكن المترجم (أو بالأحرى borrow checker الصارم) يرفض ترجمته. تنظر إلى الخطأ وتفكر: "لكني لم أعد أستخدم هذا المرجع! لماذا يشتكي؟".

\n\n

قبل إصدار Rust 1.31 (إصدار 2018)، كانت الإجابة بسيطة: "لأن عمر المتغير (lifetime) يُحدد بواسطة نطاق الرؤية — معجميًا". هذا هو lexical lifetimes. بدءًا من Rust 2018، أصبح المترجم أكثر ذكاءً. حصل على non-lexical lifetimes (NLL) — الأعمار غير المعجمية.

\n\n

في هذا المقال، سنفهم ما هو هذا المفهوم، ولماذا كان ظهوره ثورة لمطوري Rust، وكيف يجعل الكود أنظف وأكثر بديهية. لن نتعمق في نظريات الأنواع المعقدة، بل سنركز على الأمثلة العملية.

\n\n

مشكلة lexical lifetimes: لماذا كان borrow checker صارمًا جدًا

\n\n

دعنا نتذكر كيف يعمل الاقتراض (borrowing) في Rust. لدينا قواعد:

\n
    \n
  • في أي لحظة، يمكن أن يكون هناك إما مرجع قابل للتغيير واحد (&mut T)، أو أي عدد من المراجع غير القابلة للتغيير (&T).
  • \n
  • يجب أن تكون المراجع صالحة دائمًا (لا يمكن الإشارة إلى بيانات محذوفة).
  • \n
\n\n

قبل NLL، كان المترجم يحدد عمر المرجع بناءً على نطاقه المعجمي. أي من { إلى }، حيث تم تعريف المتغير. إذا تم إنشاء المرجع داخل كتلة، فإنه "يعيش" حتى نهاية تلك الكتلة، حتى لو لم تعد تستخدمه.

\n\n

انظر إلى هذا المثال الكلاسيكي الذي لم يكن يُترجم في الإصدارات القديمة من Rust:

\n\n
fn main() {\n    let mut data = vec![1, 2, 3];\n\n    let x = &data[0];  // مرجع غير قابل للتغيير\n\n    data.push(4);       // محاولة اقتراض قابل للتغيير\n\n    println!("{}", x);  // استخدام المرجع غير القابل للتغيير\n}
\n\n

من وجهة نظر بشرية: أنشأنا المرجع x، ثم غيرنا المتجه، ثم استخدمنا x. لكن المتجه data عند استدعاء push قد يعيد توزيع الذاكرة، مما يجعل المرجع x غير صالح. المترجم محق — هذا خطير.

\n\n

لكن ماذا لو استخدمنا المرجع قبل التغيير؟ هذا هو الألم الحقيقي:

\n\n
fn main() {\n    let mut name = String::from("أحمد");\n\n    let first_char = &name[0..1]; // مرجع غير قابل للتغيير\n    println!("الحرف الأول: {}", first_char); // نستخدمه\n\n    name.push_str(" محمد"); // نغير السلسلة النصية\n    println!("الاسم الكامل: {}", name);\n}
\n\n

هذا الكود آمن. استخدمنا المرجع first_char، طبعناه، ثم غيرنا السلسلة النصية. لكن قبل Rust 2018، كان المترجم يظهر خطأ! لماذا؟ لأن العمر المعجمي للمرجع first_char استمر حتى نهاية الدالة main، أي حتى القوس المغلق. والاقتراض القابل للتغيير (push_str) داخل هذه الفترة ممنوع.

\n\n

كان على المطورين استخدام حلول مؤقتة: إضافة كتل إضافية {} لتحديد عمر المرجع يدويًا:

\n\n
fn main() {\n    let mut name = String::from("أحمد");\n\n    {\n        let first_char = &name[0..1];\n        println!("الحرف الأول: {}", first_char);\n    } // المرجع first_char يموت هنا\n\n    name.push_str(" محمد");\n    println!("الاسم الكامل: {}", name);\n}
\n\n

كان هذا يعمل، لكنه كان غير مريح، ويُلوّث الكود بأقواس إضافية ويجعل القراءة صعبة. خاصة في الدوال المعقدة التي تحتوي على عدة اقتراضات.

\n\n

Non-lexical lifetimes (NLL): كيف يعمل

\n\n

فكرة NLL عبقرية في بساطتها: عمر المرجع لا يُحدد بواسطة نطاق الرؤية، بل بالمكان الذي يُستخدم فيه فعليًا.

المدونات

توصيات الكتب