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

برمجة لعبة التخمين

دعونا نبدأ رحلتنا مع لغة Rust من خلال العمل على مشروع عملي معاً! يقدم لك هذا الفصل بعض مفاهيم Rust الشائعة من خلال توضيح كيفية استخدامها في برنامج حقيقي. ستتعلم عن let و match والدوال (methods) والدوال المرتبطة (associated functions) والصناديق الخارجية (external crates) والمزيد! في الفصول التالية، سنستكشف هذه الأفكار بمزيد من التفصيل. في هذا الفصل، ستتدرب فقط على الأساسيات.

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

إعداد مشروع جديد

لإعداد مشروع جديد، انتقل إلى دليل projects الذي أنشأته في الفصل الأول وأنشئ مشروعاً جديداً باستخدام Cargo، كما يلي:

$ cargo new guessing_game
$ cd guessing_game

الأمر الأول، cargo new يأخذ اسم المشروع (guessing_game) كمعامل أول. الأمر الثاني يغير المسار إلى دليل المشروع الجديد.

ألقِ نظرة على ملف Cargo.toml الذي تم إنشاؤه:

اسم الملف: Cargo.toml

[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"

[dependencies]

كما رأيت في الفصل الأول، يقوم cargo new بإنشاء برنامج “Hello, world!” لك. تحقق من ملف src/main.rs:

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

fn main() {
    println!("Hello, world!");
}

الآن دعونا نقوم بتجميع برنامج “Hello, world!” هذا وتشغيله في نفس الخطوة باستخدام أمر cargo run:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
     Running `target/debug/guessing_game`
Hello, world!

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

أعد فتح ملف src/main.rs. ستكتب كل الكود في هذا الملف.

معالجة التخمين

الجزء الأول من برنامج لعبة التخمين سيطلب مدخلات من المستخدم، ويعالج تلك المدخلات، ويتحقق من أن المدخلات في الشكل المتوقع. للبدء، سنسمح للاعب بإدخال تخمين. أدخل الكود الموجود في القائمة 2-1 في ملف src/main.rs.

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

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

    println!("You guessed: {guess}");
}

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

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

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

    println!("You guessed: {guess}");
}

بشكل افتراضي، تمتلك Rust مجموعة من العناصر المحددة في المكتبة القياسية والتي تجلبها إلى نطاق كل برنامج. تسمى هذه المجموعة بـ prelude، ويمكنك رؤية كل شيء فيها في توثيق المكتبة القياسية.

إذا كان النوع الذي تريد استخدامه ليس في الـ prelude، فيجب عليك جلب ذلك النوع إلى النطاق صراحةً باستخدام عبارة use. يوفر لك استخدام مكتبة std::io عدداً من الميزات المفيدة، بما في ذلك القدرة على قبول مدخلات المستخدم.

كما رأيت في الفصل الأول، فإن دالة main هي نقطة الدخول إلى البرنامج:

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

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

    println!("You guessed: {guess}");
}

تعلن صيغة fn عن دالة جديدة؛ وتشير الأقواس () إلى عدم وجود معاملات؛ ويبدأ القوس المتعرج { جسم الدالة.

كما تعلمت أيضاً في الفصل الأول، فإن println! هو ماكرو (macro) يطبع سلسلة نصية على الشاشة:

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

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

    println!("You guessed: {guess}");
}

يقوم هذا الكود بطباعة مطالبة توضح ماهية اللعبة وتطلب مدخلات من المستخدم.

تخزين القيم باستخدام المتغيرات

بعد ذلك، سننشئ متغيراً لتخزين مدخلات المستخدم، كما يلي:

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

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

    println!("You guessed: {guess}");
}

الآن أصبح البرنامج مثيراً للاهتمام! هناك الكثير مما يحدث في هذا السطر الصغير. نستخدم عبارة let لإنشاء المتغير. إليك مثالاً آخر:

let apples = 5;

ينشئ هذا السطر متغيراً جديداً باسم apples ويربطه بالقيمة 5. في Rust، تكون المتغيرات غير قابلة للتغيير (immutable) افتراضياً، مما يعني أنه بمجرد إعطاء المتغير قيمة، فلن تتغير القيمة. سنناقش هذا المفهوم بالتفصيل في قسم “المتغيرات وقابلية التغيير” في الفصل الثالث. لجعل المتغير قابلاً للتغيير، نضيف mut قبل اسم المتغير:

let apples = 5; // غير قابل للتغيير
let mut bananas = 5; // قابل للتغيير

ملاحظة: تبدأ صيغة // تعليقاً يستمر حتى نهاية السطر. تتجاهل Rust كل شيء في التعليقات. سنناقش التعليقات بمزيد من التفصيل في الفصل الثالث.

