Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

أنواع البيانات (Data Types)

كل قيمة في Rust تنتمي إلى “نوع بيانات” (Data Type) معين، والذي يخبر Rust بنوع البيانات التي يتم تحديدها حتى تعرف كيفية التعامل مع تلك البيانات. سنلقي نظرة على مجموعتين فرعيتين من أنواع البيانات: السلمية (Scalar) والمركبة (Compound).

ضع في اعتبارك أن Rust هي لغة “ذات أنواع ثابتة” (Statically Typed)، مما يعني أنها يجب أن تعرف أنواع جميع المتغيرات في وقت التجميع (Compile Time). يمكن للمترجم (Compiler) عادةً استنتاج النوع الذي نريد استخدامه بناءً على القيمة وكيفية استخدامنا لها. في الحالات التي تكون فيها أنواع عديدة ممكنة، مثل عندما قمنا بتحويل String إلى نوع رقمي باستخدام parse في قسم “مقارنة التخمين بالرقم السري” في الفصل 2، يجب علينا إضافة توضيح للنوع (Type Annotation)، مثل هذا:

#![allow(unused)]
fn main() {
let guess: u32 = "42".parse().expect("Not a number!");
}

إذا لم نضف Type Annotation الموضح في الكود السابق (: u32)، فستعرض Rust الخطأ التالي، مما يعني أن الـ Compiler يحتاج إلى مزيد من المعلومات منا لمعرفة النوع الذي نريد استخدامه:

$ cargo build
   Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0284]: type annotations needed
 --> src/main.rs:2:9
  |
2 |     let guess = "42".parse().expect("Not a number!");
  |         ^^^^^        ----- type must be known at this point
  |
  = note: cannot satisfy `<_ as FromStr>::Err == _`
help: consider giving `guess` an explicit type
  |
2 |     let guess: /* Type */ = "42".parse().expect("Not a number!");
  |              ++++++++++++

For more information about this error, try `rustc --explain E0284`.
error: could not compile `no_type_annotations` (bin "no_type_annotations") due to 1 previous error

سترى Type Annotations مختلفة لأنواع البيانات الأخرى.

الأنواع السلمية (Scalar Types)

يمثل النوع “السلمي” (Scalar) قيمة واحدة. تمتلك Rust أربعة أنواع سلمية أساسية: الأعداد الصحيحة (Integers)، وأرقام الفاصلة العائمة (Floating-point numbers)، والقيم المنطقية (Booleans)، والأحرف (Characters). قد تتعرف على هذه الأنواع من لغات البرمجة الأخرى. دعنا ننتقل إلى كيفية عملها في Rust.

أنواع الأعداد الصحيحة (Integer Types)

“العدد الصحيح” (Integer) هو رقم بدون مكون كسري. استخدمنا نوعاً واحداً من الـ Integers في الفصل 2، وهو نوع u32. يشير إعلان النوع هذا إلى أن القيمة المرتبطة به يجب أن تكون عدداً صحيحاً غير موقع (Unsigned Integer) - تبدأ أنواع الأعداد الصحيحة الموقعة (Signed Integer) بـ i بدلاً من u - يشغل مساحة 32 بت. يوضح الجدول 3-1 أنواع الأعداد الصحيحة المدمجة في Rust. يمكننا استخدام أي من هذه التنويعات للإعلان عن نوع قيمة عدد صحيح.

الجدول 3-1: أنواع الأعداد الصحيحة في Rust

الطولموقع (Signed)غير موقع (Unsigned)
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
معتمد على المعماريةisizeusize

يمكن أن تكون كل تنويعة إما Signed أو Unsigned ولها حجم صريح. تشير كلمتا “موقع” (Signed) و “غير موقع” (Unsigned) إلى ما إذا كان من الممكن أن يكون الرقم سالباً - بعبارة أخرى، ما إذا كان الرقم يحتاج إلى علامة معه (Signed) أو ما إذا كان سيكون موجباً دائماً وبالتالي يمكن تمثيله بدون علامة (Unsigned). الأمر يشبه كتابة الأرقام على الورق: عندما تهم العلامة، يظهر الرقم بعلامة زائد أو علامة ناقص؛ ومع ذلك، عندما يكون من الآمن افتراض أن الرقم موجب، فإنه يظهر بدون علامة. يتم تخزين الأرقام الـ Signed باستخدام تمثيل المكمل لـ 2.

