اذهب إلى المحتوى

التحقق من المراجع References عبر دورات الحياة Lifetimes في لغة رست


Naser Dakhel

تعدّ دورات الحياة نوعًا آخر من الأنواع المعمّمة generic وقد استعملناها سابقًا دون معرفتنا، إذ تتأكد دورات الحياة أن المراجع references صالحة طوال حاجتنا لها بدلًا من التأكد أن لنوع ما سلوك معيّن.

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

مفهوم الإشارة إلى دورات الحياة غير موجود في معظم لغات البرمجة، لذا قد تشعر بأن محتوى هذا المقال غير مألوف بالنسبة لك، على الرغم من أننا لن نتكلم عن دورات الحياة بالتفصيل هنا بل سنتكلم عن الطرق الشائعة التي قد تصادف بها طريقة كتابة دورة حياة بحيث تألف هذا المفهوم.

منع المراجع المعلقة dangling references بدورات الحياة

هدف دورات الحياة الأساسي هو منع المراجع المعلّقة dangling references إذ تسبب للبرنامج إشارته إلى مرجع بيانات لا يتطابق مع البيانات التي نريدها، ألقِ نظرةً على البرنامج في الشيفرة 16، إذ يحتوي على نطاق خارجي وداخلي.

fn main() {
    let r;

    {
        let x = 5;
        r = &x;
    }

    println!("r: {}", r);
}

الشيفرة 16: محاولة لاستخدام مرجع خرجت قيمته عن النطاق

ملاحظة: تصرّح الأمثلة في الشيفرة 16 والشيفرة 17 والشيفرة 23 عن متغيرات دون إعطائها قيم أولية، لذا لا يوجد اسم المتغير في النطاق الخارجي. قد يبدو ذلك للوهلة الأولى تعارضًا مع مبدأ عدم وجود قيم فارغة null values في رست، إلا أننا سنحصل على خطأ عند التصريف إذا حاولنا استخدام متغير قبل منحه قيمة، وهو ما يؤكد عدم سماح رست بوجود قيم فارغة.

يصرح النطاق الخارجي عن متغير يدعى r دون إسناد قيمة أولية له، بينما يصرح النطاق الداخلي على متغير يدعى x بقيمة أولية 5. نحاول في النطاق الداخلي ضبط قيمة r لتصبح مرجعًا إلى القيمة x وعندما ينتهي النطاق الداخلي نحاول طباعة القيمة الموجودة في r. لن تُصرَّف هذه الشيفرة البرمجية وذلك لأن r يمثل مرجعًا لمتغير خرج عن النطاق قبل أن نستخدمه. إليك رسالة الخطأ:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `x` does not live long enough
 --> src/main.rs:6:13
  |
6 |         r = &x;
  |             ^^ borrowed value does not live long enough
7 |     }
  |     - `x` dropped here while still borrowed
8 | 
9 |     println!("r: {}", r);
  |                       - borrow later used here

