سننظر أولًا إلى نوع التجميعة Vec<T>
، المعروف أيضًا باسم الشعاع vector، إذ تسمح لك الأشعة بتخزين أكثر من قيمة واحدة في هيكل بيانات واحد يضع القيم على نحوٍ متتالي في الذاكرة، ويمكن أن تخزن الأشعة قيمًا من النوع ذاته، وهي مفيدةٌ عندما يكون لديك لائحةٌ من العناصر، مثل سطور نصية ضمن ملف، أو أسعار منتجات في سلة تسوّق.
إنشاء شعاع جديد
لإنشاء شعاع جديد فارغ نستدعي الدالة Vec::new
كما هو موضح في الشيفرة 1.
let v: Vec<i32> = Vec::new();
الشيفرة 1: إنشاء شعاع جديد فارغ لتخزين قيم من النوع i32
لاحظ أننا كتبنا ما يشير إلى نوع البيانات، لأننا لا نستطيع إدخال أي نوع نريده في الشعاع، ويجب على رست أن تعلم أي نوع من البيانات نريد أن ندخله إلى الشعاع، وهذه النقطة مهمة جدًا. بُنيَت الأشعة باستخدام الأنواع المعمّاة generics وسنغطّي استخدام الأنواع المعمّاة مع أنواعك الخاصة لاحقًا، ويكفي حاليًا أن تعرف أن النوع Vec<T>
الموجود في المكتبة القياسية يستطيع تخزين أي نوع داخله. يمكننا تحديد النوع الذي يحمله الشعاع عند إنشائه دون استخدام الأقواس المثلثة angle brackets. أخبرنا راست في الشيفرة 1 أن Vec<T>
في v
يحمل عناصر من النوع i32
.
ستُنشئ معظم الأحيان شعاع Vec<T>
يحتوي على قيم ابتدائية ويستنتج رست نوع البيانات التي تريد أن تخزنها داخل الشعاع من القيم الابتدائية، لذا يُعد استخدام الطريقة السابقة نادرًا. توفر لنا رست الماكرو vec!
الذي يُنشئ شعاعًا جديدًا يحتوي على القيم التي تمررها له، وتوضح الشيفرة 2 ذلك بإنشاء شعاع من النوع Vec<i32>
يحتوي على القيم 1 و2 و3، والنوع هو i32
لأنه النوع الافتراضي للأعداد الصحيحة كما ناقشنا سابقًا في مقال أنواع البيانات.
let v = vec![1, 2, 3];
الشيفرة 2: إنشاء شعاع جديد يحتوي على قيم
تستطيع رست استنتاج أن نوع الشعاع v
هو Vec<i32>
، لأننا أعطينا قيمًا ابتدائية من النوع i32
، وكتابة النوع مباشرةً ليس ضروريًا هنا. الآن دعنا ننظر إلى كيفية التعديل على شعاع.
تحديث شعاع
يمكننا استخدام التابع push
لإضافة عناصر إلى شعاع بعد إنشائه كما هو موضح في الشيفرة 3.
let mut v = Vec::new(); v.push(5); v.push(6); v.push(7); v.push(8);
الشيفرة 3: استخدام التابع push لإضافة قيم إلى شعاع
إذا أردنا تغيير القيم، علينا أن نجعل الشعاع قابلًا للتعديل -كما هو الحال مع أي متغير اعتيادي- باستخدام الكلمة المفتاحية mut
كما ناقشنا سابقًا. جميع الأرقام التي وضعناها في الشعاع هي من النوع i32
، وتستنتج رست ذلك من البيانات، لذلك ليس من الضروري تحديد النوع بكتابة Vec<i32>
.
قراءة العناصر من الأشعة
هناك طريقتان للإشارة إلى قيمة موجودة في الشعاع: إما باستخدام الدليل index أو باستخدام التابع get
، نوضح في الأمثلة التالية أنواع البيانات التي تُعيدها الدوال بهدف جعلها واضحةً قدر الإمكان.
توضح الشيفرة 4 كلا الطريقتين في الوصول إلى قيمة ضمن شعاع، وذلك باستخدام دليل العنصر أو التابع get
.
let v = vec![1, 2, 3, 4, 5]; let third: &i32 = &v[2]; println!("The third element is {}", third); let third: Option<&i32> = v.get(2); match third { Some(third) => println!("The third element is {}", third), None => println!("There is no third element."), }
الشيفرة 4: استخدام دليل العنصر أو التابع get للحصول على عنصر ضمن الشعاع
لاحظ بعض التفاصيل المهمة هنا؛ إذ استخدمنا قيمة الدليل "2" للحصول على العنصر الثالث وذلك لأن العناصر في الشعاع تحمل دليل عددي يبدأ من الصفر، كما يعطي استخدام &
و[]
مرجعًا إلى العنصر الموجود في الدليل الذي حددناه. عندما نستخدم التابع get
مع تمرير الدليل مثل وسيط argument نحصل على Option<&T>
الذي يمكننا استخدامه مع البنية match
.
توفّر رست طريقتين مختلفتين ليتسنى لك اختيار تصرف برنامجك عندما تحاول استخدام قيمة دليل خارج نطاق العناصر الموجودة في الشعاع، على سبيل المثال دعنا ننظر إلى ما يحدث عندما يكون لدينا شعاع من خمسة عناصر ونحاول الوصول إلى العنصر ذو الدليل 100 باستخدام كلا الطريقتين كما هو موضح في الشيفرة 5.
let v = vec![1, 2, 3, 4, 5]; let does_not_exist = &v[100]; let does_not_exist = v.get(100);
الشيفرة 5: محاولة الوصول إلى الدليل 100 في شعاع يحتوي على خمسة عناصر فقط
ستتسبب الطريقة الأولى []
بهلع panic البرنامج عند تشغيل الشيفرة البرمجية السابقة، لأنها تحاول لإشارة إلى عنصر غير موجود، وهذه الطريقة هي الأفضل في حال أردت لبرنامجك أن يتوقف عن العمل في حال محاولتك الوصول إلى عنصر يقع بعد نهاية الشعاع.
عندما نمرر إلى التابع get
دليلًا يقع خارج المصفوفة فهو يُعيد القيمة None
دون الهلع، ويجب أن تستخدم هذه الطريقة إذا كانت محاولة الوصول إلى عنصر يقع خارج نطاق الشعاع ممكنة الحدوث تحت الظروف الاعتيادية، ويجب على برنامجك عندها أن يتعامل مع Some(&element)
أو None
كما ناقشنا سابقًا. على سبيل المثال، قد يكون الدليل مُدخلًا من قِبل المستخدم وبالتالي إذا أدخل المستخدم رقمًا أكبر من حجم الشعاع عن طريق الخطأ نحصل على القيمة None
ويمكنك إخبار المستخدم عندها أن الرقم الذي أدخله كبير ويمكن إعلامه أيضًا بحجم الشعاع الحالي وإعادة طلب إدخال القيمة منه، وهذه وسيلة عملية أكثر من جعل البرنامج يتوقف بالكامل بسبب خطأ كتابي بسيط
عندما نحصل على مرجع صالح، يتأكد مدقق الاستعارة borrow checker من أن قوانين الملكية ownership والاستعارة borrowing محققة (ناقشنا هذه القوانين سابقًا) للتأكد من أن المرجع وأي مراجع أخرى لمحتوى الشعاع هي مراجع صالحة. تذكر القاعدة التي تنص على أنه لا يمكنك الحصول على مرجع قابل للتعديل ومرجع آخر غير قابل للتعديل في النطاق ذاته، وتنطبق هذه القاعدة على الشيفرة 6 عندما نخزن مرجعًا غير قابلٍ للتعديل للعنصر الأول في الشعاع ومن ثم نجرّب إضافة عنصر إلى نهاية الشعاع، ولن يعمل هذا البرنامج إذا أردنا الإشارة إلى ذلك العنصر لاحقًا ضمن الدالة ذاتها:
let mut v = vec![1, 2, 3, 4, 5]; let first = &v[0]; v.push(6); println!("The first element is: {}", first);
الشيفرة 6: محاولة إضافة عنصر إلى شعاع مع تخزين مرجع إلى عنصر داخل الشعاع في الوقت ذاته
سيتسبب تصريف الشيفرة البرمجية السابقة بالخطأ التالي:
$ cargo run Compiling collections v0.1.0 (file:///projects/collections) error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable --> src/main.rs:6:5 | 4 | let first = &v[0]; | - immutable borrow occurs here 5 | 6 | v.push(6); | ^^^^^^^^^ mutable borrow occurs here 7 | 8 | println!("The first element is: {}", first); | ----- immutable borrow later used here For more information about this error, try `rustc --explain E0502`. error: could not compile `collections` due to previous error
قد تبدو الشيفرة 6 صالحة ويجب أن تعمل. لماذا يتعلق مرجع العنصر الأول بالتغييرات الحاصلة بنهاية الشعاع؟ لنفهم الخطأ يجب أن نفهم كيف تعمل الأشعة، إذ تضع الأشعة القيم بصورةٍ متتالية في الذاكرة وبالتالي يتطلب إضافة عنصر جديد إلى نهاية الشعاع حجز ذاكرة جديدة ونسخ العناصر القديمة في الشعاع إلى مساحة جديدة إذا لم يكن هناك مساحةٌ كافية لوضع جميع العناصر في الشعاع على نحوٍ متتالي، وسيشير مرجع العنصر الأول في تلك الحالة إلى ذاكرة مُحرَّرة deallocated، وتمنع قوانين الاستعارة البرامج من التسبب بهذا النوع من الأخطاء قبل حدوثها.
ملاحظة: للمزيد من التفاصيل حول النوع Vec<T>
ألقِ نظرةً على إلى مثال عن تنفيذ Vec Rustonomicon.
الوصول إلى قيم شعاع متعاقبة
للوصول إلى عناصر الشعاع بصورةٍ متعاقبة، نمرّ بجميع العناصر الموجودة دون الحاجة لاستخدام دليل كل عنصر في كل مرة، وتوضح الشيفرة 7 كيفية استخدام الحلقة for
للحصول على مراجع غير قابلة للتعديل لعناصر الشعاع من النوع i32
وطباعتها.
let v = vec![100, 32, 57]; for i in &v { println!("{}", i); }
الشيفرة 7: طباعة كل عنصر في شعاع عن طريق المرور على العناصر باستخدام حلقة for
يمكننا أيضًا المرور على مراجع قابلة للتعديل mutable لكل عنصر في شعاع قابل للتعديل، وذلك بهدف إجراء تغييرات على جميع العناصر. تجمع الحلقة for
في الشيفرة 8 القيمة 50 إلى كل عنصر في الشعاع.
let mut v = vec![100, 32, 57]; for i in &mut v { *i += 50; }
الشيفرة 8: المرور على مراجع قابلة للتعديل لعناصر في شعاع
لتغيير قيمة العنصر الذي يشير المرجع القابل للتعديل إليه نستخدم عامل التحصيل dereference *
للحصول على القيمة الموجودة في i
قبل استخدام العامل =+
، وسنناقش عامل التحصيل بتوسع أكبر لاحقًا.
المرور على عناصر الشعاع عملية آمنة، سواءً كان ذلك الشعاع قابلًا للتعديل أو غير قابل للتعديل وذلك بسبب قوانين مدقق الاستعارة، فإذا حاولنا إدخال أو إزالة العناصر ضمن الحلقة for
في الشيفرة 7 أو الشيفرة 8، فسنحصل على خطأ تصريفي مشابه للخطأ الذي حصلنا عليه عند تصريفنا للشيفرة 6، إذ يمنع المرجع الذي يشير إلى الشعاع ضمن الحلقة for
أي تعديلات متزامنة لكامل الشعاع.
استخدام تعداد لتخزين عدة أنواع
يمكن للأشعة أن تخزن قيمًا من النوع ذاته فقط، وقد يشكّل هذا عقبةً صغيرةً، إذ أن هناك الكثير من الاستخدامات التي نريد فيها تخزين لائحة من عناصر من أنواع مختلفة، ولحسن الحظ المتغايرات variants الخاصة بالتعداد enum معرفة تحت نوع التعداد ذاته وبالتالي يمكننا استخدام نوع واحد لتمثيل عناصر من أنواع مختلفة بتعريف واستخدام تعداد.
على سبيل المثال، لنفرض أننا بحاجة للحصول على قيم من صف ضمن جدول، ويحتوي هذا الصف قيمًا من نوع الأعداد الصحيحة وقيم أعداد عشرية floating point وقيم سلاسل نصية strings، حينها يمكننا تعريف تعداد يحتوي على متغايرات يمكنها تخزين أنواع القيم المختلفة وسيُنظر إلى جميع متغايرات التعداد إلى أنها من النوع ذاته الخاص بالتعادد، يمكننا بعد ذلك إنشاء شعاع يخزّن ذلك التعداد، وبالتالي سيخزّن أنواعًا مختلفة، وقد وضحنا هذه العملية في الشيفرة 9.
enum SpreadsheetCell { Int(i32), Float(f64), Text(String), } let row = vec![ SpreadsheetCell::Int(3), SpreadsheetCell::Text(String::from("blue")), SpreadsheetCell::Float(10.12), ];
الشيفرة 9: تعريف تعداد enum لتخزين قيم من أنواع مختلفة في شعاع واحد
تحتاج رست إلى معرفة الأنواع التي ستُخزَّن في الشعاع وقت التصريف حتى تعلم بالضبط المساحة التي ستحتاجها لتخزين كل عنصر في الكومة، ويجب علينا أن نكون واضحين بخصوص الأنواع التي ستُخزَّن في الشعاع. في الحقيقة إذا سمحت رست للشعاع بأن يحمل أي نوع فهناك احتمال أن يتسبب نوع أو أكثر بالأخطاء عند إجراء العمليات على عناصر الشعاع، واستخدام التعداد مع التعبير match
يعني أن رست ستعالج كل حالة بوقت التصريف كما ناقشنا سابقًا.
لن تعمل طريقة التعداد هذه إذا لم تُعرّف جميع أنواع البيانات التي سيحتاجها البرنامج عند وقت التشغيل، ويمكنك استخدام كائن السمة trait object عندها، والذي سنتكلم عنه لاحقًا.
الآن، وبعد أن ناقشنا أكثر الطرق شيوعًا في استخدام الأشعة، ألقِ نظرةً على توثيق الواجهة البرمجية للمزيد من التوابع المفيدة المعرفة في النوع <Vec<T
في المكتبة القياسية، على سبيل المثال، بالإضافة إلى تابع push
، هناك تابع pop
لحذف وإعادة العنصر الأخير ضمن الشعاع.
التخلص من الشعاع يعني التخلص من عناصره
يُحرَّر الشعاع من الذاكرة مثل أيّ هيكل struct
اعتيادي عند خروجه من النطاق كما هو موضح في الشيفرة 10.
{ let v = vec![1, 2, 3, 4]; // استخدام v } // تخرج v من النطاق وتُحرَّر من الذاكرة بحلول هذه النقطة
الشيفرة 10: توضيح النقطة التي يُحرَّر فيها الشعاع ونفقد عناصره
عندما تُحرَّر الذاكرة الخاصة بالشعاع، هذا يعني أننا نفقد عناصر أيضًا، أي أننا لن نستطيع الوصول إلى الأعداد الصحيحة في الشيفرة السابقة بعد خروج الشعاع من النطاق. يتأكد مدقق الاستعارة من أن جميع المراجع التي تشير إلى محتوى الشعاع -إن وُجدت- تُستخدَم فقط عندما يكون الشعاع بنفسه صالحًا.
دعنا ننتقل إلى نوع التجميعة التالي: ألا وهو السلسلة النصية String
.
ترجمة -وبتصرف- لقسم من الفصل Common Collections من كتاب The Rust Programming Language.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.