Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

الدوال والإغلاقات المتقدمة (Advanced Functions and Closures)

يستكشف هذا القسم بعض الميزات المتقدمة المتعلقة بالدوال والإغلاقات (closures)، بما في ذلك مؤشرات الدوال (function pointers) وإرجاع الإغلاقات.

مؤشرات الدوال (Function Pointers)

لقد تحدثنا عن كيفية تمرير الإغلاقات (closures) إلى الدوال؛ يمكنك أيضًا تمرير الدوال العادية إلى الدوال! هذه التقنية مفيدة عندما تريد تمرير دالة قمت بتعريفها بالفعل بدلاً من تعريف إغلاق جديد. تتحول الدوال تلقائيًا إلى النوع fn (بحرف f صغير)، ولا ينبغي الخلط بينه وبين سمة الإغلاق Fn. يسمى النوع fn بـ مؤشر الدالة (function pointer). سيسمح لك تمرير الدوال باستخدام مؤشرات الدوال باستخدام الدوال كمعاملات (arguments) لدوال أخرى.

إن بناء الجملة (syntax) لتحديد أن المعامل هو مؤشر دالة يشبه بناء جملة الإغلاقات، كما هو موضح في القائمة 20-28، حيث قمنا بتعريف دالة add_one تضيف 1 إلى معاملها. تأخذ الدالة do_twice معاملين: مؤشر دالة لأي دالة تأخذ معامل i32 وترجع i32 وقيمة i32 واحدة. تستدعي الدالة do_twice الدالة f مرتين، وتمرر لها قيمة arg ثم تجمع نتيجتي استدعاء الدالة معًا. تستدعي الدالة main الدالة do_twice بالمعاملات add_one و 5.

fn add_one(x: i32) -> i32 {
    x + 1
}

fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
    f(arg) + f(arg)
}

fn main() {
    let answer = do_twice(add_one, 5);

    println!("The answer is: {answer}");
}

يطبع هذا الكود The answer is: 12. نحدد أن المعامل f في do_twice هو fn يأخذ معاملًا واحدًا من النوع i32 ويرجع i32. يمكننا بعد ذلك استدعاء f في جسم do_twice. في main يمكننا تمرير اسم الدالة add_one كمعامل أول لـ do_twice.

على عكس الإغلاقات، فإن fn هو نوع (type) وليس سمة (trait)، لذا نحدد fn كنوع المعامل مباشرةً بدلاً من التصريح عن معامل نوع عام (generic type parameter) مع إحدى سمات Fn كقيد سمة (trait bound).

تنفذ مؤشرات الدوال جميع سمات الإغلاق الثلاث (Fn و FnMut و FnOnce) مما يعني أنه يمكنك دائمًا تمرير مؤشر دالة كمعامل لدالة تتوقع إغلاقًا. من الأفضل كتابة الدوال باستخدام نوع عام وإحدى سمات الإغلاق حتى تتمكن دوالك من قبول إما دوال أو إغلاقات.

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

كمثال على المكان الذي يمكنك فيه استخدام إغلاق محدد في السطر (inline closure) أو دالة مسماة، دعنا ننظر إلى استخدام طريقة map التي توفرها سمة Iterator في المكتبة القياسية (standard library). لاستخدام طريقة map لتحويل متجه (vector) من الأرقام إلى متجه من السلاسل النصية (strings)، يمكننا استخدام إغلاق، كما في القائمة 20-29.

fn main() {
    let list_of_numbers = vec![1, 2, 3];
    let list_of_strings: Vec<String> =
        list_of_numbers.iter().map(|i| i.to_string()).collect();
}

أو يمكننا تسمية دالة كمعامل لـ map بدلاً من الإغلاق. توضح القائمة 20-30 كيف سيبدو هذا.

fn main() {
    let list_of_numbers = vec![1, 2, 3];
    let list_of_strings: Vec<String> =
        list_of_numbers.iter().map(ToString::to_string).collect();
}

لاحظ أنه يجب علينا استخدام بناء الجملة المؤهل بالكامل (fully qualified syntax) الذي تحدثنا عنه في قسم “السمات المتقدمة” لأن هناك دوال متعددة متاحة باسم to_string.

هنا، نستخدم دالة to_string المعرفة في سمة ToString والتي نفذتها المكتبة القياسية لأي نوع ينفذ Display.

تذكر من قسم “قيم التعداد” في الفصل 6 أن اسم كل متغير تعداد (enum variant) نقوم بتعريفه يصبح أيضًا دالة تهيئة (initializer function). يمكننا استخدام دوال التهيئة هذه كمؤشرات دوال تنفذ سمات الإغلاق، مما يعني أنه يمكننا تحديد دوال التهيئة كمعاملات للطرق التي تأخذ إغلاقات، كما هو موضح في القائمة 20-31.

fn main() {
    enum Status {
        Value(u32),
        Stop,
    }

    let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect();
}