For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` due to previous error

لا "يعيش" المتغير x طويلًا، والسبب في ذلك هو أن x سيخرج عن النطاق عند انتهاء النطاق الداخلي في السطر 7، بينما سيبقى r صالحًا في النطاق الخارجي لأن نطاقه أكبر، وعندها نقول أنه "سيعيش" أطول. إذا سمحت رست لهذه الشيفرة البرمجية بالعمل فهذا يعني أن r سيمثل مرجعًا لمكان محرر في الذاكرة deallocated بعد خروج x من النطاق ولن يعمل أي شيء باستخدام r على النحو المطلوب، إذًا كيف تتحقق رست من صلاحية هذه الشيفرة البرمجية؟ باستخدام مدقق الاستعارة borrow checker.

مدقق الاستعارة

لمصرّف رست مدقق استعارة يقارن بين النطاقات لتحديد أن جميع عمليات الاستعارة صالحة، وتوضح الشيفرة 17 إصدارًا مماثلًا للشيفرة 16 ولكن بتوضيح دورة الحياة لكل من المتغيرات.

fn main() {
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {}", r); //          |
}                         // ---------+

الشيفرة 17: دورة حياة rيشار إليها باستخدام ‎'a بينما يشار إلى دورة حياة x باستخدام ‎'b

أشرنا هنا إلى دورة حياة r باستخدام ‎'a ودورة حياة x باستخدام ‎'b، وكما ترى، فإن الكتلة ‎'b الداخلية أصغر بكثير من كتلة دورة حياة ‎'a الخارجية. تقارن رست عند وقت التصريف ما بين حجم دورتي الحياة هاتين وتجد أن r لها دورة حياة ‎'a إلا أنها تمثل مرجعًا إلى موقع ذاكرة دورة حياته ‎'b، وبالتالي يُرفَض البرنامج لأن ‎'b أقصر من ‎'a، أي أن الغرض الذي نستخدم المرجع إليه يعيش أقصر من المرجع ذاته.

نصلح الشيفرة البرمجية السابقة في الشيفرة 18، إذ لا يوجد لدينا مراجع معلقة بعد الآن، وتُصرَّف الشيفرة البرمجية بنجاح دون أي أخطاء.

fn main() {
    let x = 5;            // ----------+-- 'b
                          //           |
    let r = &x;           // --+-- 'a  |
                          //   |       |
    println!("r: {}", r); //   |       |
                          // --+       |
}                         // ----------+

الشيفرة 18: مرجع صالح لأن للبيانات دورة حياة أطول من المرجع

دورة حياة x التي تدعى ‎'b أكبر من ‎'a، مما يعني أن r يمكن أن يمثل مرجعًا للمتغير x، لأن رست تعلم أن المرجع في r صالح ما دام x صالح.

الآن وبعد أن تعرفت على دورات حياة المراجع وكيف تحلل رست دورات الحياة للتأكد من أن المراجع ستكون دائمًا صالحة، حان وقت التعرف إلى دورات الحياة المعمّمة الخاصة بالمعاملات والقيمة المُعادة في سياق الدوال.

دورات الحياة المعممة في الدوال

دعنا نكتب دالةً تُعيد أطول شريحة نصية string slice من شريحتين نصيتين، إذ ستأخذ هذه الدالة شريحتين نصيتين وتُعيد شريحةً نصيةً واحدة.

اسم الملف: src/main.rs

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}

الشيفرة 19: دالة main تستدعي الدالة longest لإيجاد أطول شريحة نصية من شريحتين نصيتين

يجب أن تطبع الشيفرة 19 بعد تطبيق الدالة longest ما يلي:

The longest string is abcd

لاحظ أننا نريد أن تأخذ الدالة شرائح النصية وهي مراجع وليست سلاسل نصية لأننا لا نريد للدالة longest أن تأخذ ملكية معاملاتها. عُد للمقال الذي تكلمنا فيه عن شرائح السلاسل النصية مثل معاملات لمعرفة المزيد حول سبب استخدامنا للمعاملات في الشيفرة 19 كما هي.

لن تُصرّف الشيفرة البرمجية إذا حاولنا كتابة الدالة longest كما هو موضح في الشيفرة 20.

اسم الملف: src/main.rs

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

الشيفرة 20: تنفيذ الدالة longest الذي يُعيد أطول شريحة نصية من شريحتين إلا أنه لا يُصرَّف بنجاح

نحصل على الخطأ التالي الذي يتحدث عن دورات الحياة:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0106]: missing lifetime specifier
 --> src/main.rs:9:33
  |
9 | fn longest(x: &str, y: &str) -> &str {
  |               ----     ----     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
  |
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `chapter10` due to previous error

تساعدنا رسالة الخطأ في معرفة أن النوع المُعاد يجب أن يكون له معامل بدورة حياة معممة لأن رست لا تعلم إذا كان المرجع المُعاد يمثل مرجعًا إلى x أو y، وفي الحقيقة لا نعلم نحن أيضًا بدورنا لأن كتلة if في متن الدالة يُعيد مرجعًا للمتغير x وكتلة else تُعيد مرجعًا للمتغير y.

لا نعلم القيم الثابتة التي ستُمرر لهذه الدالة عندما نعرفها، لذا لا نعلم إذا ما كانت حالة if محققة أو حالة else، كما أننا لا نعرف دورة الحياة الثابتة للمراجع التي ستُمرر للدالة، لذا لا يمكننا النظر إلى النطاق كما فعلنا في الشيفرة 17 والشيفرة 18 للتأكد إذا ما كان المرجع المُعاد صالحًا دومًا، ولا يمكن لمدقق الاستعارة معرفة ذلك أيضًا لأنه لا يعرف أيّ من دورتي الحياة لكل من x و y ستكون مرتبطة بدورة الحياة الخاصة بالقيمة المُعادة؛ ولتصحيح هذا الخطأ نُضيف معاملًا ذا دورة حياة معممة يعرّف العلاقة ما بين المراجع حتى يستطيع مدقق الاستعارة إجراء تحليله.

طريقة كتابة دورة الحياة

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

طريقة كتابة دورة الحياة غير مألوفة جدًا، إذ يجب أن تبدأ أسماء معاملات دورات الحياة بالفاصلة العليا ' وعادةً ما تكون أسمائها قصيرة ومكتوبة بأحرف قصيرة كما هو الحال مع الأنواع المعممة. يستخدم معظم الناس الاسم ‎'a بمثابة اسم أول دورة حياة، ومن ثم نضع معامل دورة الحياة بعد إشارة & الخاصة بالمرجع باستخدام المسافة للفصل بين طريقة كتابة دورة الحياة ونوع المرجع.

إليك بعض الأمثلة على ذلك: مرجع لقيمة من نوع i32 دون معامل دورة حياة، ومرجع لقيمة من نوع i32 بمعامل دورة حياة يدعى ‎'a ومرجع قابل للتعديل mutable لقيمة من نوع i32 بالاسم 'a ذاته.

&i32        // مرجع
&'a i32     // مرجع مع دورة حياة صريحة
&'a mut i32 // مرجع قابل للتعديل مع دورة حياة صريحة

لا تعني كتابة دورة الحياة بمفردها بالشكل السابق الكثير، إذ أن الهدف من هذه الطريقة هو إخبار رست بعلاقة المراجع فيما بينها في معاملات دورة الحياة المعممة. دعنا ننظر إلى كيفية تحقيق ذلك في سياق الدالة longest.

توصيف دورة الحياة في بصمات الدالة

نحتاج للتصريح عن معاملات دورة الحياة المعممة داخل أقواس مثلثة حتى نستطيع استخدام توصيف دورة الحياة في بصمات الدوال، وذلك بين اسم الدالة وقائمة معاملاتها كما فعلنا سابقًا في معاملات النوع المعمم.

نريد من بصمة الدالة أن توضح القيود التالية: سيكون المرجع المُعاد صالح طالما أن كلا المعاملين صالحان؛ وهذه هي العلاقة بين دورات حياة المعاملات والقيمة المعادة. سنسمّي دورة حياة بالاسم ‎'a، ثم نُضيفها لكل مرجع كما هو موضح في الشيفرة 21.

اسم الملف: src/main.rs

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

الشيفرة 21: تعريف الدالة longest الذي يحدد أن دورة الحياة لجميع المراجع في بصمة الدالة هي ‎'a

يجب أن تعمل الشيفرة البرمجية السابقة بنجاح وأن تمنحنا النتيجة المرجوة عند استخدامها ضمن الدالة main كما فعلنا في الشيفرة 19 السابقة.

تخبر بصمة الدالة رست بأن الدالة تأخذ معاملين لبعض دورات الحياة ‎'a وكلاهما شريحة نصية يعيشان على الأقل بطول دورة حياة ‎'a، كما تخبر بصمة الدالة رست بأن شريحة السلسلة النصية المُعادة من الدالة ستعيش على الأقل بطول دورة الحياة ‎'a، وهذا يعني عمليًا أن دورة حياة المرجع المُعاد من الدالة longest مماثلة لأقصر دورة حياة من دورات حياة القيم التي استخدمنا مراجعها في وسطاء الدالة، وهذه هي العلاقة التي نريد أن تستخدمها رست عند تحليل هذه الشيفرة البرمجية.

تذكر أننا لا نعدّل من دورات حياة القيم الممررّة أو المُعادة عندما نحدد دورة الحياة المعاملات في بصمة الدالة، وإنما نحدد أنه يجب على مدقق الاستعارة أن يرفض أي قيمة لا تتوافق مع القيود المذكورة. لاحظ أن الدالة longest لا تحتاج لمعرفة أيّ من المتغيرين x و y سيعيش لمدة أطول، بل فقط بحاجة لمعرفة أن نطاق ما سيُستبدل بدورة الحياة ‎'a التي ستطابق بصمة الدالة.

نكتب توصيف دورات الحياة عند استخدامها مع الدوال في بصمة الدالة وليس في متنها، إذ يصبح توصيف دورة الحياة جزءًا من عقد contract الدالة كما هو الحال بالنسبة للأنواع ضمن بصمة الدالة. احتواء بصمة الدالة على عقد دورة الحياة يعني أن التحليل الذي يجريه مصرف رست سيصبح أبسط، وإذا وُجدت مشكلة بطريقة توصيف الدالة أو طريقة استدعائها يمكن لأخطاء المصرف أن تُشير إلى ذلك الجزء ضمن الشيفرة البرمجية والقيود التي خرقتها بصورةٍ أدقّ.

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

عند تمرير المراجع الثابتة إلى longest تكون دورة الحياة الثابتة المُستبدلة بدورة الحياة ‎'a جزءًا من نطاق x الذي يتداخل مع نطاق y، بمعنى آخر، تحصل دورة الحياة المعممة ‎'a على دورة حياة ثابتة مساوية إلى أصغر دورة حياة (أصغر دورة بين الدورتين الخاصة بالمتغير y والمتغير x).

دعنا ننظر إلى نتيجة استخدام توصيف دورة الحياة وكيف يقيّد ذلك من دالة longest بتمرير المراجع التي لها دورات حياة ثابتة مختلفة، وتمثّل الشيفرة 22 مثالًا مباشرًا على ذلك.

اسم الملف: src/main.rs

fn main() {
    let string1 = String::from("long string is long");

    {
        let string2 = String::from("xyz");
        let result = longest(string1.as_str(), string2.as_str());
        println!("The longest string is {}", result);
    }
}

الشيفرة 22: استخدام الدالة longest مع مراجع لقيم من نوع String تمتلك دورات حياة ثابتة مختلفة

تكون القيمة string1 صالحةً في المثال السابق حتى الوصول لنهاية النطاق الخارجي، بينما تبقى string2 صالحة حتى نهاية النطاق الداخلي، وأخيرًا تمثل result مرجعًا لقيمة صالحة حتى نهاية النطاق الخارجية. نفّذ الشيفرة البرمجية السابقة وسترى أن مدقق الاستعارة لن يعترض على الشيفرة البرمجية وستُصرَّف وتطبع ما يلي:

The longest string is long string is long

دعنا نجرّب مثالًا يوضح أن دورة حياة المرجع في result يجب أن تكون أصغر من دورة حياة كلا الوسيطين؛ إذ سننقل التصريح عن المتغير result خارج النطاق الداخلي مع المحافظة على عملية إسناد قيمة إلى المتغير result داخل النطاق حيث توجد string2، ثم سننقل println!‎ الذي يستخدم result خارج النطاق الداخلي بعد انتهائه. لن تُصرَّف الشيفرة 23 بنجاح.

اسم الملف: src/main.rs

fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {}", result);
}

الشيفرة 23: محاولة استخدام result بعد خروج string2 من النطاق

نحصل على رسالة الخطأ التالية عندما نحاول تصريف الشيفرة البرمجية:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `string2` does not live long enough
 --> src/main.rs:6:44
  |
6 |         result = longest(string1.as_str(), string2.as_str());
  |                                            ^^^^^^^^^^^^^^^^ borrowed value does not live long enough
7 |     }
  |     - `string2` dropped here while still borrowed
8 |     println!("The longest string is {}", result);
  |                                          ------ borrow later used here

For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` due to previous error