بالعودة إلى برنامج لعبة التخمين، تعلم الآن أن let mut guess ستقدم متغيراً قابلاً للتغيير باسم guess. تخبر علامة التساوي (=) لغة Rust أننا نريد ربط شيء ما بالمتغير الآن. على يمين علامة التساوي توجد القيمة التي يرتبط بها guess وهي نتيجة استدعاء String::new وهي دالة تعيد نسخة جديدة من String. النوع String هو نوع سلسلة نصية توفره المكتبة القياسية وهو عبارة عن نص بتنسيق UTF-8 وقابل للنمو.

تشير صيغة :: في سطر ::new إلى أن new هي دالة مرتبطة (associated function) بنوع String. الدالة المرتبطة هي دالة يتم تنفيذها على نوع معين، في هذه الحالة String. تنشئ دالة new هذه سلسلة نصية جديدة وفارغة. ستجد دالة new في العديد من الأنواع لأنها اسم شائع لدالة تنشئ قيمة جديدة من نوع ما.

بشكل كامل، قام سطر let mut guess = String::new(); بإنشاء متغير قابل للتغيير مرتبط حالياً بنسخة جديدة وفارغة من String. يا للهول!

استقبال مدخلات المستخدم

تذكر أننا قمنا بتضمين وظائف الإدخال/الإخراج من المكتبة القياسية باستخدام use std::io; في السطر الأول من البرنامج. الآن سنستدعي دالة stdin من وحدة io والتي ستسمح لنا بالتعامل مع مدخلات المستخدم:

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

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

    println!("You guessed: {guess}");
}

إذا لم نكن قد استوردنا وحدة io باستخدام use std::io; في بداية البرنامج، كان بإمكاننا استدعاء الدالة بكتابتها كـ std::io::stdin. تعيد دالة stdin نسخة من std::io::Stdin وهو نوع يمثل مقبضاً (handle) للإدخال القياسي لمحطتك الطرفية (terminal).

بعد ذلك، يستدعي السطر .read_line(&mut guess) دالة read_line على مقبض الإدخال القياسي للحصول على مدخلات من المستخدم. نقوم أيضاً بتمرير &mut guess كمعامل لـ read_line لإخبارها بالسلسلة النصية التي يجب تخزين مدخلات المستخدم فيها. الوظيفة الكاملة لـ read_line هي أخذ كل ما يكتبه المستخدم في الإدخال القياسي وإلحاقه بسلسلة نصية (دون الكتابة فوق محتوياتها)، لذا نقوم بتمرير تلك السلسلة كمعامل. يجب أن يكون معامل السلسلة النصية قابلاً للتغيير حتى تتمكن الدالة من تغيير محتوى السلسلة.

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

التعامل مع الفشل المحتمل باستخدام Result

ما زلنا نعمل على هذا السطر من الكود. نحن نناقش الآن السطر الثالث من النص، ولكن لاحظ أنه لا يزال جزءاً من سطر منطقي واحد من الكود. الجزء التالي هو هذه الدالة:

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

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

    println!("You guessed: {guess}");
}

كان بإمكاننا كتابة هذا الكود كـ:

io::stdin().read_line(&mut guess).expect("فشل في قراءة السطر");

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

كما ذكرنا سابقاً، تضع read_line كل ما يدخله المستخدم في السلسلة النصية التي نمررها إليها، ولكنها تعيد أيضاً قيمة من نوع Result. النوع Result هو تعداد (enumeration)، وغالباً ما يسمى enum، وهو نوع يمكن أن يكون في حالة واحدة من عدة حالات ممكنة. نسمي كل حالة ممكنة بـ variant.

سيغطي الفصل السادس التعدادات بمزيد من التفصيل. الغرض من أنواع Result هذه هو تشفير معلومات معالجة الأخطاء.

حالات Result هي Ok و Err. تشير حالة Ok إلى أن العملية كانت ناجحة، وتحتوي على القيمة التي تم إنشاؤها بنجاح. تعني حالة Err أن العملية فشلت، وتحتوي على معلومات حول كيفية أو سبب فشل العملية.

القيم من نوع Result مثل القيم من أي نوع، لها دوال محددة عليها. نسخة Result لها دالة expect يمكنك استدعاؤها. إذا كانت نسخة Result هذه هي قيمة Err فستتسبب expect في تعطل البرنامج وعرض الرسالة التي مررتها كمعامل لـ expect. إذا أعادت دالة read_line قيمة Err فمن المحتمل أن يكون ذلك نتيجة لخطأ قادم من نظام التشغيل الأساسي. إذا كانت نسخة Result هذه هي قيمة Ok فستأخذ expect القيمة المرتجعة التي يحملها Ok وتعيد تلك القيمة إليك لتتمكن من استخدامها. في هذه الحالة، تلك القيمة هي عدد البايتات في مدخلات المستخدم.

إذا لم تستدعِ expect فسيتم تجميع البرنامج ولكنك ستحصل على تحذير:

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
warning: unused `Result` that must be used
  --> src/main.rs:10:5
   |
10 |     io::stdin().read_line(&mut guess);
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: this `Result` may be an `Err` variant, which should be handled
   = note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
   |
