«استخدام القيمة المنقولة» في Rust: لماذا يحدث وكيف تتعايش معه
\n\nإذا كنت قد بدأت للتو في تعلم Rust، فمن المحتمل أنك واجهت بالفعل الرسالة الغامضة: error[E0382]: use of moved value. المترجم يوبخ، وأنت تحك رأسك، والكود الذي هو منطقيًا صحيح تمامًا يرفض التجميع. هل هذا مألوف؟ مرحبًا بك في عالم الملكية والاقتراض — المفاهيم الأساسية في Rust التي تجعلها آمنة، ولكنها أحيانًا تحير حتى المطورين ذوي الخبرة.
هذه المقالة ليست وثائق جافة، بل تحليل حي للمشكلة. سننظر في سبب ظهور خطأ use of moved value، من خلال أمثلة كود حقيقية، والأهم من ذلك، ماذا نفعل حيال ذلك. ستتعلم كيف تفكر مثل borrow checker وستتوقف عن محاربته. هيا بنا!
\n\nما هو «النقل» في Rust ولماذا المترجم صارم جدًا؟
\n\nفي Rust، لكل قيمة مالك واحد بالضبط. عندما تعين قيمة لمتغير آخر أو تمررها إلى دالة، فإن الملكية تُنقل. بعد النقل، يصبح المتغير الأصلي غير صالح — يمنعك المترجم من الوصول إليه. هذا هو جذر خطأ use of moved value.
\n\nانظر إلى أبسط مثال:
\n\nlet s1 = String::from("مرحبًا");\nlet s2 = s1; // تنتقل الملكية من s1 إلى s2\nprintln!("{}", s1); // خطأ: use of moved value: `s1`\n\nلماذا؟ String ليست نسخة بسيطة في المكدس. إنها تخزن البيانات في الكومة. إذا قام Rust بنسخ المؤشر فقط، فعند تحرير الذاكرة، سيحاول كلا المتغيرين حذفها — double free. بدلاً من ذلك، يقوم Rust «بنقل» الملكية: الآن s2 مسؤول عن الذاكرة، و s1 لم يعد موجودًا من وجهة نظر المترجم. هذه هي الفكرة الأساسية لـ دلالات النقل في Rust.
السيناريوهات النموذجية المؤدية إلى الخطأ
\n\nيظهر خطأ use of moved value غالبًا في ثلاث حالات:
- \n
- تمرير القيمة إلى دالة \n
- تعيين القيمة لمتغير آخر \n
- استخدام القيمة في حلقة أو مكرر \n
دعنا ننظر في كل حالة بالتفصيل.
\n\n1. التمرير إلى دالة دون إرجاع
\n\nfn print_string(s: String) {\n println!("{}", s);\n}\n\nfn main() {\n let my_string = String::from("Rust");\n print_string(my_string); // تم نقل الملكية\n println!("{}", my_string); // خطأ! use of moved value\n}\n\nبعد استدعاء print_string، انتقلت ملكية my_string إلى الدالة. عندما تنتهي الدالة، يتم حذف s، وتصبح my_string غير صالحة. يحميك المترجم من استخدام كائن ميت.
2. التعيين دون نسخ
\n\nلقد رأينا هذا المثال مع String. لكن المهم: ليست كل الأنواع تتصرف هكذا. الأنواع البسيطة مثل i32 و bool و f64 تطبق الخاصية Copy. لا يتم نقلها، بل يتم نسخها تلقائيًا.
let x = 5;\nlet y = x; // نسخة، وليس نقل\nprintln!("{}", x); // يعمل!\n\nالأنواع التي تخزن البيانات في الكومة (String، Vec، Box) لا تطبق Copy — فقط Clone. لذلك يتم نقلها.
3. التكرار على مجموعة
\n\nمصدر آخر شائع للأخطاء هو حلقات for:
let v = vec![1, 2, 3];\nfor x in v { // يتم نقل v إلى المكرر\n println!("{}", x);\n}\nprintln!("{:?}", v); // خطأ! use of moved value\n\nعندما تكتب for x in v، فأنت في الواقع تستدعي into_iter()، الذي يستهلك المتجه. بعد الحلقة، لم يعد v موجودًا.
كيفية الإصلاح: 4 طرق مجربة
\n\nالآن