يوضح الخطأ أنه يجب على result أن يكون صالحًا حتى تُنفَّذ التعليمة println!‎، كما يجب على المتغير string2 أن يكون صالحًا حتى نهاية النطاق الخارجي، وتعلم رست ذلك بسبب توصيفنا لدورات حياة معاملات الدالة والقيم المُعادة باستخدام معامل دورة الحياة ذاته ‎'a.

يمكننا النظر إلى هذه الشيفرة البرمجية على أننا بشر ورؤية أن string1 أطول من string2 وبالتالي سيحتوي المتغير result على مرجع للمتغير string1، ولأن string1 لم يخرج من النطاق بعد، فسيبقى مرجع string1 صالحًا حتى تستخدمه تعليمة !println، إلا أن المصرف لا ينظر إلى المرجع بكونه صالحًا في هذه الحالة إذ أننا أخبرنا رست أن دورة حياة المرجع المُعاد بواسطة الدالة longest هو بطول أصغر دورة حياة مرجع مُمرّر لها، وبالتالي لا يسمح مدقق الاستعارة للشيفرة 23 بامتلاك الفرصة للحصول على مرجع غير صالح.

جرّب كتابة المزيد من الأمثلة لتجربة الحالات والقيم ودورات حياة المراجع المختلفة المُمرّر إلى الدالة longest ولاحظ كيفية استخدام المرجع المُعاد، وتنبّأ فيما إذا كانت تجربتك ستُصرَّف ويوافق عليها مدقق الاستعارة أم لا قبل أن تحاول تصريفها، ومن ثم جرّب تصريفها لترى إن كنت مصيبًا أم لا.