10 |     let _ = io::stdin().read_line(&mut guess);
   |     +++++++

warning: `guessing_game` (bin "guessing_game") generated 1 warning
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.59s

تحذر Rust من أنك لم تستخدم قيمة Result المرتجعة من read_line مما يشير إلى أن البرنامج لم يعالج خطأً محتملاً.

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

طباعة القيم باستخدام نائبات println!

بصرف النظر عن القوس المتعرج الختامي، هناك سطر واحد فقط متبقٍ في الكود الذي أضفناه حتى الآن:

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

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

    println!("You guessed: {guess}");
}

يطبع هذا السطر السلسلة النصية التي تحتوي الآن على مدخلات المستخدم. مجموعة الأقواس المتعرجة {} هي نائبة (placeholder): فكر في {} كأطراف كماشة صغيرة تثبت القيمة في مكانها. عند طباعة قيمة متغير، يمكن أن يوضع اسم المتغير داخل الأقواس المتعرجة. عند طباعة نتيجة تعبير، ضع أقواس متعرجة فارغة في سلسلة التنسيق، ثم اتبع سلسلة التنسيق بقائمة من التعبيرات مفصولة بفاصلة لطباعتها في كل نائبة من الأقواس المتعرجة الفارغة بالترتيب. طباعة متغير ونتيجة تعبير في استدعاء واحد لـ println! ستبدو كما يلي:

#![allow(unused)]
fn main() {
let x = 5;
let y = 10;

println!("x = {x} and y + 2 = {}", y + 2);
}

سيطبع هذا الكود x = 5 and y + 2 = 12.

اختبار الجزء الأول

دعونا نختبر الجزء الأول من لعبة التخمين. قم بتشغيله باستخدام cargo run:

{{#include ../listings/ch02-guessing-game-tutorial/listing-02-01/output.txt}}

في هذه المرحلة، الجزء الأول من اللعبة قد انتهى: نحن نحصل على مدخلات من لوحة المفاتيح ثم نطبعها.

توليد رقم سري

بعد ذلك، نحتاج إلى توليد رقم سري سيحاول المستخدم تخمينه. يجب أن يكون الرقم السري مختلفاً في كل مرة حتى تكون اللعبة ممتعة لإعادة اللعب؛ سنستخدم رقماً عشوائياً بين 1 و 100 حتى لا تكون اللعبة صعبة للغاية. لا تتضمن Rust وظائف أرقام عشوائية في مكتبتها القياسية بعد. ومع ذلك، يوفر فريق Rust صندوق rand (rand crate) بهذه الوظيفة.

استخدام صندوق للحصول على المزيد من الوظائف

تذكر أن الصندوق (crate) هو مجموعة من ملفات كود Rust. المشروع الذي نقوم ببنائه هو صندوق ثنائي (binary crate)، وهو ملف قابل للتنفيذ. صندوق rand هو صندوق مكتبة (library crate)، والذي يحتوي على كود مخصص لاستخدامه في برامج أخرى ولا يمكن تشغيله بمفرده.

استخدام Cargo للصناديق الخارجية هو المكان الذي يتألق فيه Cargo حقاً. قبل أن نتمكن من كتابة كود يستخدم rand نحتاج إلى تعديل ملف Cargo.toml لتضمين صندوق rand كاعتمادية (dependency). افتح ذلك الملف الآن وأضف السطر التالي في الأسفل، تحت عنوان القسم [dependencies] الذي أنشأه Cargo لك. تأكد من تحديد rand تماماً كما فعلنا هنا، مع رقم الإصدار هذا، وإلا فقد لا تعمل أمثلة الكود في هذا البرنامج التعليمي (في وقت كتابة هذا، كان rand بالإصدار 0.8.5):

اسم الملف: Cargo.toml

[dependencies]
rand = "0.8.5"

في ملف Cargo.toml كل ما يأتي بعد العنوان هو جزء من ذلك القسم الذي يستمر حتى يبدأ قسم آخر. في [dependencies] تخبر Cargo بالصناديق الخارجية التي يعتمد عليها مشروعك وأي إصدارات من تلك الصناديق تحتاجها. في هذه الحالة، نحدد صندوق rand مع محدد الإصدار الدلالي (Semantic Versioning) 0.8.5. يفهم Cargo الإصدار الدلالي (يسمى أحياناً SemVer)، وهو معيار لكتابة أرقام الإصدارات. الرقم 0.8.5 هو في الواقع اختصار لـ ^0.8.5 مما يعني أي إصدار يتوافق مع الإصدار 0.8.5 على الأقل ولكن أقل من 0.9.0.

يعتبر Cargo أن هذه الإصدارات لها واجهات برمجية (APIs) متوافقة مع الإصدار 0.8.5 ويضمن هذا التحديد أنك ستحصل على أحدث إصدار تصحيحي (patch release) سيظل يجمع مع الكود في هذا الفصل. لا يمكن ضمان توافق أي إصدار 0.9.0 أو أعلى مع الواجهة البرمجية التي تستخدمها الأمثلة التالية.

الآن، دون تغيير أي كود، دعونا نبني المشروع، كما هو موضح في القائمة 2-2.

$ cargo build
    Updating crates.io index
  Downloaded rand v0.8.5
  Downloaded libc v0.2.143
  Downloaded rand_chacha v0.3.1
  Downloaded rand_core v0.6.4
   Compiling libc v0.2.143
   Compiling rand_core v0.6.4
   Compiling rand_chacha v0.3.1
   Compiling rand v0.8.5
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 2.53s

القائمة 2-2: المخرجات بعد إضافة صندوق rand كاعتمادية وتشغيل cargo build

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

عندما نقوم بتضمين اعتمادية خارجية، يجلب Cargo أحدث الإصدارات من كل ما تحتاجه تلك الاعتمادية من Crates.io وهو المكان الذي ينشر فيه الأشخاص في نظام Rust البيئي مشاريع Rust مفتوحة المصدر الخاصة بهم ليستخدمها الآخرون.

بعد تحديث الفهرس، يتحقق Cargo من قسم [dependencies] وينزل أي صناديق مدرجة لم يتم تنزيلها بعد. في هذه الحالة، على الرغم من أننا أدرجنا rand فقط كاعتمادية، فقد جلب Cargo أيضاً صناديق أخرى يعتمد عليها rand ليعمل. بعد تنزيل الصناديق، تقوم Rust بتجميعها ثم تجميع المشروع بالاعتماديات المتاحة.

إذا قمت بتشغيل cargo build مرة أخرى فوراً دون إجراء أي تغييرات، فلن تحصل على أي مخرجات سوى سطر Finished. يعرف Cargo أنه قد قام بالفعل بتنزيل وتجميع الاعتماديات، ولم تقم بتغيير أي شيء عنها في ملف Cargo.toml. يعرف Cargo أيضاً أنك لم تغير أي شيء في الكود الخاص بك، لذا فهو لا يعيد تجميعه أيضاً. مع عدم وجود شيء للقيام به، فإنه يخرج ببساطة.

إذا فتحت ملف src/main.rs وأجريت تغييراً تافهاً، ثم حفظته، وقمت بالبناء مرة أخرى، فسترى مخرجات سطرين فقط:

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 2.53s

توضح هذه السطور أن Cargo يقوم فقط بتحديث البناء بتغييرك الصغير في ملف src/main.rs. لم تتغير اعتمادياتك، لذا يعرف Cargo أنه يمكنه إعادة استخدام ما قام بتنزيله وتجميعه بالفعل.

ضمان عمليات بناء قابلة للتكرار باستخدام ملف Cargo.lock

يحتوي Cargo على آلية تضمن إمكانية إعادة بناء نفس الملف الثنائي في كل مرة تقوم فيها أنت أو أي شخص آخر ببناء الكود الخاص بك: سيستخدم Cargo فقط إصدارات الاعتماديات التي حددتها حتى تشير إلى خلاف ذلك. على سبيل المثال، لنفترض أن الأسبوع القادم سيصدر الإصدار 0.8.6 من صندوق rand ويحتوي هذا الإصدار على إصلاح لخلل مهم، ولكنه يحتوي أيضاً على تراجع (regression) سيؤدي إلى كسر الكود الخاص بك. للتعامل مع هذا، تنشئ Rust ملف Cargo.lock في المرة الأولى التي تقوم فيها بتشغيل cargo build لذا لدينا الآن هذا الملف في دليل guessing_game.

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

تحديث صندوق للحصول على إصدار جديد

عندما تريد تحديث صندوق، يوفر Cargo أمر update والذي سيتجاهل ملف Cargo.lock ويكتشف جميع أحدث الإصدارات التي تناسب مواصفاتك في Cargo.toml. سيقوم Cargo بعد ذلك بكتابة تلك الإصدارات في ملف Cargo.lock. وبخلاف ذلك، سيبحث Cargo افتراضياً فقط عن الإصدارات الأكبر من 0.8.5 وأقل من 0.9.0. إذا أصدر صندوق rand إصدارين جديدين 0.8.6 و 0.9.0 فسترى ما يلي إذا قمت بتشغيل cargo update:

$ cargo update
    Updating crates.io index
    Updating rand v0.8.5 -> v0.8.6

يتجاهل Cargo الإصدار 0.9.0. في هذه المرحلة، ستلاحظ أيضاً تغييراً في ملف Cargo.lock يشير إلى أن إصدار صندوق rand الذي تستخدمه الآن هو 0.8.6. لاستخدام الإصدار 0.9.0 من rand أو أي إصدار في سلسلة 0.9.x يجب عليك تحديث ملف Cargo.toml ليبدو كما يلي بدلاً من ذلك (أو الإصدار المناسب إذا كنت تستخدم rand 0.8):

[dependencies]
rand = "0.999.0"

في المرة القادمة التي تقوم فيها بتشغيل cargo build سيقوم Cargo بتحديث سجل الصناديق المتاحة ويعيد تقييم متطلبات rand الخاصة بك وفقاً للإصدار الجديد الذي حددته.

هناك الكثير مما يمكن قوله عن Cargo و نظامه البيئي والذي سنناقشه في الفصل 14، ولكن في الوقت الحالي، هذا كل ما تحتاج إلى معرفته. يجعل Cargo من السهل جداً إعادة استخدام المكتبات، لذا يستطيع مطورو Rust كتابة مشاريع أصغر يتم تجميعها من عدد من الحزم.

توليد رقم عشوائي

دعونا نبدأ في استخدام rand لتوليد رقم للتخمين. الخطوة التالية هي تحديث ملف src/main.rs كما هو موضح في القائمة 2-3.

use std::io;

use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    let mut guess = String::new();

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

    println!("You guessed: {guess}");
}

أولاً، نضيف السطر use rand::Rng;. تحدد سمة (trait) Rng الدوال التي تنفذها مولدات الأرقام العشوائية، ويجب أن تكون هذه السمة في النطاق لنتمكن من استخدام تلك الدوال. سيغطي الفصل العاشر السمات بالتفصيل.

بعد ذلك، نضيف سطرين في المنتصف. في السطر الأول، نستدعي دالة rand::thread_rng التي تعطينا مولد الأرقام العشوائية المحدد الذي سنستخدمه: وهو مولد محلي لخيط التنفيذ الحالي (thread) ويتم تزويده ببذرة (seed) من قبل نظام التشغيل. ثم نستدعي دالة gen_range على مولد الأرقام العشوائية. تم تحديد هذه الدالة بواسطة سمة Rng التي جلبناها إلى النطاق باستخدام عبارة use rand::Rng;. تأخذ دالة gen_range تعبيراً للنطاق كمعامل وتولد رقماً عشوائياً في ذلك النطاق. نوع تعبير النطاق الذي نستخدمه هنا يأخذ الشكل start..=end وهو شامل للحدود الدنيا والعليا، لذا نحتاج إلى تحديد 1..=100 لطلب رقم بين 1 و 100.

ملاحظة: لن تعرف ببساطة أي السمات يجب استخدامها وأي الدوال يجب استدعاؤها من صندوق ما، لذا فإن كل صندوق له توثيق مع تعليمات لاستخدامه. ميزة أخرى رائعة في Cargo هي أن تشغيل أمر cargo doc --open سيبني التوثيق المقدم من جميع اعتمادياتك محلياً ويفتحه في متصفحك. إذا كنت مهتماً بوظائف أخرى في صندوق rand على سبيل المثال، فقم بتشغيل cargo doc --open وانقر على rand في الشريط الجانبي على اليسار.

السطر الجديد الثاني يطبع الرقم السري. هذا مفيد أثناء تطوير البرنامج لنتمكن من اختباره، لكننا سنحذفه من النسخة النهائية. لن تكون اللعبة ممتعة إذا قام البرنامج بطباعة الإجابة بمجرد بدئه!

حاول تشغيل البرنامج عدة مرات:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 7
Please input your guess.
4
You guessed: 4

$ cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 83
Please input your guess.
5
You guessed: 5

يجب أن تحصل على أرقام عشوائية مختلفة، ويجب أن تكون جميعها أرقاماً بين 1 و 100. عمل رائع!

مقارنة التخمين بالرقم السري

الآن بعد أن أصبح لدينا مدخلات المستخدم ورقم عشوائي، يمكننا مقارنتهما. تظهر هذه الخطوة في القائمة 2-4. لاحظ أن هذا الكود لن يتم تجميعه بعد، كما سنوضح.

use std::cmp::Ordering;
use std::io;

use rand::Rng;

fn main() {
    // --snip--
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    let mut guess = String::new();

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

    println!("You guessed: {guess}");

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}

أولاً، نضيف عبارة use أخرى، لجلب نوع يسمى std::cmp::Ordering إلى النطاق من المكتبة القياسية. النوع Ordering هو تعداد آخر وله الحالات Less و Greater و Equal. هذه هي النتائج الثلاث الممكنة عند مقارنة قيمتين.

ثم نضيف خمسة أسطر جديدة في الأسفل تستخدم نوع Ordering. تقارن دالة cmp قيمتين ويمكن استدعاؤها على أي شيء يمكن مقارنته. تأخذ مرجعاً لأي شيء تريد مقارنته به: هنا، تقارن guess بـ secret_number. ثم تعيد حالة من تعداد Ordering الذي جلبناه إلى النطاق باستخدام عبارة use. نستخدم تعبير match لتقرير ما يجب فعله بعد ذلك بناءً على الحالة التي تم إرجاعها من استدعاء cmp بالقيم الموجودة في guess و secret_number.

يتكون تعبير match من أذرع (arms). يتكون الذراع من نمط (pattern) للمطابقة معه، والكود الذي يجب تشغيله إذا كانت القيمة المعطاة لـ match تناسب نمط ذلك الذراع. تأخذ Rust القيمة المعطاة لـ match وتبحث في نمط كل ذراع بالترتيب. الأنماط وبنية match هي ميزات قوية في Rust: فهي تتيح لك التعبير عن مجموعة متنوعة من المواقف التي قد يواجهها الكود الخاص بك، وتتأكد من معالجتها جميعاً. سيتم تغطية هذه الميزات بالتفصيل في الفصل السادس والفصل التاسع عشر على التوالي.

دعونا نمر بمثال مع تعبير match الذي نستخدمه هنا. لنفترض أن المستخدم قد خمن 50 والرقم السري الذي تم توليده عشوائياً هذه المرة هو 38.

عندما يقارن الكود 50 بـ 38، ستعيد دالة cmp القيمة Ordering::Greater لأن 50 أكبر من 38. يحصل تعبير match على القيمة Ordering::Greater ويبدأ في التحقق من نمط كل ذراع. ينظر إلى نمط الذراع الأول Ordering::Less ويرى أن القيمة Ordering::Greater لا تطابق Ordering::Less لذا يتجاهل الكود الموجود في ذلك الذراع وينتقل إلى الذراع التالي. نمط الذراع التالي هو Ordering::Greater والذي يطابق فعلاً Ordering::Greater! سيتم تنفيذ الكود المرتبط في ذلك الذراع وطباعة Too big! على الشاشة. ينتهي تعبير match بعد أول مطابقة ناجحة، لذا لن ينظر إلى الذراع الأخير في هذا السيناريو.

ومع ذلك، لن يتم تجميع الكود الموجود في القائمة 2-4 بعد. دعونا نجرب ذلك:

$ cargo build
   Compiling libc v0.2.86
   Compiling getrandom v0.2.2
   Compiling cfg-if v1.0.0
   Compiling ppv-lite86 v0.2.10
   Compiling rand_core v0.6.2
   Compiling rand_chacha v0.3.0
   Compiling rand v0.8.5
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
error[E0308]: mismatched types
  --> src/main.rs:23:21
   |
23 |     match guess.cmp(&secret_number) {
   |                 --- ^^^^^^^^^^^^^^ expected `&String`, found `&{integer}`
   |                 |
   |                 arguments to this method are incorrect
   |
   = note: expected reference `&String`
              found reference `&{integer}`
note: method defined here
  --> /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/cmp.rs:979:8

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

جوهر الخطأ ينص على وجود أنواع غير متطابقة (mismatched types). تمتلك Rust نظام أنواع قوي وثابت. ومع ذلك، لديها أيضاً استنتاج الأنواع (type inference). عندما كتبنا let mut guess = String::new() تمكنت Rust من استنتاج أن guess يجب أن تكون من نوع String ولم تجبرنا على كتابة النوع. من ناحية أخرى، فإن secret_number هو نوع رقمي. يمكن لعدد قليل من أنواع الأرقام في Rust أن يكون لها قيمة بين 1 و 100: i32 وهو رقم 32 بت؛ u32 وهو رقم 32 بت غير موقع (unsigned)؛ i64 وهو رقم 64 بت؛ بالإضافة إلى أنواع أخرى. ما لم يتم تحديد خلاف ذلك، تفترض Rust افتراضياً النوع i32 وهو نوع secret_number ما لم تضف معلومات النوع في مكان آخر من شأنها أن تجعل Rust تستنتج نوعاً رقمياً مختلفاً. سبب الخطأ هو أن Rust لا يمكنها مقارنة سلسلة نصية ونوع رقمي.

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

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

use std::cmp::Ordering;
use std::io;

use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    // --snip--

    let mut guess = String::new();

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

    let guess: u32 = guess.trim().parse().expect("Please type a number!");

    println!("You guessed: {guess}");

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}

السطر هو:

let guess: u32 = guess.trim().parse().expect("يرجى كتابة رقم!");

ننشئ متغيراً باسم guess. ولكن انتظر، أليس لدى البرنامج بالفعل متغير باسم guess؟ بلى، ولكن لحسن الحظ تسمح لنا Rust بـ “تظليل” (shadow) القيمة السابقة لـ guess بقيمة جديدة. يتيح لنا التظليل (Shadowing) إعادة استخدام اسم المتغير guess بدلاً من إجبارنا على إنشاء متغيرين فريدين، مثل guess_str و guess على سبيل المثال. سنغطي هذا بمزيد من التفصيل في الفصل الثالث ولكن في الوقت الحالي، اعلم أن هذه الميزة غالباً ما تستخدم عندما تريد تحويل قيمة من نوع إلى نوع آخر.

نربط هذا المتغير الجديد بالتعبير guess.trim().parse(). تشير guess في التعبير إلى متغير guess الأصلي الذي يحتوي على المدخلات كسلسلة نصية. ستقوم دالة trim على نسخة String بإزالة أي مسافات بيضاء في البداية والنهاية، وهو ما يجب علينا فعله قبل أن نتمكن من تحويل السلسلة النصية إلى u32 والذي يمكن أن يحتوي فقط على بيانات رقمية. يجب على المستخدم الضغط على مفتاح enter لإرضاء read_line وإدخال تخمينه، مما يضيف حرف سطر جديد إلى السلسلة النصية. على سبيل المثال، إذا كتب المستخدم 5 وضغط على enter فستبدو guess كما يلي: 5\n. يمثل \n “سطر جديد”. (في ويندوز، يؤدي الضغط على enter إلى رجوع العربة وسطر جديد \r\n). تزيل دالة trim حرف \n أو \r\n مما ينتج عنه 5 فقط.

تقوم دالة parse على السلاسل النصية بتحويل السلسلة النصية إلى نوع آخر. هنا، نستخدمها للتحويل من سلسلة نصية إلى رقم. نحتاج إلى إخبار Rust بنوع الرقم الدقيق الذي نريده باستخدام let guess: u32. تخبر النقطتان الرأسيتان (:) بعد guess لغة Rust أننا سنقوم بتوضيح نوع المتغير. تمتلك Rust بعض أنواع الأرقام المدمجة؛ u32 الموضح هنا هو عدد صحيح غير موقع بـ 32 بت. إنه خيار افتراضي جيد لرقم موجب صغير. ستتعلم عن أنواع الأرقام الأخرى في الفصل الثالث.

بالإضافة إلى ذلك، فإن توضيح u32 في هذا البرنامج التجريبي والمقارنة مع secret_number يعني أن Rust ستستنتج أن secret_number يجب أن يكون من نوع u32 أيضاً. لذا، ستكون المقارنة الآن بين قيمتين من نفس النوع!

ستعمل دالة parse فقط على الأحرف التي يمكن تحويلها منطقياً إلى أرقام، وبالتالي يمكن أن تتسبب بسهولة في حدوث أخطاء. إذا كانت السلسلة النصية تحتوي على A👍% على سبيل المثال، فلن تكون هناك طريقة لتحويل ذلك إلى رقم. ولأنها قد تفشل، تعيد دالة parse نوع Result تماماً كما تفعل دالة read_line (التي نوقشت سابقاً في “التعامل مع الفشل المحتمل باستخدام Result). سنعامل هذا الـ Result بنفس الطريقة باستخدام دالة expect مرة أخرى. إذا أعادت parse حالة Err من نوع Result لأنها لم تستطع إنشاء رقم من السلسلة النصية، فسيؤدي استدعاء expect إلى تعطل اللعبة وطباعة الرسالة التي نعطيها إياها. إذا تمكنت parse من تحويل السلسلة النصية إلى رقم بنجاح، فستعيد حالة Ok من نوع Result وستعيد expect الرقم الذي نريده من قيمة Ok.

دعونا نشغل البرنامج الآن:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.26s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 58
Please input your guess.
  76
You guessed: 76
Too big!

رائع! على الرغم من إضافة مسافات قبل التخمين، إلا أن البرنامج لا يزال يدرك أن المستخدم خمن 76. قم بتشغيل البرنامج عدة مرات للتحقق من السلوك المختلف مع أنواع مختلفة من المدخلات: خمن الرقم بشكل صحيح، خمن رقماً مرتفعاً جداً، وخمن رقماً منخفضاً جداً.

لدينا معظم اللعبة تعمل الآن، ولكن يمكن للمستخدم القيام بتخمين واحد فقط. دعونا نغير ذلك بإضافة حلقة تكرار!

السماح بتخمينات متعددة باستخدام الحلقات

تنشئ الكلمة المفتاحية loop حلقة تكرار لانهائية. سنضيف حلقة لمنح المستخدمين المزيد من الفرص لتخمين الرقم:

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

use std::cmp::Ordering;
use std::io;

use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    // --snip--

    println!("The secret number is: {secret_number}");

    loop {
        println!("Please input your guess.");

        // --snip--


        let mut guess = String::new();

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

        let guess: u32 = guess.trim().parse().expect("Please type a number!");

        println!("You guessed: {guess}");

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => println!("You win!"),
        }
    }
}

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