يمكن لكل تنويعة Signed تخزين أرقام من −(2n − 1) إلى 2n − 1 − 1 ضمناً، حيث n هو عدد البتات التي تستخدمها تلك التنويعة. لذا، يمكن لـ i8 تخزين أرقام من −(27) إلى 27 − 1، وهو ما يعادل −128 إلى 127. يمكن للتنويعات الـ Unsigned تخزين أرقام من 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.

الجدول 3-2: الثوابت العددية الصحيحة في Rust

الثوابت الرقميةمثال
عشري (Decimal)98_222
ست عشري (Hex)0xff
ثماني (Octal)0o77
ثنائي (Binary)0b1111_0000
بايت (Byte) (u8 فقط)b'A'

إذاً كيف تعرف أي نوع من الـ Integers تستخدم؟ إذا كنت غير متأكد، فإن الخيارات الافتراضية في Rust هي أماكن جيدة للبدء بشكل عام: أنواع الـ Integers الافتراضية هي i32. الحالة الأساسية التي تستخدم فيها isize أو usize هي عند فهرسة (Indexing) نوع من المجموعات (Collections).

طفحان الأعداد الصحيحة (Integer Overflow)

لنفترض أن لديك متغيراً من نوع u8 يمكنه حمل قيم بين 0 و 255. إذا حاولت تغيير المتغير إلى قيمة خارج هذا النطاق، مثل 256، فسيحدث “طفحان الأعداد الصحيحة” (Integer Overflow)، مما قد يؤدي إلى أحد سلوكين. عندما تقوم بالتجميع في وضع التصحيح (Debug Mode)، تتضمن Rust فحوصات لـ Integer Overflow تتسبب في “هلع” (Panic) برنامجك في الـ Runtime إذا حدث هذا السلوك. تستخدم Rust مصطلح “الهلع” (Panicking) عندما يخرج البرنامج مع وجود خطأ؛ سنناقش الـ Panics بمزيد من العمق في قسم “أخطاء غير قابلة للاسترداد مع panic! في الفصل 9.

عندما تقوم بالتجميع في وضع الإصدار (Release Mode) باستخدام علم --release لا تتضمن Rust فحوصات لـ Integer Overflow التي تسبب Panics. بدلاً من ذلك، إذا حدث Overflow، تقوم Rust بإجراء “التفاف المكمل لـ 2” (Two’s Complement Wrapping). باختصار، القيم الأكبر من القيمة القصوى التي يمكن أن يحملها النوع “تلتف” إلى الحد الأدنى من القيم التي يمكن أن يحملها النوع. في حالة u8 تصبح القيمة 256 هي 0، والقيمة 257 تصبح 1، وهكذا. لن يهلع البرنامج، ولكن المتغير سيكون له قيمة ربما ليست هي ما كنت تتوقع أن تكون عليه. الاعتماد على سلوك الالتفاف الخاص بـ Integer Overflow يعتبر خطأ.

للتعامل صراحة مع احتمالية حدوث Overflow، يمكنك استخدام هذه المجموعات من الـ Methods التي توفرها الـ Standard Library للأنواع الرقمية الأولية:

  • الالتفاف في جميع الأوضاع باستخدام Methods الـ wrapping_* مثل wrapping_add.
  • إرجاع قيمة None إذا كان هناك Overflow باستخدام Methods الـ checked_*.
  • إرجاع القيمة وقيمة منطقية تشير إلى ما إذا كان هناك Overflow باستخدام Methods الـ overflowing_*.
  • التشبع عند القيم الدنيا أو القصوى للقيمة باستخدام Methods الـ saturating_*.

أنواع الفاصلة العائمة (Floating-Point Types)

تمتلك Rust أيضاً نوعين أوليين لـ “أرقام الفاصلة العائمة” (Floating-point numbers)، وهي الأرقام التي تحتوي على فواصل عشرية. أنواع الـ Floating-point في Rust هي f32 و f64 وحجمهما 32 بت و 64 بت على التوالي. النوع الافتراضي هو f64 لأنه في وحدات المعالجة المركزية (CPUs) الحديثة، يكون بنفس سرعة f32 تقريباً ولكنه قادر على توفير دقة أكبر. جميع أنواع الـ Floating-point هي Signed.

إليك مثال يوضح أرقام الفاصلة العائمة أثناء العمل:

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

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

    let y: f32 = 3.0; // f32
}

يتم تمثيل الـ Floating-point numbers وفقاً لمعيار IEEE-754.

العمليات الرقمية (Numeric Operations)

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

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

fn main() {
    // addition
    let sum = 5 + 10;

    // subtraction
    let difference = 95.5 - 4.3;

    // multiplication
    let product = 4 * 30;

    // division
    let quotient = 56.7 / 32.2;
    let truncated = -5 / 3; // Results in -1

    // remainder
    let remainder = 43 % 5;
}