هنا، نقوم بإنشاء مثيلات Status::Value باستخدام كل قيمة u32 في النطاق الذي يتم استدعاء map عليه باستخدام دالة التهيئة لـ Status::Value. يفضل بعض الأشخاص هذا الأسلوب ويفضل البعض الآخر استخدام الإغلاقات. يتم ترجمتها (compile) إلى نفس الكود، لذا استخدم الأسلوب الأوضح بالنسبة لك.

إرجاع الإغلاقات (Returning Closures)

يتم تمثيل الإغلاقات بواسطة السمات، مما يعني أنه لا يمكنك إرجاع الإغلاقات مباشرةً. في معظم الحالات التي قد ترغب فيها في إرجاع سمة، يمكنك بدلاً من ذلك استخدام النوع الملموس (concrete type) الذي ينفذ السمة كقيمة إرجاع للدالة. ومع ذلك، لا يمكنك عادةً القيام بذلك مع الإغلاقات لأنها لا تملك نوعًا ملموسًا يمكن إرجاعه؛ لا يُسمح لك باستخدام مؤشر الدالة fn كنوع إرجاع إذا كان الإغلاق يلتقط أي قيم من نطاقه (scope)، على سبيل المثال.

بدلاً من ذلك، ستستخدم عادةً بناء جملة impl Trait الذي تعلمناه في الفصل 10. يمكنك إرجاع أي نوع دالة باستخدام Fn و FnOnce و FnMut. على سبيل المثال، سيتم ترجمة الكود الموجود في القائمة 20-32 بشكل جيد تمامًا.

#![allow(unused)]
fn main() {
fn returns_closure() -> impl Fn(i32) -> i32 {
    |x| x + 1
}
}

ومع ذلك، كما لاحظنا في قسم “استنتاج وتحديد أنواع الإغلاق” في الفصل 13، فإن كل إغلاق هو أيضًا نوعه المتميز الخاص. إذا كنت بحاجة إلى العمل مع دوال متعددة لها نفس التوقيع (signature) ولكن بتنفيذات مختلفة، فستحتاج إلى استخدام كائن سمة (trait object) لها. فكر فيما يحدث إذا كتبت كودًا مثل الموضح في القائمة 20-33.

fn main() {
    let handlers = vec![returns_closure(), returns_initialized_closure(123)];
    for handler in handlers {
        let output = handler(5);
        println!("{output}");
    }
}

fn returns_closure() -> impl Fn(i32) -> i32 {
    |x| x + 1
}

fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
    move |x| x + init
}

هنا لدينا دالتان، returns_closure و returns_initialized_closure وكلاهما يرجعان impl Fn(i32) -> i32. لاحظ أن الإغلاقات التي يرجعانها مختلفة، على الرغم من أنها تنفذ نفس النوع. إذا حاولنا ترجمة هذا، فإن Rust تخبرنا أنه لن يعمل:

$ cargo build
   Compiling functions-example v0.1.0 (file:///projects/functions-example)
error[E0308]: mismatched types
  --> src/main.rs:2:44
   |
 2 |     let handlers = vec![returns_closure(), returns_initialized_closure(123)];
   |                                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected opaque type, found a different opaque type
...
 9 | fn returns_closure() -> impl Fn(i32) -> i32 {
   |                         ------------------- the expected opaque type
...
13 | fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
   |                                              ------------------- the found opaque type
   |
   = note: expected opaque type `impl Fn(i32) -> i32`
              found opaque type `impl Fn(i32) -> i32`
   = note: distinct uses of `impl Trait` result in different opaque types

For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions-example` (bin "functions-example") due to 1 previous error

تخبرنا رسالة الخطأ أنه كلما أرجعنا impl Trait فإن Rust تنشئ نوعًا معتمًا (opaque type) فريدًا، وهو نوع لا يمكننا رؤية تفاصيل ما تبنيه Rust لنا فيه، ولا يمكننا تخمين النوع الذي ستنشئه Rust لنكتبه بأنفسنا. لذا، على الرغم من أن هذه الدوال ترجع إغلاقات تنفذ نفس السمة Fn(i32) -> i32 إلا أن الأنواع المعتمة التي تنشئها Rust لكل منها متميزة. (هذا مشابه لكيفية إنتاج Rust لأنواع ملموسة مختلفة لكتل async المتميزة حتى عندما يكون لها نفس نوع الإخراج، كما رأينا في “النوع Pin وسمة Unpin في الفصل 17). لقد رأينا حلاً لهذه المشكلة عدة مرات الآن: يمكننا استخدام كائن سمة، كما في القائمة 20-34.

fn main() {
    let handlers = vec![returns_closure(), returns_initialized_closure(123)];
    for handler in handlers {
        let output = handler(5);
        println!("{output}");
    }
}

fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
    Box::new(|x| x + 1)
}

fn returns_initialized_closure(init: i32) -> Box<dyn Fn(i32) -> i32> {
    Box::new(move |x| x + init)
}

سيتم ترجمة هذا الكود بشكل جيد تمامًا. لمزيد من المعلومات حول كائنات السمات، ارجع إلى قسم “استخدام كائنات السمات للتجريد من السلوك المشترك” في الفصل 18.

بعد ذلك، دعنا ننظر إلى الماكرو (macros)!