التفكير في سياق دورات الحياة

تعتمد الطريقة التي تحدد فيها دورة حياة المعاملات على الغرض من الدالة، فعلى سبيل المثال إذا عدّلت من كتابة الدالة longest لتُعيد دائمًا المعامل الأول بدلًا من المعامل الذي يمثل أطول شريحة نصية فلن نحتاج عندئذ لتحديد دورة حياة المعامل y. تُصرَّف الشيفرة البرمجية التالية بنجاح:

اسم الملف: src/main.rs

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

حددنا معامل دورة حياة مُمثّل بالاسم ‎'a للمعامل x والقيمة المُعادة، إلا أننا لم نحدد دورة حياة للمعامل y لأن ليس لدورة حياة y أي علاقة بدورة حياة x أو القيمة المُعادة.

يجب أن يطابق معامل دورة حياة القيمة المُعادة دورة حياة أحد من المعاملات عند إعادة مرجع من دالة ما، وإذا لم يشير المرجع المُعاد إلى واحد من المعاملات فيجب أن يشير إلى قيمة أُنشئت داخل الدالة ذاتها، إلا أن هذا المرجع سيكون مرجعًا معلَّقًا، لأن القيمة ستخرج من النطاق في نهاية الدالة. ألقِ نظرةً على المحاولة التالية لتطبيق الدالة longest التي لن تُصرَّف بنجاح:

اسم الملف: src/main.rs

fn longest<'a>(x: &str, y: &str) -> &'a str {
    let result = String::from("really long string");
    result.as_str()
}

على الرغم من أننا حددنا معامل دورة الحياة ‎'a للنوع المُعاد إلا أن الشيفرة البرمجية لن تُصرَّف لأن دورة حياة القيمة المُعادة غير مرتبطة بدورة حياة المعاملات إطلاقًا. إليك رسالة الخطأ التي سنحصل عليها:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0515]: cannot return reference to local variable `result`
  --> src/main.rs:11:5
   |
11 |     result.as_str()
   |     ^^^^^^^^^^^^^^^ returns a reference to data owned by the current function

For more information about this error, try `rustc --explain E0515`.
error: could not compile `chapter10` due to previous error

تكمن المشكلة هنا في أن result يخرج من النطاق ويُحرَّر من الذاكرة بنهاية الدالة longest، إلا أننا نحاول أيضًا إعادة مرجع للقيمة result من الدالة في ذات الوقت، ولا يوجد هناك أي وسيلة لتحديد معاملات دورة الحياة بحيث نتخلص من المرجع المُعلَّق ولن تسمح لنا رست بإنشاء مرجع معلّق. الحل الأمثل في هذه الحال هو بجعل القيمة المُعادة نوع بيانات مملوك owned data type بدلًا من استخدام مرجع، بحيث تكون الدالة المُستدعاة حينها مسؤولة عن تحرير القيمة فيما بعد.

