تمرير الدوال كوسائط في Rust: 3 طرق لتمرير دالة

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

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

ابدأ الدورة

الدالة كوسيط في Rust: من المبتدئ إلى المحترف

\n\n

هل سبق لك أن كتبت كودًا بلغة Python أو JavaScript ومررت دالة بسهولة إلى دالة أخرى؟ ثم انتقلت إلى Rust ووجدت نفسك عالقًا؟ هذا مألوف. سؤال "How do you pass a Rust function as a parameter?" هو من أكثر الأسئلة شيوعًا على Stack Overflow. وليس بدون سبب: نظام الأنواع والملكية في Rust يضيف عدة طبقات من التعقيد، لكنه يفتح أيضًا إمكانيات جديدة.

\n\n

في هذه المقالة، سنستعرض ثلاث طرق رئيسية: مؤشرات الدوال (fn)، الإغلاقات (closures)، والـ traits. ستتعرف متى تستخدم كل منها، وستتوقف عن الخوف من الأسلوب الوظيفي في Rust.

\n\n

لماذا هذا مهم؟

\n

القدرة على تمرير الدوال كوسائط ليست مجرد خدعة أكاديمية. إنها أساس لـ:

\n
    \n
  • إنشاء مكتبات مرنة وقابلة لإعادة الاستخدام (مثل التكرارات، معالجات الأحداث)؛
  • \n
  • تنفيذ أنماط "الإستراتيجية" و "المراقب"؛
  • \n
  • كتابة كود موجز ومعبر دون الكثير من if-else.
  • \n
\n

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

\n\n

الطريقة 1: مؤشر الدالة (Function Pointer) — fn

\n

أبسط وأكثر طريقة مباشرة. إذا كانت الدالة لا تلتقط متغيرات من البيئة الخارجية (فهي "نقية")، يمكنك استخدام النوع fn. لاحظ: fn هو نوع، بينما Fn و FnMut و FnOnce هي traits. لا تخلط بينهم!

\n\n
fn add_one(x: i32) -> i32 {\n    x + 1\n}\n\n// دالة تستقبل دالة أخرى\nfn apply_twice(f: fn(i32) -> i32, value: i32) -> i32 {\n    f(f(value))\n}\n\nfn main() {\n    let result = apply_twice(add_one, 5);\n    println!("النتيجة: {}", result); // 7\n}
\n\n

متى تستخدم؟ عندما تكون الدالة معروفة في وقت الترجمة، ولا تلتقط متغيرات، ولا تحتاج إلى أداء الإغلاقات. مؤشرات الدوال هي تجريد بدون تكلفة (zero-cost abstraction)، ولا تتطلب تخصيص ذاكرة.

\n\n

قيود fn

\n

العيب الرئيسي: لا يمكنك تمرير إغلاق يلتقط متغيرات. الكود التالي لن يتم ترجمته:

\n\n
fn main() {\n    let y = 10;\n    // الإغلاق يلتقط y\n    let closure = |x: i32| -> i32 { x + y };\n    // خطأ! expected fn item, found closure\n    // let result = apply_twice(closure, 5);\n}
\n\n

لهذا تحتاج إلى traits Fn و FnMut و FnOnce.

\n\n

الطريقة 2: الإغلاقات و traits Fn و FnMut و FnOnce

\n

Rust لغة نظامية، ويجب أن تعرف ما يحدث للذاكرة. يمكن للإغلاقات التقاط المتغيرات بثلاث طرق:

\n
    \n
  • بالمرجع (&T) — trait Fn. يمكن استدعاؤها أي عدد من المرات دون تغيير البيانات الملتقطة.
  • \n
  • بالمرجع القابل للتغيير (&mut T) — trait FnMut. يمكن استدعاؤها عدة مرات، لكن البيانات قد تتغير.
  • \n
  • بالقيمة (T) — trait FnOnce. يمكن استدعاؤها مرة واحدة فقط، وبعد ذلك يتم "استهلاك" الإغلاق.
  • \n
\n\n

مثال مع Fn

\n
fn apply_twice_fn<F>(f: F, value: i32) -> i32\nwhere\n    F: Fn(i32) -> i32,\n{\n    f(f(value))\n}\n\nfn main() {\n    let y = 10;\n    let closure = |x| x + y; // يلتقط y بالمرجع\n    let result = apply_twice_fn(closure, 5);\n    println!("Fn: {}", result); // 25\n}
\n\n

مثال مع FnMut

\n
fn apply_twice_mut<F>(mut f: F, value: i32) -> i32\nwhere\n    F: FnMut(i32) -> i32,\n{\n    f(f(value))\n}\n\nfn main() {\n    let mut counter = 0;\n    let mut closure = |x: i32| -> i32 {\n        counter += 1;\n        x + counter\n    };\n    let result = apply_twice_mut(closure, 5);\n    println!("FnMut: {}", result); // 8 (5+1 ثم 6+2)\n}
\n\n

مثال مع FnOnce

\n
fn apply_once<F>(f: F, value: i32) -> i32\nwhere\n    F: FnOnce(i32) -> i32,\n{\n    f(value) // يمكن استدعاؤها مرة واحدة فقط\n}\n\nfn main() {\n    let name = String::from("Rust");\n    let closure = |x: i32| -> i32 {\n        println!("مرحبًا {}!", name);\n        x * 2\n    };\n    let result = apply_once(closure, 10);\n    println!("FnOnce: {}", result); // 20\n    // لا يمكن استدعاء closure مرة أخرى هنا\n}
\n\n

الطريقة 3: استخدام traits مخصصة

\n

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

\n\n
trait Operation {\n    fn apply(&self, x: i32) -> i32;\n}\n\nstruct AddOne;\nimpl Operation for AddOne {\n    fn apply(&self, x: i32) -> i32 {\n        x + 1\n    }\n}\n\nstruct MultiplyBy {\n    factor: i32,\n}\nimpl Operation for MultiplyBy {\n    fn apply(&self, x: i32) -> i32 {\n        x * self.factor\n    }\n}\n\nfn apply_twice_trait(op: &dyn Operation, value: i32) -> i32 {\n    op.apply(op.apply(value))\n}\n\nfn main() {\n    let add = AddOne;\n    let multiply = MultiplyBy { factor: 3 };\n    println!("Trait Add: {}", apply_twice_trait(&add, 5)); // 7\n    println!("Trait Multiply: {}", apply_twice_trait(&multiply, 5)); // 45\n}
\n\n

متى تستخدم كل طريقة؟

\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
الطريقةمتى تستخدممثال
fn (مؤشر دالة)دوال بسيطة بدون التقاط متغيرات، أداء عاليتمرير دالة رياضية مثل sin أو cos
Fn / FnMut / FnOnceإغلاقات تلتقط متغيرات من السياقمعالجات الأحداث، دوال الفرز المخصصة
Trait مخصصسلوك معقد مع طرق متعددة أو حالة داخليةأنماط الإستراتيجية، محاكاة الكائنات
\n\n

نصائح للمبتدئين

\n
    \n
  • ابدأ بـ fn إذا كانت دالتك لا تلتقط أي شيء.
  • \n
  • استخدم Fn كمعامل عام (generic parameter) للمرونة القصوى.
  • \n
  • تذكر أن fn ينفذ جميع traits Fn و FnMut و FnOnce، لذا يمكنك تمرير مؤشر دالة حيثما يتوقع أحد هذه traits.
  • \n
  • لا تتردد في استخدام Box<dyn Fn> إذا كنت بحاجة إلى تخزين الإغلاق في هيكل بيانات.
  • \n
\n\n

الخلاصة

\n

تمرير الدوال كوسائط في Rust ليس معقدًا كما يبدو. بفهمك للفرق بين fn و Fn والـ traits، ستتمكن من كتابة كود أكثر مرونة وقوة. تذكر أن Rust يعطيك تحكمًا دقيقًا في الأداء والذاكرة، مقابل القليل من الجهد الإضافي في كتابة الأنواع.

\n\n

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

المدونات

توصيات الكتب