تستخدم كل تعبير (Expression) في هذه العبارات عاملاً حسابياً ويتم تقييمه إلى قيمة واحدة، والتي يتم ربطها بعد ذلك بمتغير. يحتوي الملحق ب على قائمة بجميع العوامل (Operators) التي توفرها Rust.

النوع المنطقي (The Boolean Type)

كما هو الحال في معظم لغات البرمجة الأخرى، فإن النوع المنطقي (Boolean) في Rust له قيمتان ممكنتان: true و false. حجم الـ Booleans هو بايت واحد. يتم تحديد النوع الـ Boolean في Rust باستخدام bool. على سبيل المثال:

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

fn main() {
    let t = true;

    let f: bool = false; // with explicit type annotation
}

الطريقة الرئيسية لاستخدام قيم الـ Boolean هي من خلال الشروط، مثل تعبير if. سنغطي كيفية عمل تعبيرات if في Rust في قسم “تدفق التحكم”.

نوع الحرف (The Character Type)

نوع char في Rust هو النوع الأبجدي الأكثر أولية في اللغة. إليك بعض الأمثلة على الإعلان عن قيم char:

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

fn main() {
    let c = 'z';
    let z: char = 'ℤ'; // with explicit type annotation
    let heart_eyed_cat = '😻';
}

لاحظ أننا نحدد ثوابت الـ char (Character Literals) بعلامات اقتباس مفردة، على عكس الـ String Literals التي تستخدم علامات اقتباس مزدوجة. حجم نوع char في Rust هو 4 بايت ويمثل قيمة سلمية لليونيكود (Unicode Scalar Value)، مما يعني أنه يمكنه تمثيل أكثر بكثير من مجرد ASCII. الأحرف المشكلة؛ والأحرف الصينية واليابانية والكورية؛ والرموز التعبيرية (Emojis)؛ والمساحات ذات العرض الصفري كلها قيم char صالحة في Rust. تتراوح الـ Unicode Scalar Values من U+0000 إلى U+D7FF ومن U+E000 إلى U+10FFFF ضمناً. ومع ذلك، فإن “الحرف” ليس حقاً مفهوماً في Unicode، لذا فإن حدسك البشري لما هو “الحرف” قد لا يتطابق مع ما هو الـ char في Rust. سنناقش هذا الموضوع بالتفصيل في “تخزين النصوص المشفرة بـ UTF-8 باستخدام السلاسل النصية” في الفصل 8.

الأنواع المركبة (Compound Types)

يمكن لـ “الأنواع المركبة” (Compound Types) تجميع قيم متعددة في نوع واحد. تمتلك Rust نوعين مركبين أوليين: الصفوف (Tuples) والمصفوفات (Arrays).

نوع الصف (The Tuple Type)

“الصف” (Tuple) هو طريقة عامة لتجميع عدد من القيم بأنواع متنوعة في نوع مركب واحد. الـ Tuples لها طول ثابت: بمجرد الإعلان عنها، لا يمكن أن تنمو أو تتقلص في الحجم.

ننشئ Tuple عن طريق كتابة قائمة قيم مفصولة بفاصلة داخل أقواس. كل موضع في الـ Tuple له نوع، ولا يجب أن تكون أنواع القيم المختلفة في الـ Tuple هي نفسها. لقد أضفنا Type Annotations اختيارية في هذا المثال:

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

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

يرتبط المتغير tup بالـ Tuple بأكملها لأن الـ Tuple تعتبر عنصراً مركباً واحداً. للحصول على القيم الفردية من الـ Tuple، يمكننا استخدام مطابقة الأنماط (Pattern Matching) لـ “تفكيك” (Destructure) قيمة الـ Tuple، مثل هذا:

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

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

    let (x, y, z) = tup;

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

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

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

اسم الملف: 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;
}

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

الـ Tuple التي لا تحتوي على أي قيم لها اسم خاص، وهو “الوحدة” (Unit). هذه القيمة ونوعها المماثل يكتبان () ويمثلان قيمة فارغة أو نوع إرجاع فارغ. تعيد التعبيرات (Expressions) قيمة الـ Unit ضمنياً إذا لم تعيد أي قيمة أخرى.

نوع المصفوفة (The Array Type)

طريقة أخرى لامتلاك مجموعة من قيم متعددة هي باستخدام “المصفوفة” (Array). على عكس الـ Tuple، يجب أن يكون لكل عنصر في الـ Array نفس النوع. وعلى عكس الـ Arrays في بعض اللغات الأخرى، فإن الـ Arrays في Rust لها طول ثابت.