يمكن للمستخدم دائماً مقاطعة البرنامج باستخدام اختصار لوحة المفاتيح ctrl-C. ولكن هناك طريقة أخرى للهروب من هذا الوحش النهم، كما ذكرنا في مناقشة parse في “مقارنة التخمين بالرقم السري”: إذا أدخل المستخدم إجابة ليست رقماً، فسيتحطم البرنامج. يمكننا الاستفادة من ذلك للسماح للمستخدم بالخروج، كما هو موضح هنا:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.23s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 59
Please input your guess.
45
You guessed: 45
Too small!
Please input your guess.
60
You guessed: 60
Too big!
Please input your guess.
59
You guessed: 59
You win!
Please input your guess.
quit

thread 'main' panicked at src/main.rs:28:47:
يرجى كتابة رقم!: ParseIntError { kind: InvalidDigit }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

كتابة quit ستؤدي إلى الخروج من اللعبة، ولكن كما ستلاحظ، فإن إدخال أي مدخلات أخرى ليست رقماً سيفعل الشيء نفسه. هذا ليس مثالياً على أقل تقدير؛ نريد أن تتوقف اللعبة أيضاً عند تخمين الرقم الصحيح.

الخروج بعد التخمين الصحيح

دعونا نبرمج اللعبة لتخرج عندما يفوز المستخدم عن طريق إضافة عبارة break:

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

