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

برمجة لعبة تخمين الأرقام بلغة رست Rust


Naser Dakhel

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

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

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

اذهب إلى مجلد "directory" الذي أنشأناه في المقال السابق لإعداد المشروع الجديد باستخدام أداة كارجو Cargo كما يلي:

$ cargo new guessing_game
$ cd guessing_game

يأخذ الأمر الأول cargo new اسم المشروع، وهو في حالتنا "guessing_game" مقل وسيطٍ أول، بينما ينتقل الأمر الثاني إلى مجلد المشروع.

ألقِ نظرةً إلى محتويات الملف "Cargo.toml" الناتج:

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

# يمكنك رؤية المزيد من المفاتيح من الرابط‫ https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

كما رأينا في المقال السابق، يولّد الأمر cargo new برنامج "!Hello, world" لك. ألقِ نظرةً على محتويات ملف "src/main.rs":

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

دعنا الآن نصرّف هذا البرنامج ونشغله باتباع نفس الخطوة وباستخدام أمر cargo run:

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

تبرز أهمية الأمر run عندما تريد أن تفحص التغييرات في مشروعك تباعًا وأن تفحص البرنامج بسرعة بعد كل إضافة قبل المضيّ قدمًا للإضافة التالية وهذا ما سنفعله بالضبط في لعبتنا هذه.

أعِد الآن فتح الملف "src/main.rs"، إذ سنكتب الشيفرة البرمجية لمشروعنا فيه.

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

الجزء الأول من برنامج لعبة التخمين هو سؤال المستخدم ليدخل التخمين، ومن ثم معالجة هذا الدخل والتحقق من أنه بتنسيق مناسب. دعنا بدايةً نسمح المستخدم بإدخال تخمين. اكتب الشيفرة التالية في الملف "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}");
}

[شيفرة 2-1: شيفرة برمجية تأخذ التخمين من المستخدم في الدخل وتطبعه]

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

use std::io;

تحتوي رست افتراضيًا على مجموعةٍ معرفةٍ من العناصر ضمن المكتبة القياسية التي تُضاف إلى نطاق أي برنامج، وتُسمى هذه العناصر باسم المقدمة prelude ويمكنك رؤية جميع محتوياتها في توثيق المكتبة القياسية.

إذا أردت استخدام نوع محدد غير متواجد في المقدمة، فعليك إضافته إلى النطاق عن طريق استخدام تعليمة use. تتيح لك مكتبة std::io استخدام عدد من المزايا المفيدة منها القدرة على تلقّي دخل المستخدم.

دالة main هي النقطة التي يبدأ منها البرنامج كما رأينا في المقال السابق:

fn main() {

تصرّح الصيغة fn عن دالة جديدة وتُشير الأقواس () إلى أن الدالة لا تأخذ أي معاملات، ويُشير القوس المعقوص } إلى بداية متن الدالة.

تشير println!‎ إلى ماكرو كما تطرقنا إلى ذلك في المقال السابق ويطبع هذا الماكرو السلسلة النصية إلى الشاشة:

    println!("Guess the number!");

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

تطبع الشيفرة السابقة جملةً تدلّ المستخدم على ماهية اللعبة ومن ثم جملة تطلب منه إدخالًا.

تخزين القيم والمتغيرات

نُنشئ الآن متغيرًا لتخزين دخل المستخدم كما يلي:

let mut guess = String::new();

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

let apples = 5;

يُنشئ هذا السطر متغيرًا جديدًا باسم apples ويُسنده إليه القيمة 5. المتغيرات في لغة رست ثابتة immutable افتراضيًا، وهذا يعني أن المتغير سيحافظ على قيمته الأولية التي أُسندت إليه ولن تُغيّر وسنتحدث في هذا الموضوع بتوسع أكثر لاحقًا. لجعل المتغير قابلًا للتغيير mutable نستخدم الكلمة المفتاحية mut قبل اسم المتغير:

let apples = 5; // ثابت
let mut bananas = 5; // متغيّر

لاحظ أن // تتسبب ببدء تعليق سطري ينتهي بنهاية السطر وتتجاهل رست كل ما ورد ضمن التعليق، وسنناقش التعليقات لاحقًا بتوسع أكبر.

بالعودة إلى برنامج لعبة التخمين، فأنت تعلم الآن أن let mut guess سيُضيف متغيرًا يقبل التغيير باسم guess، وتُخبر إشارة المساواة = رست أنّنا نريد إسناد قيمة ما إلى المتغير، يقع على يمين إشارة المساواة القيمة التي نريد إسنادها إلى guess وهي قيمةٌ ناتجةٌ عن استدعاء الدالة String::new وهي دالة تُعيد نُسخةً instance من النوع "String"؛ وهو نوع من أنواع السلاسل النصية الموجود في المكتبة القياسية وهو نص بترميز UTF-8 وقابل للزيادة.

تُشير :: في السطر new:: إلى أن new مرتبطةٌ بدالة من نوع String؛ والدالة المرتبطة associated function هي دالة تُطبّق على نوع ما -وفي هذه الحالة هو String- وتُنشئ الدالة new هذه سلسلةً نصيةً جديدةً وفارغة، وستجد دالة new هذه في العديد من الأنواع لأنه اسم شائع لدالة تُنشئ قيمةً جديدةً من نوعٍ ما.

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

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

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

io::stdin()
        .read_line(&mut guess)

يمكننا استخدام استدعاء الدالة السابقة حتى لو لم نستورد مكتبة io بكتابتنا use std::io في بداية البرنامج ولكن الاستدعاء حينها سيكون بالشكل std::io::stdin. تُعيد الدالة stdin نسخة من النوع std::io::Stdin وهو نوع يُمثّل مُعالجًا للدخل القياسي من الطرفية.

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

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

التعامل مع الأخطاء الممكنة باستخدام نوع النتيجة Result

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

.expect("Failed to read line");

كان بإمكانك كتابة السطر البرمجي على النحو التالي:

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

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

تضع الدالة read_line كل ما يكتبه المستخدم إلى السلسلة النصية التي نمررها لها كما ذكرنا سابقًا، إلا أنها تُعيد أيضًا قيمة 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: `#[warn(unused_must_use)]` on by default
   = note: this `Result` may be an `Err` variant, which should be handled

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

يحذرك رست هنا أنك لم تستخدم قيمة Result المُعادة من التابع read_line، مما يعني أن البرنامج لن يستطيع التعامل مع الأخطاء ممكنة الحدوث.

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

طباعة القيم باستخدام مواضع println!‎ المؤقتة

هناك سطر واحد متبقي لمناقشته -بتجاهل الأقواس المعقوصة- ألا وهو:

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

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

let x = 5;
let y = 10;

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

سنحصل بعد تنفيذ الشيفرة السابقة على الخرج x = 5 and y = 10.

التأكد من عمل الجزء الأول

دعنا نتأكد من عمل الجزء الأول من لعبة التخمين، لذلك شغّل الشيفرة البرمجية باستخدام الأمر cargo run:

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

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

توليد الرقم السري

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

استخدام صندوق ما للحصول على إمكانيات أكبر

تذكر أن الصندوق هو مجموعة من ملفات رست المصدرية، إذ يمثّل المشروع الذي نبنيه الآن صندوقًا ثنائيًا binary crate أي أنه ملف تنفيذي، بينما يمثل صندوق rand صندوق مكتبة library crate، أي أنه يحتوي شيفرة مصدرية مكتوبة لتُستخدم في برامج أخرى ولا يمكن تشغيلها بصورةٍ مستقلة.

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

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

rand = "0.8.3"

