إنشاء متجه من الأصفار في Rust: السحر تحت الغطاء

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

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

ابدأ الدورة

متجه الأصفار في Rust: ليس بسيطًا كما يبدو

\n\n

قد يبدو الأمر بسيطًا، إنشاء متجه مليء بالأصفار؟ في Python يتم ذلك بسطر واحد: [0] * n. في JavaScript — Array(n).fill(0). ولكن عندما تنتقل إلى Rust، حتى هذه المهمة التافهة تفتح عالمًا كاملاً من البرمجة النظامية، وإدارة الذاكرة، والتكاليف الصفرية للتجريدات.

\n\n

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

\n\n

لماذا هو أكثر تعقيدًا مما في Python؟

\n\n

في Python أو JavaScript، تقوم ببساطة بإنشاء مصفوفة، ويقوم المفسّر بكل العمل القذر نيابة عنك. أما Rust فهي لغة على مستوى النظام. لا تسمح بترك الذاكرة غير مهيأة (إلا إذا كنت تستخدم unsafe). يجب أن تكون كل بايت إما مملوءة بشكل صريح أو مُعلّمة على أنها "سلوك غير محدد".

\n\n

تخيل: أنت تطلب 100 صندوق فارغ. في Python، سيتم توصيل 100 صندوق فارغ إليك ببساطة. في Rust، سيسألك الساعي: "ماذا تريد أن تضع داخل كل صندوق؟" — وإذا لم تخبره، فلن يأتي ببساطة. هذا يضمن أنك لن تقرأ أبدًا "قمامة" من الذاكرة متبقية من برنامج سابق عن طريق الخطأ.

\n\n

الطريقة رقم 1: الكلاسيكية vec![0; size]

\n\n

الطريقة الأكثر وضوحًا والمُوصى بها هي استخدام ماكرو vec!:

\n\n
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\n
    \n
  1. يخصص ذاكرة على الكومة (heap) لـ 10 عناصر من النوع i32. كل i32 يشغل 4 بايت، بإجمالي 40 بايت.
  2. \n
  3. يتحقق من أن النوع يطبق trait Clone. الأرقام i32 تطبقه، لذلك يمكن للمُجمّع نسخ القيمة 0 لكل عنصر.
  4. \n
  5. يشغل حلقة نسخ داخل alloc::vec::from_elem. يقوم المُجمّع بتحسين هذه الحلقة إلى memset (إذا كان النوع عدديًا بسيطًا).
  6. \n
  7. يعيد Vec بطول 10 وسعة 10.
  8. \n
\n\n

النقطة الأساسية: الذاكرة لا تبقى غير مهيأة. يضمن Rust أن كل بايت سيتم ملؤه بالأصفار قبل أن تتمكن من الوصول إلى المتجه.

\n\n

الطريقة رقم 2: vec![Default::default(); size]

\n\n

إذا كان النوع الخاص بك ليس رقمًا، ولكنه يطبق trait Default، يمكنك استخدام نهج عام:

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

وماذا عن الأداء؟

\n\n

دعنا نكتب معيارًا بسيطًا (في الكود الحقيقي استخدم criterion):

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

في

المدونات

توصيات الكتب