use std::cmp::Ordering;
use std::io;

use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

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

        let guess: u32 = guess.trim().parse().expect("Please type a number!");

        println!("You guessed: {guess}");

        // --snip--

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

إضافة سطر break بعد You win! تجعل البرنامج يخرج من الحلقة عندما يخمن المستخدم الرقم السري بشكل صحيح. الخروج من الحلقة يعني أيضاً الخروج من البرنامج، لأن الحلقة هي الجزء الأخير من دالة main.

معالجة المدخلات غير الصالحة

لمزيد من تحسين سلوك اللعبة، بدلاً من تحطيم البرنامج عندما يدخل المستخدم شيئاً ليس رقماً، دعونا نجعل اللعبة تتجاهل المدخلات غير الرقمية حتى يتمكن المستخدم من الاستمرار في التخمين. يمكننا القيام بذلك عن طريق تغيير السطر الذي يتم فيه تحويل guess من String إلى u32 كما هو موضح في القائمة 2-5.

use std::cmp::Ordering;
use std::io;

use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        // --snip--

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

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {guess}");

        // --snip--

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

ننتقل من استدعاء expect إلى تعبير match للانتقال من التحطم عند حدوث خطأ إلى معالجة الخطأ. تذكر أن parse تعيد نوع Result و Result هو تعداد له الحالات Ok و Err. نحن نستخدم تعبير match هنا، كما فعلنا مع نتيجة Ordering لدالة cmp.