كُل ما يتبع ترويسة القسم في ملف "Cargo.toml" هو جزءٌ من قسم ما يستمر حتى بداية القسم الآخر، وفي القسم [dependencies] أنت تُعلم كارجو بالصناديق الخارجية التي يعتمد مشروعك عليها وأي إصدار منها يتطلّب، ونحدّد في حالتنا هذه الصندوق rand ذو الإصدار "0.8.3"، ويفهم كارجو الإدارة الدلالية لنسخ البرمجيات Semantic Versioning -أو اختصارًا SemVer- وهي صيغة قياسية لكتابة أرقام الإصدارات، وفي الحقيقة فإن الرقم "0.8.3" هو اختصارٌ للرقم "‎^0.8.3"، وهو يعني أن أي إصدار مسموح هو "0.8.3" على الأقل و"0.9.0" على الأكثر.

يضع كارجو في الحسبان أن هذه الإصدارات تحتوي على واجهات برمجية عامة public APIs متوافقة مع الإصدار "0.8.3" ويضمن ذلك التحديد أنك ستحصل على آخر الإصدارات المتوافقة مع الشيفرة البرمجية في هذا المقال، إذ من غير المضمون أن تكون الإصدارات المساوية إلى "0.9.0" أو أعلى تحتوي على ذات الواجهة البرمجية التي نتبعها في الأمثلة هنا.

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

$ cargo build
    Updating crates.io index
  Downloaded rand v0.8.3
  Downloaded libc v0.2.86
  Downloaded getrandom v0.2.2
  Downloaded cfg-if v1.0.0
  Downloaded ppv-lite86 v0.2.10
  Downloaded rand_chacha v0.3.0
  Downloaded rand_core v0.6.2
   Compiling rand_core v0.6.2
   Compiling libc v0.2.86
   Compiling getrandom v0.2.2
   Compiling cfg-if v1.0.0
   Compiling ppv-lite86 v0.2.10
   Compiling rand_chacha v0.3.0
   Compiling rand v0.8.3
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 2.53s

[شيفرة 2-2: الخرج الناتج من تنفيذ الأمر cargo build بعد إضافة صندوق rand مثل اعتمادية]

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

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

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

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

إذا فتحت الملف "src/main.rs" وعدّلت تعديلًا بسيطًا ومن ثمّ حفظته وحاولت إعادة بناء المشروع، فستجد السطرين التاليين فقط في الخرج:

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

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

التأكد من أن المشاريع يمكن إعادة إنتاجها باستخدام ملف Cargo.lock

لدى كارجو آلية تتحقق من أنك تستطيع إعادة بناء الأداة كل مرة تبني أنت أو شخص آخر شيفرتك البرمجية، يستخدم كارجو فقط إصدارات الاعتماديات التي حددتها إلا إذا حددت عكس ذلك. على سبيل المثال، لنقل أن إصدار 0.8.4 من صندوق rand سيُطلق الأسبوع القادم، ويتضمن هذا الإصدار تصحيحًا مهمًا لمشكلة ما إلا أنه يحتوي أيضًا على تراجع regression، وسيتسبب هذا بتعطل شيفرتك البرمجية؛ تُنشئ رست في هذه الحالة ملفًا يدعى "Cargo.lock" عند أول تنفيذ للأمر cargo build ويقع هذا الملف في مجلد "guessing_game".

يوجِد كارجو جميع إصدارات الاعتماديات التي تلائم مشروعك عندما تبنيه للمرة الأولى، ومن ثم يكتب الإصدارات إلى ملف "Cargo.lock"، وبالتالي سيجد كارجو عندما تبني مشروعك في المستقبل أن الملف "Cargo.lock" موجود وسيستخدم عندها الإصدارات المحددة في ذلك الملف بدلًا من إيجاد الإصدارات المناسبة مجددًا، ويسمح لك هذا بالحصول على نسخة من المشروع قابلة لإعادة الإنتاج تلقائيًا، وبكلمات أخرى، سيظلّ مشروعك معتمدًا على الإصدار "0.8.3" حتى تقرّر التحديث إلى إصدار آخر بصورةٍ صريحة، ويعود الشكر إلى ملف "Cargo.lock" في ذلك. بما أن ملف "Cargo.lock" مهم للحصول على نسخ قابلة لإعادة الإنتاج، فمن الشائع أن يُضاف إلى نظام التحكم بالإصدارات version control مع باقي الشيفرة المصدرية في مشروعك.

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

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