يتمثّل توصيف دورة الحياة بربط دورات حياة معاملات مختلفة والقيم المُعادة من الدوال، بحيث تحصل رست على معلومات كافية بعد الربط للسماح بعمليات آمنة على الذاكرة ومنع عمليات قد تتسبب بالحصول على مؤشرات معلّقة أو تخرق أمان الذاكرة.

توصيف دورة الحياة في تعاريف الهيكل

كانت الهياكل التي عرفناها لحد اللحظة تحتوي على أنواع مملوكة، إلا أنه يمكننا تعريف الهياكل بحيث تحتوي على مراجع وفي هذه الحالة علينا توصيف دورة حياة لكل من المراجع في تعريف الهيكل. تحتوي الشيفرة 24 على هيكل يدعى ImportantExcerpt يحتوي على شريحة سلسلة نصية.

اسم الملف: src/main.rs

struct ImportantExcerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().expect("Could not find a '.'");
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}

الشيفرة 24: هيكل يحتوي على مرجع، وبذلك يتطلب توصيف دورة الحياة

يحتوي الهيكل على حقل يدعى part يخزّن داخله شريحة سلسلة نصية وهي مرجع، وينبغي علينا هنا التصريح عن اسم معامل دورة الحياة المعممة داخل أقواس مثلثة بعد اسم الهيكل كما هو الحال مع الأنواع المعممة وذلك حتى يتسنّى لنا استخدام معامل دورة الحياة في متن تعريف الهيكل، وتعني طريقة الكتابة هذه أنه لا يوجد أي نسخة من ImportantExcerpt تعيش أطول من المرجع الموجود في الحقل part.

تُنشئ الدالة main هنا نسخةً من الهيكل ImportantExcerpt بحيث يحتوي على مرجع للجملة الأولى من String والمملوك من قبل المتغير novel، والبيانات في novel موجودةٌ قبل إنشاء نسخة من ImportantExcerpt، بالإضافة إلى ذلك فإن novel لا تخرج من النطاق إلى أن يخرج ImportantExcerpt من النطاق. إذًا، فالمرجع الموجود في نسخة ImportantExcerpt صالح.

إخفاء دورة الحياة

تعلمنا أنه لكل مرجع ما دورة حياة ويجب أن نحدّد معاملات دورة الحياة للدوال أو للهياكل التي تستخدم المراجع، إلا أننا كتبنا دالةً في السابق (الشيفرة 9) كما هي موضحة في الشيفرة 25، وقد صُرَِفت بنجاح دون استخدام توصيف دورة الحياة.

اسم الملف: src/lib.rs

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

الشيفرة 25: دالة عرفناها سابقًا وصرّفت بنجاح دون استخدام توصيف دورة الحياة على الرغم من كون كل من المعاملات والقيمة المعادة مراجع

السبب في تصريف الشيفرة السابقة بنجاح هو سبب تاريخي، إذ لن تُصرَّف الشيفرة البرمجية هذه في الإصدارات السابقة من رست (قبل 1.0)، وذلك لحاجة كل مرجع لدورة حياة صريحة. إليك ما ستبدو عليه بصمة الدالة في ذلك الوقت من تطوير اللغة:

fn first_word<'a>(s: &'a str) -> &'a str {

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

هذه النقطة في تاريخ تطوير رست مهمة لأنه من الممكن ظهور المزيد من الأنماط مستقبلًا وإضافتها إلى المصرف، وبذلك قد لا نحتاج لاستخدام توصيف دورات الحياة مباشرةً في العديد من الحالات.

تُدعى هذه الأنماط الموجودة لتحليل المراجع بقواعد إخفاء دورة الحياة lifetime elision rules، إلا أن هذه القواعد ليست للمبرمجين حتى يتبعونها بل هي مجموعة من الحالات التي سينظر إليها المصرّف، إذ لن تحتاج لاستخدام توصيف دورات الحياة مباشرةً إذا كانت شيفرتك البرمجية تندرج ضمن واحدة من هذه الحالات.

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

