متجه الأصفار في Rust: ليس بسيطًا كما يبدو
\n\nقد يبدو الأمر بسيطًا، إنشاء متجه مليء بالأصفار؟ في Python يتم ذلك بسطر واحد: [0] * n. في JavaScript — Array(n).fill(0). ولكن عندما تنتقل إلى Rust، حتى هذه المهمة التافهة تفتح عالمًا كاملاً من البرمجة النظامية، وإدارة الذاكرة، والتكاليف الصفرية للتجريدات.
سؤال "إنشاء متجه من الأصفار بحجم محدد" على Stack Overflow حصد آلاف المشاهدات. وهذا ليس من قبيل الصدفة: خلف هذا الطلب البسيط يكمن فهم عميق لكيفية تعامل Rust مع الذاكرة، والتهيئة، والأمان. اليوم سنلقي نظرة تحت غطاء Rust ونحلل جميع الطرق لإنشاء متجه من الأصفار، والأهم من ذلك، سنفهم ما يحدث حقًا في ذاكرة الوصول العشوائي.
\n\nلماذا هو أكثر تعقيدًا مما في Python؟
\n\nفي Python أو JavaScript، تقوم ببساطة بإنشاء مصفوفة، ويقوم المفسّر بكل العمل القذر نيابة عنك. أما Rust فهي لغة على مستوى النظام. لا تسمح بترك الذاكرة غير مهيأة (إلا إذا كنت تستخدم unsafe). يجب أن تكون كل بايت إما مملوءة بشكل صريح أو مُعلّمة على أنها "سلوك غير محدد".
تخيل: أنت تطلب 100 صندوق فارغ. في Python، سيتم توصيل 100 صندوق فارغ إليك ببساطة. في Rust، سيسألك الساعي: "ماذا تريد أن تضع داخل كل صندوق؟" — وإذا لم تخبره، فلن يأتي ببساطة. هذا يضمن أنك لن تقرأ أبدًا "قمامة" من الذاكرة متبقية من برنامج سابق عن طريق الخطأ.
\n\nالطريقة رقم 1: الكلاسيكية vec![0; size]
\n\nالطريقة الأكثر وضوحًا والمُوصى بها هي استخدام ماكرو vec!:
let zeros: Vec<i32> = vec![0; 10];\nprintln!("{:?}", zeros); // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\n\nقصيرة، أنيقة، وواضحة. ولكن ماذا يحدث تحت الغطاء؟ دعنا نحلل خطوة بخطوة.
\n\nتحت غطاء vec![0; 10]
\n\nعندما تكتب vec![0; 10]، يقوم Rust بما يلي:
- \n
- يخصص ذاكرة على الكومة (heap) لـ 10 عناصر من النوع
i32. كلi32يشغل 4 بايت، بإجمالي 40 بايت. \n - يتحقق من أن النوع يطبق trait
Clone. الأرقامi32تطبقه، لذلك يمكن للمُجمّع نسخ القيمة0لكل عنصر. \n - يشغل حلقة نسخ داخل
alloc::vec::from_elem. يقوم المُجمّع بتحسين هذه الحلقة إلىmemset(إذا كان النوع عدديًا بسيطًا). \n - يعيد
Vecبطول 10 وسعة 10. \n
النقطة الأساسية: الذاكرة لا تبقى غير مهيأة. يضمن Rust أن كل بايت سيتم ملؤه بالأصفار قبل أن تتمكن من الوصول إلى المتجه.
\n\nالطريقة رقم 2: vec![Default::default(); size]
\n\nإذا كان النوع الخاص بك ليس رقمًا، ولكنه يطبق trait Default، يمكنك استخدام نهج عام:
#[derive(Default)]\nstruct Point {\n x: f64,\n y: f64,\n}\n\nlet points: Vec<Point> = vec![Default::default(); 5];\nprintln!("{:?}", points[0]); // Point { x: 0.0, y: 0.0 }\n\nبالنسبة للأنواع البدائية، Default::default() يعطي 0، 0.0، false أو '\\0'. بالنسبة للهياكل المخصصة — ما قمت بتعريفه في #[derive(Default)].
وماذا عن الأداء؟
\n\nدعنا نكتب معيارًا بسيطًا (في الكود الحقيقي استخدم criterion):
use std::time::Instant;\n\nfn main() {\n let n = 1_000_000;\n\n let start = Instant::now();\n let v1 = vec![0u8; n];\n let dur1 = start.elapsed();\n\n let start = Instant::now();\n let v2 = vec![0u64; n];\n let dur2 = start.elapsed();\n\n println!("u8: {:?}", dur1);\n println!("u64: {:?}", dur2);\n}\n\nفي