اذهب إلى المحتوى

أنواع البيانات Data Types في لغة رست Rust


Naser Dakhel

تنتمي كل قيمة في لغة رست إلى نوع بيانات معيّن، ويُساعد ذلك لغة رست بمعرفة نوع البيانات التي تدلّ عليها هذه القيمة وكيفية التعامل معها، وسننظر إلى مجموعتين من أنواع البيانات، هي: القيم المُفردة scalar والقيم المركّبة compound.

تذكر أن لغة رست لغة برمجة متقيدة بأنواع البيانات statically typed، أي أنه يجب أن تعرف أنواع البيانات جميعها عند وقت التصريف، ويستطيع المصرّف عادةً استنتاج نوع المتغيرات بناءً على القيمة وكيفية استخدامها ضمن الشيفرة البرمجية، إلا أننا يجب أن نحدّد الأنواع في بعض الحالات، مثل التحويل من String إلى نوع عددي باستخدام parse كما رأينا في مقالنا السابق فقرة مقارنة التخمين إلى الرقم السري:

let guess: u32 = "42".parse().expect("Not a number!");

نحصل على الخطأ التالي إن لم نُضف النوع u32 : كما هو موضح أعلاه، ويدل الخطأ على أن المصرّف يحتاج المزيد من المعلومات حول النوع الذي نريد استخدامه:

$ cargo build
   Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0282]: type annotations needed
 --> src/main.rs:2:9
  |
2 |     let guess = "42".parse().expect("Not a number!");
  |         ^^^^^ consider giving `guess` a type

For more information about this error, try `rustc --explain E0282`.
error: could not compile `no_type_annotations` due to previous error

ستجد ترميزًا مختلفًا لكل من أنواع البيانات الأخرى.

الأنواع المفردة

يمثّل النوع المفرد scalar type قيمة فردية، ولدى لغة رست أربع أنواع مُفردة أولية هي: الأعداد الصحيحة integers والأعداد ذات الفاصلة العشرية floating-point numbers والقيم البوليانية booleans والمحارف characters، وقد تتعرف على بعضها من لغة برمجة أخرى تعاملت معها سابقًا. دعنا نتحدّث عن كيفية استعمال هذه الأنواع في لغة رست.

أنواع الأعداد الصحيحة

العدد الصحيح integer هو عدد لا يحتوي على جزء كسري، وسبق لنا استخدام نوع من أنواع الأعداد الصحيحة سابقًا وهو u32، ويحدد التصريح عن هذا النوع أن القيمة المُسندة إلى المتغير ستكون عدد صحيح عديم الإشارة unsigned integer (تبدأ الأعداد الصحيحة ذات الإشارة بالحرف i بدلًا من u)، ويأخذ مساحة 32 بت. يوضّح الجدول 3-1 أنواع الأعداد الصحيحة المُضمّنة في لغة رست، ويمكننا استخدام أي من هذه المتغايرات variants للتصريح عن نوع قيمة العدد الصحيح.

الطول ذو إشارة عديم الإشارة
8-بت i8 u8
16-بت i16 u16
32-بت i32 u32
64-بت i64 u64
128-بت i128 u128
يعتمد على معمارية الحاسب isize usize

[جدول 3-1: أنواع الأعداد الصحيحة في راست]

يُمكن أن يكون كل متغاير ذو إشارة أو عديم إشارة وذو طول محدّد، إذ تُشير كلمة ذو إشارة signed وعديم الإشارة unsigned إلى إمكانية كون العدد سالبًا أم لا، وبتعبير آخر، هل يحتاج العدد إلى إشارةٍ معه (ذو إشارة signed) أم أنه سيكون موجبًا فقط وسيُمثّل بالتالي دون أي إشارة (عديم الإشارة unsigned). الأمر مماثل لكتابة الأعداد على ورقة، فعندما نحتاج لاستخدام الإشارة يُوضّح العدد وبجانبه إشارة (سواءً موجبة أو سالبة)، وفي حال كان الافتراض أن جميع الأعداد موجبة، فلا نضع أي إشارة بجانب الأعداد، وتُخزّن الأعداد ذات الإشارة باستخدام تمثيل المتمّم الثنائي two's complement.