$ cargo update
    Updating crates.io index
    Updating rand v0.8.3 -> v0.8.4

يتجاهل كارجو الإصدار "0.9.0"، وستلاحظ أيضًا بحلول هذه النقطة أن ملف "Cargo.lock" يُشير إلى أن الإصدار الحالي من صندوق rand هو "0.8.4"؛ ولاستخدام الإصدار "0.9.0" من rand أو أي إصدار آخر من السلسلة "x.‏0.9"K عليك تحديث ملف "Cargo.toml" ليبدو على النحو التالي:

[dependencies]
rand = "0.9.0"

سيحدّث كارجو في المرة القادمة التي تنفذ فيها الأمر cargo build مسجّل الصناديق المتاحة ويُعيد تقييم متطلبات rand حسب الإصدار الجديد الذي حدّدته.

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

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

دعنا نبدأ باستخدام 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}");
}

[شيفرة 2-3: إضافة شيفرة برمجية لتوليد الرقم العشوائي]

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

ثم نُضيف سطرين في وسط البرنامج، إذ نستدعي في السطر الأول الدالة rand::thread_rng التي تُعطينا مولّد رقم عشوائي معيّن سنستخدمه وهو مولد محلي لخيط التنفيذ الحالي ويُبذر seeded بواسطة نظام التشغيل، ونستدعي بعدها التابع gen_range على مولد الأرقام العشوائية، التابع السابق معرّف بالسمة Rng التي أضفناها إلى النطاق باستخدام التعليمة use rand::Rng. يأخذ التابع gen_rang تعبير مجال range expression مثل وسيط، ويولّد رقمًا ينتمي إلى ذلك المجال، إذ نستخدم تعبير المجال من النوع ذو التنسيق start..=end، ويتضمن المجال الحد الأعلى والأدنى داخله، لذا بكتابتنا للمجال "‎1..=100" فنحن نحدّد الأعداد بين 1 و100.

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

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

جرّب تنفيذ البرنامج عدة مرات:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 2.53s
     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 [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 هذه الخطوة، لاحظ أن الشيفرة البرمجية لن تُصرّف بنجاح بعد كما سنوضح لاحقًا.

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

use rand::Rng;
use std::cmp::Ordering;
use std::io;

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!"),
    }
}

[شيفرة 2-4: التعامل مع الحالات المُمكنة ضمن عملية المقارنة]

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

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

يتألف تعبير match من أذرع arms، ويتألف كل ذراع من نمط يُستخدم في عملية المقارنة والشيفرة البرمجية التي يجب أن تعمل في حال كانت القيمة المُعطاة إلى match توافق نمط الذراع. تأخذ رست القيمة المُعطاة إلى match وتنظر إلى كلّ نمط ذراع. تُعدّ الأذرع وبنية‏ match من أبرز مزايا رست، إذ تسمح لك بالتعبير عن عدّة حالات قد تحدث ضمن شيفرتك البرمجية وأن تتأكد من معالجتها جميعًا، وسنغطّي هذه المزايا بتعمُّق أكبر لاحقًا.

دعنا نوضح مثالًا عن تعبير match نستخدمه هنا؛ لنقُل أن المستخدم قد خمّن القيمة 50 وأن الرقم العشوائي السري المولّد كان 38، تُقارن شيفرتنا البرمجية القيمة 50 إلى 38 ويُعيد التابع cmp في هذه الحالة القيمة Ordering::Greater لأن 50 أكبر من 38، ويتلقّى التعبير match القيمة Ordering::Greater ويبدأ بتفقد كل نمط ذراع، إذ يُنظر إلى نمط الذراع الأولى وهو Ordering::Less وهي قيمةٌ لا توافق القيمة Ordering::Greater وبالتالي يجري تجاهل الشيفرة البرمجية ضمن الذراع وينتقل إلى نمط الذراع الأخرى وهو 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.3
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
error[E0308]: mismatched types
  --> src/main.rs:22:21
   |