إذا تمكنت parse من تحويل السلسلة النصية إلى رقم بنجاح، فستعيد قيمة Ok تحتوي على الرقم الناتج. ستطابق قيمة Ok تلك نمط الذراع الأول، وسيعيد تعبير match ببساطة قيمة num التي أنتجتها parse ووضعتها داخل قيمة Ok. سينتهي هذا الرقم تماماً حيث نريده في متغير guess الجديد الذي ننشئه.

إذا لم تتمكن parse من تحويل السلسلة النصية إلى رقم، فستعيد قيمة Err تحتوي على مزيد من المعلومات حول الخطأ. لا تطابق قيمة Err نمط Ok(num) في ذراع match الأول، ولكنها تطابق نمط Err(_) في الذراع الثاني. الشرطة السفلية _ هي قيمة شاملة (catch-all)؛ في هذا المثال، نقول إننا نريد مطابقة جميع قيم Err بغض النظر عن المعلومات الموجودة بداخلها. لذا، سيقوم البرنامج بتنفيذ كود الذراع الثاني continue والذي يخبر البرنامج بالانتقال إلى التكرار التالي من الحلقة loop وطلب تخمين آخر. وبذلك، يتجاهل البرنامج فعلياً جميع الأخطاء التي قد تواجهها parse!