تُدعى دورات الحياة لمعاملات دالة أو تابع بدورات حياة الدخل input lifetimes بينما تُدعى دورات الحياة الخاصة بالقيم المُعادة بدورات حياة الخرج output lifetimes.

يستخدم المصرف ثلاث قواعد لمعرفة دورات حياة المراجع عندما لا يوجد هناك توصيف مباشر لها: تُطبَّق القاعدة الأولى على دورات حياة الدخل والثانية والثالثة على دورات حياة الخرج. يتوقف المصرّف ويعطينا خطأ إذا تحقق من القواعد الثلاث ولم يتعرف على كل دورات حياة المراجع، وتنطبق هذه القواعد على تعاريف fn بالإضافة إلى كتل impl.

تتمثل القاعدة الأولى بإسناد المصرف معامل دورة حياة لكل معامل يشكّل مرجع، بكلمات أخرى: تحصل دالةً تحتوي على معامل واحد على معامل دورة حياة واحد fn foo<'a>(x: &'a i32)‎، بينما تحصل دالة تحتوي على معاملين على دورتَي حياة منفصلتين foo<'a, 'b>(x: &'a i32, y: &'b i32)‎، وهلمّ جرًا.

تنص القاعدة الثانية على وجود معامل دورة حياة دخل واحد فقط، وتُسند دورة الحياة هذه إلى جميع معاملات دورة حياة الخرج كالآتي:

 fn foo<'a>(x: &'a i32) -> &'a i32

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

لنفترض أننا المصرّف. دعنا نطبّق هذه القواعد لمعرفة دورات حياة المراجع في بصمة الدالة first_word في الشيفرة 25. تبدأ بصمة الدالة دون أي دورات حياة مرتبطة بالمراجع:

fn first_word(s: &str) -> &str {

يطبّق المصرف القاعدة الأولى التي تقتضي بأن كل معامل سيحصل على دورة حياة خاصة بها، دعنا نسمّي دورة الحياة باسم ‎'a كالمعتاد. أصبحت لدينا بصمة الدالة بالشكل التالي:

fn first_word<'a>(s: &'a str) -> &str {

نطبق القاعدة الثانية لوجود دورة حياة دخل واحدة، وتحدِّد القاعدة الثانية أن دورة حياة معامل الداخل تُسند إلى دورة حياة الخرج، فتصبح بصمة الدالة كما يلي:

fn first_word<'a>(s: &'a str) -> &'a str {

أصبح الآن لجميع المراجع الموجودة في بصمة الدالة دورة حياة، ويمكن للمصرف أن يستمرّ بتحليله دون حاجة المبرمج لتوصيف دورات الحياة في بصمة الدالة.

دعنا ننظر إلى مثال آخر، نستخدم هذه المرة الدالة longest التي لا تحتوي على معاملات دورة حياة عندما بدأنا بكتابتها في الشيفرة 20 سابقًا:

