استدعاءات في Rust: من الألم إلى الأناقة من خلال الأنماط الاصطلاحية

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

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

ابدأ الدورة

مقدمة: لماذا تعتبر الاستدعاءات في Rust تحدياً (وفرصة)

\n

إذا كنت قد كتبت يوماً بلغة JavaScript أو Python أو C#، فإن الاستدعاءات (callbacks) هي أداة مألوفة لك. تقوم بتمرير دالة سيتم استدعاؤها لاحقاً، وتعيش بسلام. ولكن عندما تنتقل إلى Rust، يتحول هذا النمط البسيط فجأة إلى حقل ألغام: الملكية، الاقتراض، أوقات الحياة، dyn مقابل impl...

\n

على Stack Overflow، جمع سؤال "Idiomatic callbacks in Rust" آلاف المشاهدات. هذا يعني أنه حتى المطورين ذوي الخبرة يتعثرون في هذا الموضوع. لكن الخبر السار: تقدم Rust ليس واحداً، بل عدة طرق أنيقة لتنفيذ الاستدعاءات. واليوم سنقوم بتحليلها جميعاً - من الإغلاقات البسيطة إلى كائنات التريت القوية وأنماط enum.

\n

هذه المقالة مخصصة لك إذا كنت:

\n
    \n
  • بدأت تعلم Rust وتريد فهم كيفية تنظيم البنى غير المتزامنة والحدثية بشكل صحيح؛
  • \n
  • تنتقل من لغات أخرى وتبحث عن نظائر اصطلاحية للأنماط المألوفة؛
  • \n
  • تريد كتابة كود نظيف وآمن وفعال في Rust.
  • \n
\n

هيا بنا!

\n\n

المشكلة: ما الخطأ في الاستدعاءات البسيطة؟

\n

تخيل أننا نكتب جدولة مهام بسيطة. في Python، سيبدو الأمر هكذا:

\n
def schedule(callback):\n    # نقوم بشيء ما\n    callback(result)
\n

في Rust، قد تكون المحاولة الأولى كالتالي:

\n
fn schedule(callback: fn(i32) -> i32) {\n    let result = 42;\n    callback(result);\n}
\n

وهذا يعمل! ولكن فقط مع الدوال، وليس مع الإغلاقات. حاول تمرير إغلاق يلتقط متغيراً:

\n
let factor = 2;\nlet closure = |x| x * factor;\nschedule(closure); // خطأ! expected fn pointer, found closure
\n

يشتكي المترجم بحق: fn هو مؤشر لدالة، بينما الإغلاق هو نوع مجهول يمكنه التقاط البيئة. نحتاج إلى نهج أكثر مرونة.

\n\n

النهج 1: الإغلاقات مع impl Fn (البساطة والمرونة)

\n

الطريقة الأبسط والأكثر استخداماً هي استخدام نوع معمم مع التريت Fn أو FnMut أو FnOnce.

\n

متى تستخدم Fn و FnMut و FnOnce؟

\n
    \n
  • FnOnce - إذا تم استدعاء الاستدعاء مرة واحدة بالضبط (يستهلك القيم الملتقطة)؛
  • \n
  • FnMut - إذا كان الاستدعاء يمكنه تعديل الحالة الملتقطة (يتم استدعاؤه عدة مرات)؛
  • \n
  • Fn - إذا كان الاستدعاء لا يغير الحالة (دالة نقية).
  • \n
\n

مثال مع impl Fn:

\n
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السلبيات: التماثل الشكلي - لكل نوع إغلاق، يولد المترجم نسخة منفصلة من الدالة. إذا كان هناك العديد من الاستدعاءات، قد ينتفخ الكود.

\n\n

النهج 2: كائنات التريت Box<dyn Fn> (التوزيع الديناميكي)

\n

إذا كنت تريد تخزين الاستدعاءات في مجموعات (مثل Vec) أو تمريرها بين الخيوط، فإن التعميمات لن تفي بالغرض - فأنت بحاجة إلى نوع واحد. هنا تأتي كائنات التريت للإنقاذ:

\n
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                

المدونات

توصيات الكتب