لماذا "جامع القمامة" في Rust ليس كما تظن
\n\nإذا كنت قادماً من عالم Python أو Java أو C#، فإن كلمة "جامع القمامة" (garbage collector) تستحضر صورة واضحة: بيئة خلفية تجد وتزيل بشكل دوري الكائنات التي لم يعد أحد يشير إليها. في Rust، الأمر مختلف تماماً. لا يوجد GC تقليدي هنا، ولكن يوجد نظام الملكية والاقتراض الذي يحل نفس المهمة - إدارة دورة حياة الكائنات - في مرحلة الترجمة.
\n\nومع ذلك، فإن الحياة (والكود) أكثر تعقيداً من المخططات المثالية. في بعض الأحيان تحتاج إلى مشاركة ملكية كائن واحد بين أجزاء متعددة من البرنامج، أو إنشاء مراجع دائرية، أو العمل مع بيانات قابلة للتغيير عبر مؤشرات غير قابلة للتغيير. وهنا يأتي دور الكائنات المُدارة بواسطة جامع القمامة في Rust: Rc وArc وWeak وRefCell. دعنا نفهم كيفية إدارة دورة حياتها، وتجنب تسرب الذاكرة، وعدم إرباك عقولنا.
هذه المقالة هي دليل مفصل لأولئك الذين يريدون فهم كيفية عمل إدارة دورة حياة الكائنات في Rust عملياً. لن نقتصر على سرد الأنواع فقط، بل سنعرض سيناريوهات حقيقية، بما في ذلك سؤال شائع من Stack Overflow (نعم، ذلك السؤال الذي حظي بآلاف المشاهدات).
\n\nالمفاهيم الأساسية: الملكية والاقتراض وعداد المراجع
\n\nقبل الغوص في الأعماق، دعنا نتذكر الأساسيات:
\n\n- \n
- الملكية (ownership) - لكل قيمة مالك واحد فقط. عندما يخرج المالك عن النطاق، يتم حذف القيمة. \n
- الاقتراض (borrowing) - يمكنك مؤقتاً "استعارة" مرجع إلى قيمة دون أن تصبح مالكاً. القيود: إما مرجع واحد قابل للتغيير، أو أي عدد من المراجع غير القابلة للتغيير. \n
- عداد المراجع (reference counting) - هذه آلية تسمح بوجود عدة مالكين لقيمة واحدة. في Rust، يتم تنفيذ ذلك عبر
Rc(للبرامج أحادية الخيط) وArc(للبرامج متعددة الخيوط). \n
عندما نقول "الكائنات المُدارة بواسطة جامع القمامة" في سياق Rust، فإننا نعني تحديداً الكائنات التي يديرها عداد المراجع. لا يتم حذفها فوراً بعد الخروج من النطاق، بل تنتظر حتى يصل العداد إلى الصفر. هذا هو إدارة دورة الحياة - أنت تتحكم في وقت تحرير الذاكرة.
\n\nRc وArc: أساس إدارة دورة الحياة
\n\nRc (Reference Counted) هو مؤشر ذكي يسمح بمشاركة ملكية قيمة واحدة بين أجزاء متعددة من الكود في نفس الخيط. في كل مرة تقوم فيها باستنساخ Rc، يزداد العداد. عندما يخرج الاستنساخ عن النطاق، يقل العداد. عندما يصل العداد إلى الصفر، يتم حذف القيمة.
مثال على استخدام بسيط لـ Rc:
use std::rc::Rc;\n\nfn main() {\n let data = Rc::new(vec![1, 2, 3]);\n let a = Rc::clone(&data);\n let b = Rc::clone(&data);\n\n println!("عداد المراجع: {}", Rc::strong_count(&data)); // 3\n\n drop(a);\n println!("بعد حذف a: {}", Rc::strong_count(&data)); // 2\n}\n\n\nهنا، data وa وb هم ثلاثة مالكين لمتجه واحد. طالما أن واحداً منهم على الأقل موجود، يبقى المتجه في الذاكرة. هذا مثالي للحالات التي لا يمكنك فيها تحديد أي وحدة ستنهي عملها أخيراً.
Arc (Atomic Reference Counted) يعمل بنفس الطريقة، لكنه يستخدم عمليات ذرية، مما يجعله آمناً للبرامج متعددة الخيوط. الثمن هو انخفاض طفيف في الأداء.
use std::sync::Arc;\nuse std::thread;\n\nfn main() {\n let data = Arc::new(vec![1, 2, 3]);\n let mut handles = vec![];\n\n for _ in 0..3 {\n let data_clone = Arc::clone(&data);\n handles.push(thread::spawn(move || {\n println!("{:?}", data_clone);\n }));\n }\n\n for handle in handles {\n handle.join().unwrap();\n }\n}\n\n\n