22 |     match guess.cmp(&secret_number) {
   |                     ^^^^^^^^^^^^^^ expected struct `String`, found integer
   |
   = note: expected reference `&String`
              found reference `&{integer}`

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

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

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

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

use rand::Rng;
use std::cmp::Ordering;
use std::io;

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("Please type a number!");

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

نُسند المتغير الجديد إلى التعبير guess.trim().parse()‎، إذ تشير guess ضمن التعبير إلى المتغير guess الأصلي الذي يحتوي على الدخل (سلسلة نصية string). يحذف التابع trim عند استخدامه ضمن نسخة من النوع String أي مسافات فارغة whitespaces في بداية ونهاية السلسلة النصية، وهو أمر لازم الحدوث قبل أن نحوّل السلسلة النصية إلى النوع u32 الذي يمكن أن يحتوي فقط على قيمة عددية، وذلك لأن المستخدم يضغط على زرّ الإدخال enter لإنهاء عمل التابع read_line بعد إدخال تخمينه مما يُضيف محرف سطر جديد إلى السلسلة النصية؛ فعلى سبيل المثال، إذا أدخل المستخدم 5 وضغط على زر الإدخال، فستأخذ السلسلة النصية guess القيمة "‎5\n"، إذ يمثل المحرف "‎\n" محرف سطر جديد (يتسبب الضغط على زر الإدخال في أنظمة ويندوز برجوع السطر وإضافة سطر جديد "‎\r\n") ويُزيل التابع trim المحرف "‎\n" أو "‎\r\n" ونحصل بالتالي على "5" فقط.

يحول التابع parse السلاسل النصية إلى نوع آخر، ونستخدمه هنا لتحويل السلسلة النصية إلى عدد، وعلينا إخبار رست بتحديد النوع الذي نريد التحويل إليه باستخدام let guess: u32، إذ تُشير النقطتان ":" الموجودتان بعد guess إلى أننا سنحدد نوع المتغير بعدها. تحتوي رست على عدد من الأنواع العددية المُضمّنة built-in منها u32 الذي استخدمناه هنا وهو نوع عدد صحيح عديم الإشارة بطول 32-بت وهو خيار افتراضي جيّد للقيم الموجبة الصغيرة، وستتعلّم لاحقًا عن الأنواع العددية الأخرى. إضافةً إلى ما سبق، تعني u32 في مثالنا والمقارنة مع secret_number أن رست سيستنتج أن secret_number يجب أن تكون من النوع u32 أيضًا، لذا أصبحت المقارنة الآن بين قيمتين من النوع ذاته.

يعمل التابع parse فقط على المحارف التي يمكن أن تُحوَّل منطقيًا إلى أعداد، لذلك من الشائع أن يتسبّب بأخطاء؛ فعلى سبيل المثال لن يستطيع التابع التحويل السلسلة النصية إلى نوع عددي إذا كانت تحتوي على القيمة "A?%‎" ولهذا السبب فإن التابع parse يُعيد أيضًا النوع Result بصورةٍ مماثلة للتابع read_line، الذي ناقشناه سابقًا في فقرة "التعامل مع الأخطاء الممكنة باستخدام النوع Result"، وسنتعامل مع النوع Result هذا بطريقة مماثلة باستخدام تابع expect مجددًا. إذا أعاد التابع parse متغاير Result المتمثل بالقيمة Err فهذا يعني أنه لم يستطع التحويل إلى نوع عددي من السلسلة النصية، وفي هذه الحالة، سيوقف استدعاء expect اللعبة وستُطبع الرسالة التي نمررها له، بينما يُعيد متغاير Result ذو القيمة Ok إذا استطاع تحويل القيمة بنجاح من نوع سلسلة نصية إلى نوع عددي، وتُعيد عندها expect العدد الذي نريده من قيمة Ok.

دعنا نشغّل البرنامج الآن.

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

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

تعمل اللعبة لدينا الآن جيدًا، إلا أن المستخدم يمكنه التخمين مرةً واحدةً فقط، دعنا نغيّر من ذلك بإضافة حلقة تكرارية loop.

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

تُنشئ الكلمة المفتاحية loop حلقةً تكراريةً لا نهائية، وسنستخدم الحلقة هنا بهدف منح المستخدم فرصًا أكبر في تخمين العدد:

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

use rand::Rng;
use std::cmp::Ordering;
use std::io;

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!"),
        }
    }
}