نكتب القيم في الـ Array كقائمة مفصولة بفاصلة داخل أقواس مربعة:

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

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

تكون الـ Arrays مفيدة عندما تريد تخصيص بياناتك على “المكدس” (Stack)، تماماً مثل الأنواع الأخرى التي رأيناها حتى الآن، بدلاً من “الكومة” (Heap) (سنناقش الـ Stack والـ Heap أكثر في الفصل 4) أو عندما تريد التأكد من أن لديك دائماً عدداً ثابتاً من العناصر. ومع ذلك، فإن الـ Array ليست مرنة مثل نوع المتجه (Vector). الـ Vector هو نوع مجموعة مماثل توفره الـ Standard Library ويُسمح له بالنمو أو التقلص في الحجم لأن محتوياته تعيش على الـ Heap. إذا كنت غير متأكد مما إذا كنت ستستخدم Array أو Vector، فمن المحتمل أنك يجب أن تستخدم Vector. يناقش الفصل 8 الـ Vectors بمزيد من التفصيل.

ومع ذلك، تكون الـ Arrays أكثر فائدة عندما تعرف أن عدد العناصر لن يحتاج إلى التغيير. على سبيل المثال، إذا كنت تستخدم أسماء الأشهر في برنامج ما، فمن المحتمل أن تستخدم Array بدلاً من Vector لأنك تعلم أنها ستحتوي دائماً على 12 عنصراً:

#![allow(unused)]
fn main() {
let months = ["January", "February", "March", "April", "May", "June", "July",
              "August", "September", "October", "November", "December"];
}

تكتب نوع الـ Array باستخدام أقواس مربعة مع نوع كل عنصر، وفاصلة منقوطة، ثم عدد العناصر في الـ Array، هكذا:

#![allow(unused)]
fn main() {
let a: [i32; 5] = [1, 2, 3, 4, 5];
}

هنا، i32 هو نوع كل عنصر. بعد الفاصلة المنقوطة، يشير الرقم 5 إلى أن الـ Array تحتوي على خمسة عناصر.

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

#![allow(unused)]
fn main() {
let a = [3; 5];
}

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

الوصول إلى عناصر المصفوفة (Array Element Access)

الـ Array هي قطعة واحدة من الذاكرة ذات حجم معروف وثابت يمكن تخصيصها على الـ Stack. يمكنك الوصول إلى عناصر الـ Array باستخدام الـ Indexing، مثل هذا:

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

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

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

في هذا المثال، سيحصل المتغير المسمى first على القيمة 1 لأن هذه هي القيمة عند الفهرس [0] في الـ Array. سيحصل المتغير المسمى second على القيمة 2 من الفهرس [1] في الـ Array.

الوصول غير الصالح لعناصر المصفوفة (Invalid Array Element Access)

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

اسم الملف: 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 فسيقوم البرنامج بطباعة القيمة المقابلة عند ذلك الفهرس في الـ Array. إذا أدخلت بدلاً من ذلك رقماً يتجاوز نهاية الـ Array، مثل 10 فسترى مخرجات مثل هذه:

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

أدى البرنامج إلى خطأ في الـ Runtime عند نقطة استخدام قيمة غير صالحة في عملية الـ Indexing. خرج البرنامج برسالة خطأ ولم ينفذ عبارة println! النهائية. عندما تحاول الوصول إلى عنصر باستخدام الـ Indexing، ستتحقق Rust من أن الفهرس الذي حددته أقل من طول الـ Array. إذا كان الفهرس أكبر من أو يساوي الطول، فستقوم Rust بالـ Panic. يجب أن يحدث هذا الفحص في الـ Runtime، خاصة في هذه الحالة، لأن الـ Compiler لا يمكنه معرفة القيمة التي سيدخلها المستخدم عندما يقوم بتشغيل الكود لاحقاً.

هذا مثال على مبادئ أمان الذاكرة (Memory Safety) في Rust أثناء العمل. في العديد من اللغات منخفضة المستوى، لا يتم إجراء هذا النوع من الفحص، وعندما تقدم فهرساً غير صحيح، يمكن الوصول إلى ذاكرة غير صالحة. تحميك Rust من هذا النوع من الأخطاء عن طريق الخروج فوراً بدلاً من السماح بالوصول إلى الذاكرة والاستمرار. يناقش الفصل 9 المزيد من معالجة الأخطاء في Rust وكيف يمكنك كتابة كود مقروء وآمن لا يهلع ولا يسمح بالوصول غير الصالح للذاكرة.