الدالة كوسيط في 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
- كتابة كود موجز ومعبر دون الكثير من if-else. \n
في Rust، على عكس اللغات الديناميكية، يتحقق المترجم بدقة من الأنواع وأوقات الحياة. لذلك عليك أن تفهم بوضوح ما تمرره: مجرد عنوان دالة، إغلاق مع التقاط سياق، أو شيء أكثر تعقيدًا.
\n\nالطريقة 1: مؤشر الدالة (Function Pointer) — fn
\nأبسط وأكثر طريقة مباشرة. إذا كانت الدالة لا تلتقط متغيرات من البيئة الخارجية (فهي "نقية")، يمكنك استخدام النوع fn. لاحظ: fn هو نوع، بينما Fn و FnMut و FnOnce هي traits. لا تخلط بينهم!
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\nfn 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.
الطريقة 2: الإغلاقات و traits Fn و FnMut و FnOnce
\nRust لغة نظامية، ويجب أن تعرف ما يحدث للذاكرة. يمكن للإغلاقات التقاط المتغيرات بثلاث طرق:
\n- \n
- بالمرجع (&T) — trait
Fn. يمكن استدعاؤها أي عدد من المرات دون تغيير البيانات الملتقطة. \n - بالمرجع القابل للتغيير (&mut T) — trait
FnMut. يمكن استدعاؤها عدة مرات، لكن البيانات قد تتغير. \n - بالقيمة (T) — trait
FnOnce. يمكن استدعاؤها مرة واحدة فقط، وبعد ذلك يتم "استهلاك" الإغلاق. \n
مثال مع Fn
\nfn 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
\nfn 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
\nfn 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\ntrait 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
|---|---|---|
fn (مؤشر دالة) | \n دوال بسيطة بدون التقاط متغيرات، أداء عالي | \nتمرير دالة رياضية مثل sin أو cos | \n
Fn / FnMut / FnOnce | \n إغلاقات تلتقط متغيرات من السياق | \nمعالجات الأحداث، دوال الفرز المخصصة | \n
| Trait مخصص | \nسلوك معقد مع طرق متعددة أو حالة داخلية | \nأنماط الإستراتيجية، محاكاة الكائنات | \n
نصائح للمبتدئين
\n- \n
- ابدأ بـ
fnإذا كانت دالتك لا تلتقط أي شيء. \n - استخدم
Fnكمعامل عام (generic parameter) للمرونة القصوى. \n - تذكر أن
fnينفذ جميع traitsFnوFnMutوFnOnce، لذا يمكنك تمرير مؤشر دالة حيثما يتوقع أحد هذه traits. \n - لا تتردد في استخدام
Box<dyn Fn>إذا كنت بحاجة إلى تخزين الإغلاق في هيكل بيانات. \n
الخلاصة
\nتمرير الدوال كوسائط في Rust ليس معقدًا كما يبدو. بفهمك للفرق بين fn و Fn والـ traits، ستتمكن من كتابة كود أكثر مرونة وقوة. تذكر أن Rust يعطيك تحكمًا دقيقًا في الأداء والذاكرة، مقابل القليل من الجهد الإضافي في كتابة الأنواع.
جرب الأمثلة بنفسك، وسترى كم هي أنيقة وقوية! إذا كان لديك أي أسئلة، اترك تعليقًا أدناه.