الإغلاقات في Rust: سحر أم رياضيات صارمة؟
\n\nإذا كنت قد كتبت في Rust شيئاً مثل |x| x + 1، فقد استخدمت الإغلاقات. ولكن بمجرد أن تحاول تمرير هذا الإغلاق إلى دالة أو تخزينه في هيكل، يبدأ المترجم فجأة في المطالبة: «أي trait؟ Fn أم FnMut أم FnOnce؟».
وهنا يقع الكثيرون في حيرة. خاصة إذا كنت قادماً من JavaScript أو Python أو Ruby، حيث الإغلاقات هي «مجرد دوال تلتقط المتغيرات». في Rust الأمر أكثر تعقيداً (وأكثر إثارة للاهتمام).
\n\nاليوم سنفهم ما يحدث بالفعل تحت الغطاء، عندما يقرر Rust أي trait سيعطي لإغلاقك. هذا ليس سحراً – إنه نظام أنواع صارم يحميك من سباقات البيانات والمؤشرات المعلقة. هيا بنا!
\n\nباختصار عن المهم: traits الإغلاقات الثلاثة
\n\nفي Rust هناك ثلاثة traits تنفذها الإغلاقات:
\n\n- \n
FnOnce– يمكن استدعاؤه مرة واحدة. يأخذ ملكية المتغيرات الملتقطة. \n FnMut– يمكن استدعاؤه عدة مرات، لكن يمكنه تعديل المتغيرات الملتقطة (يأخذ&mut). \n Fn– يمكن استدعاؤه عدة مرات ولا يعدل المتغيرات الملتقطة (يأخذ&). \n
كل إغلاق ينفذ تلقائياً جميع traits المناسبة. أي إذا كان الإغلاق ينفذ Fn، فإنه ينفذ تلقائياً FnMut و FnOnce. لكن العكس لا يعمل.
لماذا؟ لأن Fn هو الأكثر صرامة، و FnOnce هو الأضعف. هرمية الوراثة تبدو هكذا:
FnOnce ← FnMut ← Fn\n\nأي أن FnMut: FnOnce، و Fn: FnMut. احفظ هذا السلم – إنه أساسي.
كيف يقرر Rust أي trait يعطي للإغلاق؟
\n\nينظر المترجم إلى كيفية استخدام الإغلاق للمتغيرات الملتقطة. هناك ثلاثة أوضاع للالتقاط:
\n\n- \n
- بالمرجع (
&T) – الإغلاق يقرأ المتغير فقط. \n - بالمرجع القابل للتعديل (
&mut T) – الإغلاق يعدل المتغير. \n - بالقيمة (
T) – الإغلاق يأخذ ملكية المتغير. \n
إليك كيف يؤثر ذلك على traits:
\n\n| طريقة الالتقاط | Traits المنفذة | مثال |
|---|---|---|
فقط &T | Fn + FnMut + FnOnce | || println!("{}", x) |
&mut T (وليس T) | FnMut + FnOnce | || x += 1 |
T (ملكية) | فقط FnOnce | || drop(x) أو move || x |
دعنا نحلل كل حالة بكود حي.
\n\nمثال 1: قراءة فقط → Fn
\n\nlet x = 42;\nlet read_closure = || println!("x = {}", x);\n\n// يمكن استدعاؤه أي عدد من المرات\nread_closure();\nread_closure();\n\n// وحتى تمريره إلى دالة تتطلب Fn\nfn call_fn<F: Fn()>(f: F) {\n f();\n}\ncall_fn(read_closure);\n\nهنا الإغلاق يقرأ x فقط. لا يحتاج لتعديله أو أخذه. Rust يفهم: «حسناً، هذا آمن – نعطيه Fn».
مثال 2: تعديل → FnMut
\n\nlet mut x = 42;\nlet mut mut_closure = || {\n x += 1;\n println!("x = {}", x);\n};\n\nmut_closure(); // x = 43\nmut_closure(); // x = 44\n\n// نمرره إلى دالة تتطلب FnMut\nfn call_fn_mut<F: FnMut()>(mut f: F) {\n f();\n f();\n}\ncall_fn_mut(mut_closure);\n\nلاحظ: أعلنا mut_closure كـ let mut. لماذا؟ لأن استدعاء FnMut يتطلب قابلية للتعديل