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

المغلفات closures في لغة رست Rust


Naser Dakhel

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

الحصول على المعلومات من البيئة باستخدام المغلفات

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

هناك عدة طرق لتطبيق ذلك، إذ يمكننا على سبيل المثال استخدام معدّد enum يدعى ShirtColor يحتوي على متغايرين variants هما‏ Red و Blue (حددنا لونين فقط للبساطة). نمثّل مخزن الشركة باستخدام الهيكل‏ Inventory الذي يحتوي على حقل يدعى shirts يحتوي على النوع Vec<ShirtColor>‎، الذي يمثّل لون القميص الموجود حاليًا في المخزن. يحصل التابع giveaway المُعرّف في Inventory على لون القميص المفضّل الاختياري للمستخدم من المستخدمين الرابحين القميص مجانًا ويُعيد لون القميص الذي سيحصل عليه المستخدم. التطبيق لكل ما سبق ذكره موضح في الشيفرة 1:

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

#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
    Red,
    Blue,
}

struct Inventory {
    shirts: Vec<ShirtColor>,
}

impl Inventory {
    fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
        user_preference.unwrap_or_else(|| self.most_stocked())
    }

    fn most_stocked(&self) -> ShirtColor {
        let mut num_red = 0;
        let mut num_blue = 0;

        for color in &self.shirts {
            match color {
                ShirtColor::Red => num_red += 1,
                ShirtColor::Blue => num_blue += 1,
            }
        }
        if num_red > num_blue {
            ShirtColor::Red
        } else {
            ShirtColor::Blue
        }
    }
}

fn main() {
    let store = Inventory {
        shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue],
    };

    let user_pref1 = Some(ShirtColor::Red);
    let giveaway1 = store.giveaway(user_pref1);
    println!(
        "The user with preference {:?} gets {:?}",
        user_pref1, giveaway1
    );

    let user_pref2 = None;
    let giveaway2 = store.giveaway(user_pref2);
    println!(
        "The user with preference {:?} gets {:?}",
        user_pref2, giveaway2
    );
}

الشيفرة 1: شيفرة توزيع القمصان للمستخدمين

يحتوي المتغير store المعرف في الدالة main على قميصين أحدهما باللون الأزرق والآخر باللون الأحمر للتوزيع ضمن حملة التسويق هذه. نستدعي التابع giveaway للمستخدم الذي يفضّل القميص الأحمر وللمستخدم الذي ليس لديه أي تفضيل معين.

يمكن تطبيق الشيفرة البرمجية بمختلف الطرق، إلا أننا نركز هنا على استخدام المغلفات، لذا فقد التزمنا بالمفاهيم التي تعلمتها مسبقًا باستثناء ما بداخل التابع giveaway الذي يستخدم مغلّفًا. نحصل على تفضيل المستخدم مثل معامل من النوع Option<ShirtColor>‎ في التابع giveaway ونستدعي التابع unwrap_or_else على user_preference. التابع unwrap_or_else على النوع Option<T>‎ مُعرّف في المكتبة القياسية ويأخذ وسيطًا واحدًا ألا وهو مغلف دون أي وسطاء يعيد القيمة T (النوع ذاته المُخزن في المتغاير Some داخل النوع Option<T>‎، وفي هذه الحالة ShirtColor). إذا كان النوع Option<T>‎ هو المتغاير Some، سيُعيد التابع unwrap_or_else القيمة الموجودة داخل Some، وإذا كان المتغاير داخل النوع Option<T>‎ هو None، سيستدعي التابع المغلف ويُعيد القيمة المُعادة من المغلف.

نحدد تعبير المغلف بالشكل ‎|| self.most_stocked()‎ مثل وسيط للتابع unwrap_or_else. لا يأخذ هذا المغلف أي معاملات، إذ نضع المعاملات بين الخطين العموديين إذا احتوى المغلف على معاملات. يستدعي متن المغلف التابع self.most_stocked()‎، ونعرّف هنا المغلف بحيث يُقيّم تطبيق unwrap_or_else المغلف لاحقًا إذا احتجنا للنتيجة.

يطبع تنفيذ الشيفرة البرمجية السابقة الخرج التالي:

$ cargo run
   Compiling shirt-company v0.1.0 (file:///projects/shirt-company)
    Finished dev [unoptimized + debuginfo] target(s) in 0.27s
     Running `target/debug/shirt-company`
The user with preference Some(Red) gets Red
The user with preference None gets Blue

الجانب المثير للاهتمام هنا هو أننا نمرر مغلفًا يستدعي self.most_stocked()‎ على نسخة Inventory الحالية. لا تحتاج المكتبة القياسية لمعرفة أي شيء بخصوص النوعين Inventory أو ShirtColor الذين عرّفناهما، أو المنطق الذي نريد استخدامه في هذه الحالة، إذ أن المغلف يحصل على مرجع ثابث immutable reference إلى self الخاصة بنسخة Inventory، ثم يمرره إلى الشيفرة البرمجية التي كتبناها داخل التابع unwrap_or_else. إذا قارنّا هذه العملية بالتوابع، فالتوابع غير قادرة على الحصول على هذه المعلومات من البيئة بالطريقة ذاتها.

استنتاج نوع المغلف وتوصيفه

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

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

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

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

    let expensive_closure = |num: u32| -> u32 {
        println!("calculating slowly...");
        thread::sleep(Duration::from_secs(2));
        num
    };

الشيفرة 2: إضافة توصيف اختياري لأنواع المعاملات والقيمة المُعادة في المغلف

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

fn  add_one_v1   (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x|             { x + 1 };
let add_one_v4 = |x|               x + 1  ;

يوضح السطر الأول تعريف دالة، بينما يوضح السطر الثاني تعريف مغلف موصّف بالكامل، ثمّ نزيل في السطر الثالث التوصيف من تعريف المغلف، ونزيل في السطر الرابع الأقواس الاختيارية لأن محتوى المغلف يتألف من تعبير واحد، وجميع التعاريف السابقة تعاريف صالحة الاستخدام تمنحنا السلوك ذاته عند استدعائها. يتطلب السطران add_one_v3 و add_one_v4 تقييم قيمة المغلف حتى يجري تصريفهما، لأن الأنواع يجب أن تُسنتج من خلال استخدامهما وهذا الأمر مشابه لحاجة السطر let v = Vec::new();‎ لتوصيف النوع أو إضافة قيم من نوع ما إلى Vec، بحيث تستطيع رست استنتاج النوع.

يستنتج المصرف نوعًا واحدًا ثابتًا لكل من المعاملات في حال تعريف المغلفات، إضافةً إلى النوع المُعاد منها. على سبيل المثال، توضح الشيفرة 3 تعريف مغلف قصير يُعيد القيمة التي يتلقاها مثل معاملٍ له، وهذا المغلف ليس مفيدًا جدًا إلا أن هدفه توضيحي. لاحظ أننا لم نضِف أي توصيف للنوع في التعريف وبالتالي يمكننا استدعاء المغلف باستخدام أي نوع، وهو ما فعلناه في الشيفرة 3، إذ استدعينا المغلّف في المرة الأولى باستخدام النوع String، إلا أننا حصلنا على خطأ عند استدعائنا للمغلف example_closure باستخدام قيمة عدد صحيح integer.

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

    let example_closure = |x| x;

    let s = example_closure(String::from("hello"));
    let n = example_closure(5);

الشيفرة 3: محاولة استدعاء مغلف يُستنتج نوعه باستخدام نوعين مختلفين

يعرض لنا المصرّف الخطأ التالي:

$ cargo run
   Compiling closure-example v0.1.0 (file:///projects/closure-example)
error[E0308]: mismatched types
 --> src/main.rs:5:29
  |
5 |     let n = example_closure(5);
  |             --------------- ^- help: try using a conversion method: `.to_string()`
  |             |               |
  |             |               expected struct `String`, found integer
  |             arguments to this function are incorrect
  |
note: closure parameter defined here
 --> src/main.rs:2:28
  |
2 |     let example_closure = |x| x;
  |                            ^

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

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

الحصول على المراجع أو نقل الملكية

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

نعرّف في الشيفرة 4 مغلفًا يحصل على مرجع ثابت لشعاع يدعى list لأنه يحتاج فقط للمراجع الثابتة لطباعة القيمة:

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

fn main() {
    let list = vec![1, 2, 3];
    println!("Before defining closure: {:?}", list);

    let only_borrows = || println!("From closure: {:?}", list);

    println!("Before calling closure: {:?}", list);
    only_borrows();
    println!("After calling closure: {:?}", list);
}

الشيفرة 4: تعريف مغلف واستدعاءه، بحيث يحصل هذا المغلف على مرجع ثابت

يوضح هذا المثال أيضًا أنه من الممكن للمتغير أن يرتبط بتعريف مغلف، ويمكننا لاحقًا استدعاء المغلف باستخدام اسم المتغير والقوسين وكأن اسم المتغير يمثل اسم دالة.

يمكن الوصول للشعاع list من الشيفرة البرمجية قبل تعريف المغلف وبعد تعريفه، شرط أن يكون قبل استدعاء المغلف وبعد استدعاء المغلف، وذلك لأنه من الممكن وجود عدّة مراجع ثابتة للشعاع list في ذات الوقت، وتُصرَّف الشيفرة البرمجية بنجاح وتُنفَّذ وتطبع التالي:

$ cargo run
   Compiling closure-example v0.1.0 (file:///projects/closure-example)
    Finished dev [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
Before calling closure: [1, 2, 3]
From closure: [1, 2, 3]
After calling closure: [1, 2, 3]

نعدّل في الشيفرة 5 متن المغلف بحيث نضيف عنصرًا إلى الشعاع list، وبالتالي يحصل المغلف الآن على مرجع متغيّر:

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

fn main() {
    let mut list = vec![1, 2, 3];
    println!("Before defining closure: {:?}", list);

    let mut borrows_mutably = || list.push(7);

    borrows_mutably();
    println!("After calling closure: {:?}", list);
}

الشيفرة 5: تعريف واستدعاء مغلف يحتوي على مرجع متغيّر

تُصرَّف الشيفرة البرمجية بنجاح وتُنفَّذ ونحصل على الخرج التالي:

$ cargo run
   Compiling closure-example v0.1.0 (file:///projects/closure-example)
    Finished dev [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
After calling closure: [1, 2, 3, 7]

لاحظ أننا لا نستخدم println!‎ بين تعريف المغلف borrows_mutably واستدعائه، إذ يحصل المغلف على مرجع متغيّر للشعاع list عند تعريفه، ولا نستخدم المغلف مجددًا بعد استدعائه لذا تنتهي الاستعارة المتغيرة عندها. لا يُسمح بطباعة مرجع متغيّر بين تعريف المغلف واستدعائه، إذ لا يُسمح بوجود أي عمليات استعارة أخرى عند وجود استعارة متغيّرة. حاول إضافة استدعاء للماكرو println!‎ لترى رسالة الخطأ التي تظهر لك.

إذا أردت إجبار المغلف على أخذ ملكية القيم التي يستخدمها في البيئة على الرغم من أن متن المغلف لا يتطلب أخذ الملكية، فيمكنك استخدام الكلمة المفتاحية move قبل قائمة المعاملات.

هذه الطريقة مفيدة خصوصًا عند تمرير مغلف لخيط thread جديد لنقل البيانات، بحيث تُمتلك بواسطة الخيط الجديد. سنناقش الخيوط بالتفصيل وسبب استخدامنا لها لاحقًا، لكن دعنا للوقت الحالي نستكشف عملية إضافة خيط جديد باستخدام مغلّف يحتاج الكلمة المفتاحية move. توضح الشيفرة 6 إصدارًا عن الشيفرة 4، إلا أننا نطبع الشعاع هنا في خيط جديد بدلًا من الخيط الرئيسي:

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

use std::thread;

fn main() {
    let list = vec![1, 2, 3];
    println!("Before defining closure: {:?}", list);

    thread::spawn(move || println!("From thread: {:?}", list))
        .join()
        .unwrap();
}

الشيفرة 6: استخدام move لإجبار خيط المغلف على أخذ ملكية list

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

نقل القيم خارج المغلفات وسمات Fn

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

تؤثر الطريقة التي يحصل بها المغلف على القيم ويتعامل معها من البيئة على السمات التي يطبقها المغلف والسمات traits هي الطريقة التي تستطيع فيها كل من الدوال والهياكل تحديد نوع المغلفات الممكن استخدامها. تطبّق المغلفات تلقائيًا سمةً أو سمتين أو ثلاث من سمات Fn التالية على نحوٍ تراكمي بحسب تعامل متن المغلف للقيم:

  1. السمة FnOnce: تُطبَّق على جميع المغلفات الممكن استدعاؤها مرةً واحدة. تُطبّق جميع المغلفات هذه السمة على الأقل لأنه يمكن لجميع المغلفات أن تُستدعى، وسيطبق المغلف الذي ينقل القيم خارج متنه السمة FnOnce فقط دون أي سمات Fn أخرى لأنه يُمكن استدعاءه مرةً واحدةً فقط.
  2. السمة FnMut: تُطبَّق على جميع المغلفات التي لا تنقل القيم خارج متنها، إلا أنها من الممكن أن تعدّل على هذه القيم، ويمكن استدعاء هذه المغلفات أكثر من مرة واحدة.
  3. السمة Fn: تُطبَّق على المغلفات التي لا تنقل القيم خارج متنها ولا تعدل على القيم، إضافةً إلى المغلفات التي لا تحصل على أي قيم من البيئة. يمكن لهذه المغلفات أن تُستدعى أكثر من مرة واحدة دون التعديل على بيئتها وهو أمرٌ مهم في حالة استدعاء مغلف عدة مرات على نحوٍ متعاقب.

دعنا ننظر إلى تعريف التابع unwrap_or_else على النوع Option<T>‎ الذي استخدمناه في الشيفرة 1:

impl<T> Option<T> {
    pub fn unwrap_or_else<F>(self, f: F) -> T
    where
        F: FnOnce() -> T
    {
        match self {
            Some(x) => x,
            None => f(),
        }
    }
}

تذكّر أن T هو نوع معمم generic type يمثل نوع القيمة في المتغاير Some للنوع Option، وهذا النوع T يمثل أيضًا نوع القيمة المعادة من الدالة unwrap_or_else، إذ ستحصل الشيفرة البرمجية التي تستدعي unwrap_or_else على النوع Option<String>‎ على النوع String على سبيل المثال.

لاحظ تاليًا أن الدالة unwrap_or_else تحتوي على معامل نوع معمم إضافي يُدعى F والنوع F هنا هو نوع المعامل ذو الاسم f وهو المغلف الذي نمرره للدالة unwrap_or_else عند استدعائها.

حد السمة trait bound المحدد على النوع المعمم E هو FnOnce() -> T، مما يعني أن النوع F يجب أن يكون قابلًا للاستدعاء مرةً واحدة وألّا يأخذ أي وسطاء وأن يعيد قيمةً من النوع T. يوضح استخدام FnOnce في حد السمة القيد: أن unwrap_or_else ستستدعي f مرةً واحدةً على الأكثر. يمكنك من رؤية متن الدالة unwrap_or_else معرفة أن f لن تُستدعى إذا كان المتغاير Some موجودًا في Option، بينما ستُستدعى f مرةً واحدة إذا وُجد المتغاير None في Option. تقبل الدالة unwrap_or_else أنواعًا مختلفة من المغلفات بصورةٍ مرنة، لأن جميع المغلفات تطبّق السمة FnOnce.

ملاحظة: يمكن أن تطبق الدوال سمات Fn الثلاث أيضًا. يمكننا استخدام اسم الدالة بدلًا من مغلف عندما نريد شيئًا يطبق واحدةً من سمات Fn إذا كان ما نريد فعله لا يتطلب الحصول على قيمة من البيئة. يمكننا على سبيل المثال، استدعاء unwrap_or_else(Vec::new)‎ على قيمة Option<Vec<T>>‎ للحصول على شعاع فارغ جديد إذا كانت القيمة هي None.

دعنا ننظر الآن إلى تابع المكتبة القياسية sort_by_key المعرف على الشرائح slices لرؤية الاختلاف بينه وبين unwrap_or_else وسبب استخدام sort_by_key للسمة FnMut بدلًا من السمة FnOnce لحد السمة. يحصل المغلف على وسيط واحد على هيئة مرجع للعنصر الحالي في الشريحة ويُعيد قيمةً من النوع K يمكن ترتيبها، وهذه الدالة مفيدة عندما تريد ترتيب شرحة بسمة attribute معينة لكل عنصر. لدينا في الشيفرة 7 قائمة من نسخ instances من الهيكل Rectangle ونستخدم sort_by_key لترتيبها حسب سمة width من الأصغر إلى الأكبر:

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

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];

    list.sort_by_key(|r| r.width);
    println!("{:#?}", list);
}

الشيفرة 7: استخدام sort_by_key لترتيب المستطيلات بحسب عرضها

نحصل على الخرج التالي مما سبق:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished dev [unoptimized + debuginfo] target(s) in 0.41s
     Running `target/debug/rectangles`
[
    Rectangle {
        width: 3,
        height: 5,
    },
    Rectangle {
        width: 7,
        height: 12,
    },
    Rectangle {
        width: 10,
        height: 1,
    },
]

السبب في كون sort_by_key معرفًا ليأخذ مغلفًا يطبق السمة FnMut هو استدعاء الدالة للمغلف عدة مرات: مرةً واحدةً لكل عنصر في الشريحة. لا يحصل المغلف ‎|r| r.width على أي قيمة من البيئة أو يعدل عليها أو ينقلها لذا فهو يحقق شروط حد السمة هذه.

توضح الشيفرة 8 مثالًا لمغلف على النقيض، إذ يطبق هذا المغلف السمة FnOnce فقط لأنه ينقل قيمة خارج البيئة، ولن يسمح لنا المصرّف باستخدام هذا المغلف مع sort_by_key:

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

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];

    let mut sort_operations = vec![];
    let value = String::from("by key called");

    list.sort_by_key(|r| {
        sort_operations.push(value);
        r.width
    });
    println!("{:#?}", list);
}

الشيفرة 8: محاولة استخدام مغلف يطبق السمة FnOnce فقط مع التابع sort_by_key

يمثّل ما سبق طريقةً معقدة (لا تعمل بنجاح) لمحاولة عدّ المرات التي يُستدعى بها التابع sort_by_key عند ترتيب list، وتحاول الشيفرة البرمجية تحقيق ذلك بإضافة value -ألا وهي قيمة من النوع String من بيئة المغلف- إلى الشعاع sort_operations. يحصل المغلف على القيمة value، ثم ينقلها خارج المغلف بنقل ملكيتها إلى الشعاع sort_operations، ويمكن أن يُستدعى هذا المغلف مرةً واحدةً إلا أن محاولة استدعائه للمرة الثانية لن تعمل لأن value لن يكون في البيئة ليُضاف إلى الشعاع sort_operations مجددًا، وبالتالي يطبق هذا المغلف السمة FnOnce فقط، وعندما نحاول تصريف الشيفرة البرمجية السابقة، سنحصل على خطأ مفاده أن value لا يمكن نقلها خارج المغلف لأن المغلف يجب أن يطبّق السمة FnMut:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
error[E0507]: cannot move out of `value`, a captured variable in an `FnMut` closure
  --> src/main.rs:18:30
   |
15 |     let value = String::from("by key called");
   |         ----- captured outer variable
16 |
17 |     list.sort_by_key(|r| {
   |                      --- captured by this `FnMut` closure
18 |         sort_operations.push(value);
   |                              ^^^^^ move occurs because `value` has type `String`, which does not implement the `Copy` trait

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

يشير الخطأ إلى السطر الذي ننقل فيه القيمة value خارج البيئة داخل متن المغلف، ولتصحيح هذا الخطأ علينا تعديل متن المغلف بحيث لا ينقل القيم خارج البيئة. نحافظ على وجود عدّاد في البيئة ونزيد قيمته داخل المغلف بحيث نستطيع عدّ المرات التي يُستدعى فيها التابع sort_by_key. يعمل المغلف في الشيفرة 9 مع sort_by_key لأنه يحصل فقط على المرجع المتغيّر الخاص بالعداد num_sort_operations ويمكن بالتالي استدعاؤه أكثر من مرة واحدة:

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

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];

    let mut num_sort_operations = 0;
    list.sort_by_key(|r| {
        num_sort_operations += 1;
        r.width
    });
    println!("{:#?}, sorted in {num_sort_operations} operations", list);
}

الشيفرة 9: استخدام مغلف يطبق السمة FnMut مع التابع sort_by_key دون الحصول على أخطاء

سمات Fn مهمة عند تعريف أو استخدام الدوال أو الأنواع التي تستخدم المغلفات. سنناقش في المقال التالي المكررات iterators، إذ أن العديد من توابع المكررات تأخذ المغلفات مثل وسطاء، لذا تذكّر التفاصيل المتعلقة بالمغلّفات عند قراءة المقالة التالية.

ترجمة -وبتصرف- لقسم من الفصل Functional Language Features: Iterators and Closures من كتاب 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.


×
×
  • أضف...