يُمكن أن يخزِّن كل متغاير ذي إشارة القيم المنتمية إلى المجال من 2n-1- إلى 2n-1 - 1، إذ تمثّل "n" عدد البتات التي يستخدمها المتغاير، وبالتالي يمكن للنوع i8 تخزين القيم التي تنتمي إلى المجال من 27- إلى 1- 27 الذي يساوي من ‎-128 إلى 127، بينما يمكن للمتغايرات عديمة الإشارة تخزين القيم ضمن المجال من 0 إلى 2n-1، وبالتالي يمكن للنوع u8 أن يخزن الأعداد من 0 إلى 28-1 وهو ما يساوي المجال من 0 إلى 255.

إضافةً لما سبق، يعتمد النوعان isize وusize على معمارية الحاسب الذي يعمل عليه برنامجك، وهو بطول 64 بت إذا كان من معمارية 64 بت وبطول 32 بت إذا كان من معمارية 32 بت.

يُمكنك كتابة الأعداد الصحيحة المُجرّدة integer literals بأي من التنسيقات الموضحة في الجدول 3-2، لاحظ أن لغة رست توفر صياغة لكتابة الأعداد بطريقة تدل على نوعها لتمثيل عدة أنواع عددية إذ تسمح بوجود لاحقة للنوع type suffix مثل "57u8" لتحديد نوعه، ويُمكن أن تستخدم صياغة الأعداد تلك أيضًا الرمز _ بمثابة فاصل بصري لجعل الأعداد أسهل للقراءة مثل "1‎_000" الذي يحمل القيمة "1000" ذاتها.

العدد المجرد مثال
عشري 98‎_222
ست عشري 0xff
ثُماني 0o77
ثُنائي 0b1111_0000
بايت (فقط بحجم u8) b'A'‎

[جدول 3-2: الأعداد الصحيحة المجردة في لغة رست]

إذًا، كيف يمكنك معرفة أي أنواع الأعداد الصحيحة التي يجب عليك استخدامها؟ أنواع لغة رست الافتراضية هي الخيار الأمثل إذا لم تكُن متأكدًا بخصوص هذا الأمر، نوع العدد الصحيح الافتراضي هو i32، والحالة التي قد تستخدم فيها أحد النوعين isize أوusize هي عندما تستخدم قيمة المتغير دليلًا index ما ضمن مجموعة collection.

طفحان الأعداد الصحيحة

بفرض أن هناك متغير من النوع u8 الذي يمكنه تخزين القيم من 0 إلى 255. إذا حاولت إسناد قيمة إلى ذلك المتغير خارج النطاق المذكور -مثل القيمة 256- فسيتسبب ذلك بحدوث ما يسمى طفحان الأعداد الصحيحة integer overflow الذي قد يتسبب بحدوث نتيجة من اثنتان.

تتفقد لغة رست عند تصريف البرنامج في نمط تنقيح الأخطاء debug mode حالات طفحان الأعداد الصحيحة التي ستتسبب بهلع panic برنامجك عند تشغيله، ويستخدم مبرمجو لغة رست مصطلح هلع panic عندما يتوقف البرنامج بسبب خطأ ما، وسنناقش هذا الأمر بتعمق أكبر لاحقًا. لا تتحقق لغة رست من حالات طفحان الأعداد الصحيحة التي تتسبب بهلع البرنامج عند تصريفه باستخدام نمط الإطلاق release mode باستخدام الراية flag‏ ‎--release، وتجري راست بدلًا من ذلك عمليةً تُعرف بانتقال المتمم الثنائي two's complement wrapping إذا حدث أي طفحان. باختصار، تنتقل القيمة التي تحتوي على قيمة أكبر من القيمة العظمى الممكن للنوع تخزينها إلى أصغر قيمة يمكن للمتغير تخزينها، فعلى سبيل المثال تصبح القيمة 256 في النوع u8 مساويةً إلى الصفر والقيمة 257 إلى 1 وهكذا، لن يهلع البرنامج في هذه الحالة، بل سيحمل المتغير قيمةً مختلفة، ويُعدّ الاعتماد على عملية الانتقال wrapping في طفحان الأعداد الصحيحة خطأً.

يُمكنك استخدام أحد الطرق التالية للتعامل على نحوٍ صريح مع حالات الطفحان وهي طرق مُضمنّة في المكتبة القياسية للأنواع العددية الأولية:

  • تمكين الانتقال في جميع أنماط بناء البرنامج باستخدام توابع wrapping_*‎ مثل wrapping_add.
  • إعادة القيمة None إذا لم يكن هناك أي طفحان باستخدام التوابع checked_*‎.
  • إعادة القيمة والقيمة البوليانية التي تشير إلى حدوث طفحان باستخدام توابع overflowing_*‎.
  • إشباع saturate القيم العُظمى والدُنيا للقيمة باستخدام توابع saturating_*‎.