fn longest(x: &str, y: &str) -> &str {

نطبّق القاعدة الأولى: يحصل كل معامل على دورة حياة خاصة به. لدينا الآن في هذه الحالة معاملين بدلًا من واحد، لذا سنحصل على دورتين حياة:

fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {

يمكنك رؤية أن القاعدة الثانية لا تنطبق على هذه الحالة لوجود أكثر من دورة حياة دخل واحدة، كما أن القاعدة الثالثة لا تنطبق لأن longest دالة وليست تابع، إذًا لا يوجد في معاملاتها self. لم نتوصل إلى دورة حياة النوع المُعادة بعد تطبيق القواعد الثلاث، وهذا هو السبب في حصولنا على خطأ عند محاولة تصريف الشيفرة 20، إذ أن المصرف تحقق من قواعد إخفاء دورة الحياة ولكنه لم يتعرف على جميع دورات حياة المراجع في بصمة الدالة.

سننظر إلى دورات الحياة في سياق التوابع بما أن القاعدة الثالثة تنطبق فقط في بصمات التوابع، مما سيكشف لنا السبب في كون توصيف دورات الحياة ضمن التوابع غير مُستخدم معظم الأحيان.

توصيف دورة الحياة في تعاريف التابع

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

قد ترتبط المراجع في بصمة التابع داخل الكتلة impl بدورات حياة المراجع الخاصة بحقول الهيكل، وقد تكون مستقلةً عن بعضها الآخر، كما أن قوانين إخفاء دورة الحياة تجعل من توصيف دورات الحياة غير ضروري في بصمات التابع معظم الأحيان. دعنا ننظر إلى بعض الأمثلة باستخدام هيكل يدعى ImportantExcerpt وهو هيكل عرّفناه سابقًا في الشيفرة 24.

لنستخدم أولًا تابعًا يدعى level يحتوي على معامل واحد يمثل مرجعًا إلى self ويُعيد قيمة من النوع i32 (أي لا تمثّل مرجعًا):

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

التصريح عن معامل دورة الحياة بعد impl واستخدامه بعد اسم النوع مطلوب، إلا أنه من غير المطلوب توصيف دورة حياة مرجع self بفضل قاعدة إخفاء دورة الحياة الأولى.

إليك مثالًا ينطبق عليه قاعدة إخفاء دورة الحياة الثالثة:

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part
    }
}

هناك دورتا حياة دخل، لذا يطبق رست القاعدة الأولى ويمنح لكل من ‎&self و announcment دورة حياة خاصة بهما، ومن ثم يحصل النوع المُعادة على دورة الحياة ‎&self لأن إحدى معاملات التابع قيمته ‎&self، وبهذا يجري التعرف على جميع دورات الحياة الموجودة.

دورة الحياة الساكنة

يجب أن نناقش واحدةً من دورات الحياة المميزة ألا وهي static' وهي تُشير إلى أن المرجع يمكن أن يعيش طوال فترة البرنامج، ولدى جميع السلاسل النصية نوع دورة الحياة الساكنة static'، ويمكننا توصيفه بالشكل التالي:

let s: &'static str = "I have a static lifetime.";

يُخزّن النص الموجود في السلسلة النصية في ملف البرنامج التنفيذي مباشرةً أي أنه مرئي طوال الوقت، بالتالي فإن دورة حياة جميع السلاسل النصية المجردة literals هي static'.

قد تجد اقتراحات لاستخدام دورة الحياة 'static في رسائل الخطأ، إلا أنه يجب عليك أن تفكر فيما إذا كان المرجع بذاته يعيش طيلة دورة حياة البرنامج أم لا إذا أردت اتباع هذا الاقتراح وفيما إذا كنت تريد هذا الشيء حقًا أم لا، وتنتج رسالة الخطأ التي تقترح دورة حياة static' معظم الأحيان من محاولة إنشاء مرجع معلّق أو حالة عدم تطابق ما بين دورات الحياة الموجودة، وفي هذه الحالة فالحل الأمثل هو بحل هذه المشاكل وليس بتحديد دورة الحياة الساكنة static'.

معاملات الأنواع المعممة وحدود السمة ودورات الحياة معا

دعنا ننظر إلى طريقة تحديد معاملات الأنواع المعممة وحدود السمة ودورات الحياة في دالة واحدة سويًا.

use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: Display,
{
    println!("Announcement! {}", ann);
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

تمثل الشيفرة البرمجية السابقة دالة longest من الشيفرة 21 سابقًا التي تُعيد أطول شريحة نصية من شريحتين نصيتين، إلا أننا أضفنا هنا معاملًا جديدًا يدعى ann من نوع معمّم T الذي يُمكن أن يُملأ بأي نوع يطبّق السمة Display كما هو محدد في بنية where، وسيُطبع هذا المعامل الإضافي باستخدام {} وهذا هو السبب في جعل حدود السمة Display ضرورية. نكتب كل من تصاريح معاملات دورة الحياة ‎'a ومعامل النوع المعمم T في القائمة ذاتها داخل الأقواس المثلثة بعد اسم الدالة وذلك لأن دورات الحياة هي نوع من الأنواع المعمّمة.

ترجمة -وبتصرف- لقسم من الفصل Generic Types, Traits, and Lifetimes من كتاب The Rust Programming Language.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...