الآن يجب أن يعمل كل شيء في البرنامج كما هو متوقع. دعونا نجربه:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 61
Please input your guess.
10
You guessed: 10
Too small!
Please input your guess.
99
You guessed: 99
Too big!
Please input your guess.
foo
Please input your guess.
61
You guessed: 61
You win!

رائع! مع تعديل نهائي صغير واحد، سننهي لعبة التخمين. تذكر أن البرنامج لا يزال يطبع الرقم السري. كان ذلك مفيداً للاختبار، لكنه يفسد اللعبة. دعونا نحذف سطر println! الذي يطبع الرقم السري. تظهر القائمة 2-6 الكود النهائي.

use std::cmp::Ordering;
use std::io;

use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

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

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {guess}");

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

في هذه المرحلة، تكون قد نجحت في بناء لعبة التخمين. تهانينا!

ملخص

كان هذا المشروع طريقة عملية لتعريفك بالعديد من مفاهيم Rust الجديدة: let و match والدوال واستخدام الصناديق الخارجية والمزيد. في الفصول القليلة القادمة، ستتعلم عن هذه المفاهيم بمزيد من التفصيل. يغطي الفصل الثالث المفاهيم التي تمتلكها معظم لغات البرمجة، مثل المتغيرات وأنواع البيانات والدوال، ويوضح كيفية استخدامها في Rust. يستكشف الفصل الرابع الملكية (ownership)، وهي ميزة تجعل Rust مختلفة عن اللغات الأخرى. يناقش الفصل الخامس الهياكل (structs) وصيغة الدوال، ويشرح الفصل السادس كيفية عمل التعدادات (enums).