أنواع أعداد الفاصلة العشرية

لدى لغة راست نوعَين من أنواع أعداد الفاصلة العشرية floating-point numbers وهي الأعداد التي تحتوي على فواصل عشرية، وهما f32 و f64، وبحجم 32 بت و64 بت، والنوع الافتراضي هو f64، لأنها تكون بنفس سرعة المُعالجات الحديثة f32 ولكنها أكثر دقة، وجميع أنواع أعداد الفاصلة العشرية ذات إشارة.

إليك مثالًا يوضح أعداد الفاصلة العشرية عمليًا:

اسم الملف: src/main.rs

fn main() {
    let x = 2.0; // f64

    let y: f32 = 3.0; // f32
}

تُمثّل أعداد الفاصلة العشرية بحسب معيار IEEE-754. للنوع f32 دقة وحيدة single-precision، بينما للنوع f64 دقة مضاعفة double precision.

العمليات على الأنواع العددية

تدعم لغة راست العمليات الرياضية الأساسية التي تتوقع إجرائها على الأنواع العددية، وهي الجمع والطرح والضرب والقسمة وباقي القسمة. يُقرّب ناتج قسمة الأعداد الصحيحة إلى أقرب عدد صحيح، وتوضح الشيفرة البرمجية التالية كيفية إجراء كل من العمليات باستخدام تعليمة let:

اسم الملف src/main.rs

fn main() {
    // الجمع
    let sum = 5 + 10;

    // الطرح
    let difference = 95.5 - 4.3;

    // الضرب
    let product = 4 * 30;

    // القسمة
    let quotient = 56.7 / 32.2;
    let floored = 2 / 3; // Results in 0

    // باقي القسمة
    let remainder = 43 % 5;
}

يستخدم كل تعبير من التعابير السابقة عاملًا رياضيًا ويُقيّم الناتج إلى قيمة واحدة، ثم تُسند هذه القيمة إلى المتغير.

النوع البولياني

للنوع البولياني boolean type في لغة رست -كما هو الحال في معظم لغات البرمجة الأخرى- قيمتان: true و false، ويبلغ حجم النوع هذا بتًا واحدًا، ويُحدّد النوع البولياني في لغة راست باستخدام الكلمة bool كما يوضح المثال التالي:

اسم الملف: src/main.rs

fn main() {
    let t = true;

    let f: bool = false; // تحديد النوع بوضوح
}

الاستخدام الأساسي للقيم البوليانية هو في التعابير الشرطية conditionals مثل تعابير if، وسنغطّي تعابير if وكيفية عملها في لغة رست لاحقًا.

نوع المحرف

نوع char في لغة رست هو أكثر أنواع القيم الأبجدية بدائية، إليك بعض الأمثلة عن تصريح قيم char:

اسم الملف: src/main.rs

fn main() {
    let c = 'z';
    let z: char = 'ℤ'; // تحديد النوع بوضوح
    let heart_eyed_cat = '?';
}

لاحظ أننا حددنا النوع char المجرد باستخدام علامتَي تنصيص فردية، بعكس نوع السلسلة النصية string المجرّد الذي يستخدم علامتَي تنصيص مزدوجة، ويبلغ حجم النوع char في لغة راست أربعة بايتات وتمثل القيمة قيمة يونيكود Unicode عددية التي يُمكن أن تمثل قيمًا أكثر ممّا تستطيع الآسكي ASCII تمثيله. تتضمن لغة راست كذلك الأحرف المُعلّمة accented letters وكل من المحارف الصينية واليابانية والكورية، إضافةً إلى الرموز التعبيرية emoji والمسافات الفارغة ذات العرض الصفري zero-width space، إذ تُعد جميع القيم السابقة المذكورة قيمًا صالحة ويُمكن تخزينها في متغير من نوع char.

تتراوح قيم يونيكود العددية من "U+0000" إلى "U+D7FF" ومن "U+E000" إلى "U+10FFFF"، إلا أن مصطلح المحرف character غير موجود في نظام اليونيكود، وبالتالي يمكن ألا يتطابق فهمك كإنسان لماهية المحرف مع تعريف النوع char في لغة راست، وسنناقش هذا الموضوع بالتفصيل لاحقًا.

الأنواع المركبة