نقلنا محتوى البرنامج من تلقي الدخل guess إلى ما بعده لداخل الحلقة. تأكد من محاذاة السطور الموجودة داخل الحلقة التكرارية بمقدار أربع مسافات فارغة، وشغّل البرنامج مجددًا. سيسألك البرنامج الآن عن تخمين جديد إلى ما لا نهاية وهذه مشكلةٌ جديدة، إذ لن يستطيع المستخدم الخروج من البرنامج في هذه الحالة.

يمكن للمستخدم إيقاف البرنامج قسريًا عن طريق استخدام اختصار لوحة المفاتيح "ctrl-c"، إلا أن هناك طريقة أخرى للهروب من هذا البرنامج الذي لا يشبع، إذ يمكن للمستخدم أن يُدخل قيمة غير عددية كما ذكرنا في القسم "مقارنة التخمين إلى الرقم السري" الذي يناقش استخدام parse ويتسبب ذلك بتوقف البرنامج، ويمكننا الاستفادة من ذلك الأمر بالسماح لمستخدمنا بمغادرة البرنامج كما هو موضح هنا:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 1.50s
     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 'Please type a number!: ParseIntError { kind: InvalidDigit }', src/main.rs:28:47
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

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

مغادرة اللعبة بعد إدخال التخمين الصحيح

دعنا نبرمج اللعبة بحيث نُغادر منها عند فوز المستخدم بإضافة تعليمة break:

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

use rand::Rng;
use std::cmp::Ordering;
use std::io;

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.

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

use rand::Rng;
use std::cmp::Ordering;
use std::io;

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;
            }
        }
    }
}

[شيفرة 2-5: تجاهل تخمين غير عددي وسؤال المستخدم عن تخمين آخر بدلًا من إيقاف البرنامج]

بدّلنا استدعاء التابع 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(_)‎ في الذراع الثانية، وترمز الشرطة السفلية _ إلى الحصول على جميع القيم الممكنة، وفي مثالنا هذا فنحن نقول أننا نريد أن نطابق جميع قيم Err الممكنة بغض النظر عن المعلومات الموجودة داخلها، وبالتالي سينفذ البرنامج الذراع الثانية التي تتضمن على continue التي تخبر البرنامج بالذهاب إلى الدورة الثانية من الحلقة loop وأن تسأل المستخدم عن تخمينٍ آخر، لذا أصبح برنامجنا يتجاهل جميع أخطاء parse الممكنة بنجاح.

يجب أن تعمل جميع أجزاء البرنامج كما هو متوقّع، لنجرّبه:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 4.45s
     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 محتوى البرنامج النهائي.

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

use rand::Rng;
use std::cmp::Ordering;
use std::io;

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;
            }
        }
    }
}

[شيفرة 2-6: الشيفرة البرمجية للعبة التخمين كاملةً]

ملخص

أنهينا بالوصول إلى هذه النقطة لعبة التخمين كاملةً، تهانينا.

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

ترجمة -وبتصرف- لفصل Programming a Guessing Game من كتاب 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.


×
×
  • أضف...