مقدمة: لماذا تعتبر الاستدعاءات في Rust تحدياً (وفرصة)
\nإذا كنت قد كتبت يوماً بلغة JavaScript أو Python أو C#، فإن الاستدعاءات (callbacks) هي أداة مألوفة لك. تقوم بتمرير دالة سيتم استدعاؤها لاحقاً، وتعيش بسلام. ولكن عندما تنتقل إلى Rust، يتحول هذا النمط البسيط فجأة إلى حقل ألغام: الملكية، الاقتراض، أوقات الحياة، dyn مقابل impl...
على Stack Overflow، جمع سؤال "Idiomatic callbacks in Rust" آلاف المشاهدات. هذا يعني أنه حتى المطورين ذوي الخبرة يتعثرون في هذا الموضوع. لكن الخبر السار: تقدم Rust ليس واحداً، بل عدة طرق أنيقة لتنفيذ الاستدعاءات. واليوم سنقوم بتحليلها جميعاً - من الإغلاقات البسيطة إلى كائنات التريت القوية وأنماط enum.
\nهذه المقالة مخصصة لك إذا كنت:
\n- \n
- بدأت تعلم Rust وتريد فهم كيفية تنظيم البنى غير المتزامنة والحدثية بشكل صحيح؛ \n
- تنتقل من لغات أخرى وتبحث عن نظائر اصطلاحية للأنماط المألوفة؛ \n
- تريد كتابة كود نظيف وآمن وفعال في Rust. \n
هيا بنا!
\n\nالمشكلة: ما الخطأ في الاستدعاءات البسيطة؟
\nتخيل أننا نكتب جدولة مهام بسيطة. في Python، سيبدو الأمر هكذا:
\ndef schedule(callback):\n # نقوم بشيء ما\n callback(result)\nفي Rust، قد تكون المحاولة الأولى كالتالي:
\nfn schedule(callback: fn(i32) -> i32) {\n let result = 42;\n callback(result);\n}\nوهذا يعمل! ولكن فقط مع الدوال، وليس مع الإغلاقات. حاول تمرير إغلاق يلتقط متغيراً:
\nlet factor = 2;\nlet closure = |x| x * factor;\nschedule(closure); // خطأ! expected fn pointer, found closure\nيشتكي المترجم بحق: fn هو مؤشر لدالة، بينما الإغلاق هو نوع مجهول يمكنه التقاط البيئة. نحتاج إلى نهج أكثر مرونة.
النهج 1: الإغلاقات مع impl Fn (البساطة والمرونة)
\nالطريقة الأبسط والأكثر استخداماً هي استخدام نوع معمم مع التريت Fn أو FnMut أو FnOnce.
متى تستخدم Fn و FnMut و FnOnce؟
\n- \n
FnOnce- إذا تم استدعاء الاستدعاء مرة واحدة بالضبط (يستهلك القيم الملتقطة)؛ \n FnMut- إذا كان الاستدعاء يمكنه تعديل الحالة الملتقطة (يتم استدعاؤه عدة مرات)؛ \n Fn- إذا كان الاستدعاء لا يغير الحالة (دالة نقية). \n
مثال مع impl Fn:
fn schedule<F>(callback: F)\nwhere\n F: Fn(i32) -> i32,\n{\n let result = 42;\n println!("النتيجة: {}", callback(result));\n}\n\nfn main() {\n let factor = 2;\n let closure = |x| x * factor;\n schedule(closure); // يعمل!\n \n // يمكن تمرير دالة عادية أيضاً\n fn double(x: i32) -> i32 { x * 2 }\n schedule(double);\n}\nالإيجابيات: مرنة للغاية، تدعم الإغلاقات، آمنة، قابلة للقراءة.
\nالسلبيات: التماثل الشكلي - لكل نوع إغلاق، يولد المترجم نسخة منفصلة من الدالة. إذا كان هناك العديد من الاستدعاءات، قد ينتفخ الكود.
النهج 2: كائنات التريت Box<dyn Fn> (التوزيع الديناميكي)
\nإذا كنت تريد تخزين الاستدعاءات في مجموعات (مثل Vec) أو تمريرها بين الخيوط، فإن التعميمات لن تفي بالغرض - فأنت بحاجة إلى نوع واحد. هنا تأتي كائنات التريت للإنقاذ:
type Callback = Box<dyn Fn(i32) -> i32>;\n\nstruct Scheduler {\n callbacks: Vec<Callback>,\n}\n\nimpl Scheduler {\n fn new() -> Self {\n Scheduler { callbacks: Vec::new() }\n }\n \n fn register(&mut self, callback