يُمكن للأنواع المركبة compound types أن تجمع عدّة قيم في نوع واحد، وللغة رست نوعان من الأنواع المركبة وهي المجموعات tuples والمصفوفات arrays.

نوع المجموعة

المجموعة هي طريقة عامة لجمع عدّة قيم من أنواع مختلفة إلى نوع مُركب واحد، وللمجموعات حجم مُحدّد إذ لا يُمكن أن يكبر أو يصغر الحجم بعد التصريح عنه.

نستطيع إنشاء مجموعة عن طريق كتابة لائحة من العناصر يُفصل ما بينها بالفاصلة داخل قوسين، وكل موضع داخل هذه اللائحة يمثل قيمةً بنوع مُعيّن، ويمكن أن تختلف هذه الأنواع فيما بينها. أضفنا أنواع عناصر اللائحة في مثالنا التالي ولكن هذه الخطوة اختيارية:

اسم الملف: src/main.rs

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
}

يُسند المتغير tup إلى كامل المجموعة، لأن المجموعة تمثّل عنصرًا مركبًا واحدًا، وللحصول على القيم الفردية داخل المجموعة يمكننا استخدام مطابقة الأنماط pattern matching لتفكيك destructure قيمة المجموعة كما هو موضح:

اسم الملف: src/main.rs

fn main() {
    let tup = (500, 6.4, 1);

    let (x, y, z) = tup;

    println!("The value of y is: {y}");
}

يُنشئ هذا البرنامج مجموعة ويُسندها إلى المتغير tup، ومن ثم يستخدم نمطًا مع let لأخذ المتغير tup وتحويله إلى ثلاث قيم منفصلة وهي x و y و z، ويدعى هذا بالتفكيك destructuring لأنه يُفكك المجموعة الواحدة إلى ثلاث أجزاء، ويطبع البرنامج أخيرًا قيمة y المساوية إلى "6.4".

يمكننا أيضًا الوصول إلى عناصر المجموعة مباشرةً باستخدام النقطة (.) متبوعةً بدليل القيمة التي نريد الوصول إليها، كما هو موضح في المثال التالي:

اسم الملف: src/main.rs

fn main() {
    let x: (i32, f64, u8) = (500, 6.4, 1);

    let five_hundred = x.0;

    let six_point_four = x.1;

    let one = x.2;
}

يُنشئ هذا البرنامج مجموعةً باسم x، ثم يستخدم قيمة كل من عناصرها باستخدام دليل كل منها، ويبدأ الدليل الأول بالرقم 0 كما هو الحال في معظم لغات البرمجة.

للمجموعة اسم مميّز إذا كانت فارغة ألا وهو الوحدة unit، وتُكتب قيمتها وقيمة أنواعها بالشكل ()، اللتان تُمثّلان قيمة فارغة أو قيمة إعادة فارغة empty return type، تُعيد التعابير ضمنيًا قيمة الوحدة إذا لم يكن التعبير يُعيد أي قيمة أخرى.

نوع المصفوفة

المصفوفة هي نوع من الأنواع الأخرى التي تحتوي على مجموعة من قيم متعددة، ويجب أن تكون جميع هذه القيم من النوع ذاته على عكس المجموعة، وللمصفوفات حجم ثابت بعكس بعض لغات البرمجة الأخرى.

نكتب القيم في المصفوفة مثل لائحة من القيم مفصول ما بينها بفاصلة داخل أقواس معقوفة square brackets:

اسم الملف: src/main.rs

fn main() {
    let a = [1, 2, 3, 4, 5];
}

يُمكن للمصفوفات أن تكون مفيدةً عندما تريد من بياناتك أن تكون موجودةً على المكدّس stack بدلًا من الكومة heap (سنناقش المكدس والكومة لاحقًا) أو عندما تريد أن تتأكد أن هناك مجموعة ثابتة العدد من العناصر. المصفوفة ليست نوعًا مرنًا مثل نوع الشعاع vector، فالشعاع هو نوع مماثل يحتوي على مجموعة وهو مُضمّن في المكتبة القياسية ويمكن أن يتغير حجمه بالزيادة أو النقصان، وإن لم تكُن متأكدًا أيُّهما تستخدم، فذلك يعني أنك غالبًا بحاجة استخدام الشعاع، وسنناقش هذا الأمر بالتفصيل لاحقًا.

تبرز أهمية المصفوفات عندما تعرف عدد العناصر التي تحتاجها، على سبيل المثال إذا كنت تستخدم أسماء الأشهر في برنامج فمن الأفضل في هذه الحالة استخدام المصفوفة بدلًا من الشعاع لأنك تعلم أنك بحاجة 12 عنصر فقط:

let months = ["January", "February", "March", "April", "May", "June", "July",
              "August", "September", "October", "November", "December"];

يُكتب نوع المصفوفة باستخدام الأقواس المعقوفة مع نوع العناصر ومن ثم فاصلة منقوطة وعدد العناصر ضمن المصفوفة كما هو موضح:

let a: [i32; 5] = [1, 2, 3, 4, 5];

يمثل النوع i32 في مثالنا هذا نوع عناصر المصفوفة، بينما يمثل العدد "5" الذي يقع بعد الفاصلة المنقوطة عدد عناصر المصفوفة الخمس.

يمكنك تهيئة المصفوفة بحيث تحمل القيمة ذاتها لكافة العناصر عن طريق تحديد القيمة الابتدائية initial value متبوعةً بفاصلة منقوطة ومن ثم طول المصفوفة ضمن أقواس معقوفة، كما هو موضح:

let a = [3; 5];

ستحتوي المصفوفة a على 5 عناصر وستكون قيم العناصر جميعها مساوية إلى 3 مبدئيًا، وهذا الأمر مماثل لكتابة السطر البرمجي let a = [3, 3, 3 ,3 ,3];‎ إلا أن هذه الطريقة مختصرة.

الوصول إلى عناصر المصفوفة

تُمثل المصفوفة جزءًا واحدًا معلوم الحجم من الذاكرة، والذي يُمكن تخزينه في المكدس، ويمكنك الوصول إلى عناصر المصفوفة باستخدام الدليل كما هو موضح:

اسم الملف: src/main.rs

fn main() {
    let a = [1, 2, 3, 4, 5];

    let first = a[0];
    let second = a[1];
}

في مثالنا السابق، سيُسند إلى المتغير first القيمة الابتدائية 1 لأنها القيمة الموجودة في الدليل [0] ضمن المصفوفة، بينما سيُسند إلى المتغير second القيمة 2 لأنها القيمة الموجودة في الدليل[1] ضمن المصفوفة.

محاولة الوصول الخاطئ إلى عناصر المصفوفة

دعنا نرى ما الذي سيحدث إذا حاولت الوصول إلى عنصر من عناصر المصفوفة إذا كان ذلك العنصر يقع خارج المصفوفة بعد نهايتها، ولنقل أننا سننفّذ الشيفرة البرمجية التالية المشابهة للعبة التخمين في المقال السابق بالحصول على دليل المصفوفة من المستخدم:

اسم الملف: src/main.rs

use std::io;

fn main() {
    let a = [1, 2, 3, 4, 5];

    println!("Please enter an array index.");

    let mut index = String::new();

    io::stdin()
        .read_line(&mut index)
        .expect("Failed to read line");

    let index: usize = index
        .trim()
        .parse()
        .expect("Index entered was not a number");

    let element = a[index];

    println!("The value of the element at index {index} is: {element}");
}

ستُصرّف الشيفرة البرمجية بنجاح، وإذا شغلت البرنامج باستخدام cargo run وأدخلت القيم 0 أو 1 أو 2 أو 3 أو 4، فسيطبع البرنامج القيمة الموافقة لهذا الدليل ضمن المصفوفة، إلا أنك ستحصل على الخرج التالي إذا حاولت إدخال قيمة أكبر من حجم المصفوفة (مثل 10):

thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 10', src/main.rs:19:19
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

تسبب البرنامج بخطأ عند التشغيل runtime error عند إدخال قيمة خاطئة إلى عملية الوصول للعناصر بالدليل، وانتهى البرنامج برسالة خطأ ولم يُنفّذ تعليمة !println الأخيرة. تتفقد لغة راست الدليل الذي حدّدته عند محاولتك الوصول إليه فيما إذا كان أصغر من حجم المصفوفة، وإذا كان الدليل أكبر أو يساوي حجم المصفوفة فسيهلع panic البرنامج، وتحدث عملية التفقد هذه عند وقت التشغيل خصوصًا في هذه الحالة وذلك لأن المصرف ربما لن يعرف القيمة التي سيدخلها المستخدم عند تشغيل الشيفرة بعد ذلك.

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

ترجمة -وبتصرف- للقسم Data Types من كتاب The Rust Programming Language.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...