مقدمه

در این فصل قصد داریم زبان برنامه‌نویسی Rust را از طریق یک پروژه‌ی واقعی تجربه کنیم. در ضمن این کار با تعدادی از مفاهیم مربوط به زبان Rust و نحوه‌ی استفاده از آنها در عمل آشنا می‌شویم. البته بررسی رسمی و ذکر جزئیات بیشتر در مورد مفاهیم معرفی شده در این فصل را به فصول بعدی موکول می‌کنیم. پروژه‌ی مورد نظر ما یک مسئله‌ی کلاسیک مقدماتی در برنامه‌نویسی است که با نام بازی حدس عدد (number guessing)‌ شناخته می‌شود. کارکرد این برنامه به این صورت است که ابتدا برنامه یک عدد صحیح تصادفی بین 1 تا 100 تولید می‌کند و سپس از بازیکن خواسته می‌شود که این عدد را حدس بزند. اگر حدس او صحیح باشد، یک پیغام تبریک برای وی نمایش داده خواهد شد و در غیر این صورت، پیغامی نمایش داده خواهد شد که مشخص می‌کند عددی که حدس زده از عدد مورد نظر کوچکتر است یا بزرگتر.

ایجاد و تنظیم یک پروژه جدید

طبیعتاً در بدو کار باید یک پروژه‌ی جدید ایجاد کنیم. پس به دایرکتوری projects که در فصل اول ایجاد کردیم، بروید و مانند زیر یک پروژه‌ی جدید بسازید:

$ cargo new guessing_game
$ cd guessing_game

دستور اول پروژه‌ای با نام guessing_game ایجاد می‌کند و دستور دوم ما را به دایرکتوری مربوط به این پروژه منتقل می‌کند.

اکنون نگاهی به فایل Cargo.toml تولید شده توسط کامپایلر بیندازید.

Cargo.toml
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"
            
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
            
[dependencies]

چنانچه اطلاعات هویتی که Cargo از محیط شما بدست آورده نادرست است، آن را اصلاح کرده و مجدداً این فایل را ذخیره کنید.

در فصل اول دیدیم که دستور cargo new منجر به ایجاد یک پروژه‌ی Hello, world! برای ما می‌شود. برای اطمینان از این موضوع، فایل main.rs درون دایرکتوری src را بررسی کنید.

Copy Icon src/main.rs
fn main() {
  println!("Hello, world!");
}

اکنون با استفاده از دستور cargo run این برنامه را کامپایل و اجرا می‌کنیم.

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

همانطور که می‌بینید، این دستور باعث می‌شود تا برنامه‌ی ما ابتدا کامپایل و سپس اجرا شود. اکنون یک بار دیگر فایل 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);
}

این کد شامل اطلاعات زیادی است. پس اجازه دهید خط به خط جلو برویم.

در Rust مثل هر زبان برنامه‌نویسی دیگر، اعمالی مانند I/O و Networking نه در هسته‌ی زبان بلکه به عنوان بخشی از کتابخانه‌ی استاندارد این زبان ارائه می‌شوند. کتابخانه‌ی استاندارد Rust که std نام دارد، شامل ماژولی است با نام io که قابلیت‌های لازم برای اعمال I/O را ارائه می‌دهد. در اولین خط از کد بالا، این ماژول را با استفاده از یک دایرکتیو use به پروژه وارد کرده‌ایم.

use std::io;

کتابخانه استاندارد Rust دارای یک زیرمجموعه‌ی کوچک با نام prelude است که ماژول‌هایی را شامل است که به طور خودکار و ضمنی به هر پروژه‌ای اضافه می‌شوند. اگر به ماژولی از std نیاز داشته باشیم که بخشی از prelude نباشد، باید آن ماژول را به شکلی که در بالا می‌بینید، به پروژه اضافه کنیم.

در فصل اول هم اشاره کردیم که تابع main نقطه‌ی ورود یا Entry point برنامه‌های اجرایی Rust محسوب می‌شود و هر کدی که باید در نهایت اجرا شود، باید درون این تابع نوشته شود.

fn main() {

}

کلمه کلیدی fn‌ برای تعریف یا اعلان یک تابع به کار می‌رود و پرانتزهای خالی به این معناست که این تابع هیچ پارامتر ورودی ندارد و آکلاد باز نشان‌دهنده‌ی شروع بدنه‌ی تابع و آکلاد بسته نشان‌دهنده‌ی پایان بدنه است.

یادآوری می‌کنم که println! یک ماکروست که یک رشته‌ی متنی را در صفحه چاپ می‌کند.

println!("Guess the number!");

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

با این حساب، این کد باعث چاپ عنوان بازی و درخواست وارد کردن یک عدد از کاربر می‌شود.

ذخیره مقدار در متغیرها

پس از دریافت ورودی از کاربر باید آن را در محلی ذخیره کنیم. برای این کار به یک متغیر نیاز داریم. در Rust بر خلاف اکثر زبان‌های دیگر، متغیرها در حالت پیش‌فرض تغییرناپذیر (immutable) هستند؛ یعنی وقتی مقداری به یک متغیر دادیم، امکان تغییر آن مقدار و اعطای یک مقدار جدید به آن متغیر وجود ندارد. تیم توسعه‌ی Rust برای این امر دلایل خوبی دارند که در فصل سوم به آنها اشاره خواهیم کرد. اما فعلاً همینقدر بدانید که متغیرهای Rust در حالت پیش‌فرض تغییرناپذیرند اما می‌توانیم با استفاده از کلمه کلیدی mut این رفتار پیش‌فرض را تغییر داده و یک متغیر تغییرپذیر (mutable) ایجاد کنیم.

let mut guess = String::new();

مقداری که به متغیر guess اختصاص داده‌ایم، یک رشته‌ی خالی و به بیان فنی‌تر یک نمونه یا instance از یک نوع (type) با نام String است. سینتکس بالا از نام یک نوع و یک جفت کارکتر دونقطه (double colon) و نام یک تابع یعنی new تشکیل شده و به این معناست که این تابع یک Associated Function برای نوع String است. توابع associated روی یک نوع پیاده‌سازی و فراخوانی می‌شوند نه روی یک شیء یا نمونه. از این توابع در زبان‌های دیگر با نام توابع یا متدهای استاتیک یاد می‌شود.

برای بسیاری از نوع‌ها (و نه همه) یک تابع new وجود دارد. در واقع، فراخوانی این تابع روی هر نوع به منزله‌ی ایجاد یک مقدار جدید از همان نوع است. بنابراین، در اینجا استفاده از تابع new روی نوع String باعث ایجاد یک رشته‌ی متنی خالی می‌شود. در مجموع می‌توان گفت که خط let mut guess = String::new(); یک متغیر رشته‌ای تغییرپذیر ایجاد می‌کند که در حال حاضر مقداری ندارد.

در ادامه، به شکل زیر تابع stdin را از ماژول io فراخوانی کرده‌ایم:

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

اگر ما خط use std::io را در ابتدای برنامه وارد نکرده بودیم، باید این تابع را به صورت std::io::stdin() فراخوانی می‌کردیم. تابع stdin یک instance یا نمونه از std::io::stdin را برمی‌گرداند که نوعی است که امکان کنترل ورودی کنسول (standard input) را به ما می‌دهد.

سپس متد read_line روی stdin فراخوانی شده تا ورودی را از کاربر دریافت کند. در ضمن، یک آرگومان با نام &mut guess نیز به این متد پاس شده است. کار read_line این است که آنچه را که کاربر وارد می‌کند، دریافت کرده و آنرا به یک رشته الحاق (append) کند. رشته‌ای که باید عبارت ورودی به آن الحاق شود، آرگومان این متد است. با این حساب، علت استفاده از کلمه کلیدی mut در تعریف این آرگومان نیز واضح است: این آرگومان باید تغییرپذیر باشد تا متد read_line بتواند مقداری را به آن الحاق کند و آنرا تغییر دهد.

اما در تعریف این پارامتر، یک کاراکتر & نیز دیده می‌شود. کاراکتر &‌ مشخص می‌کند که این آرگومان یک ارجاع (reference) است. رفرنس‌ها این امکان را فراهم می‌کنند تا چندین بخش از کد بتوانند به یک بخش از داده‌ها دسترسی داشته باشند، بدون اینکه نیاز به کپی چندباره‌ی آن داده‌ها در حافظه باشد. رفرنس یک ویژگی پیچیده است و یکی از مزایای اصلی Rust که آن را به یک زبان امن و پایدار تبدیل کرده، همین ویژگی است. برای به پایان رساندن این برنامه نیازی به دانستن اطلاعات زیادی در مورد رفرنس‌ها ندارید و تنها چیزی که باید بدانید این است که رفرنس‌ها نیز مانند متغیرها به طور پیش‌فرض تغییرناپذیر هستند. به همین دلیل است که ما در اینجا به جای &guess باید بنویسیم &mut guess. در فصل چهارم در مورد جزئیات رفرنس‌ها صحبت می‌کنیم.

مدیریت خطای احتمالی با Result

در زبان Rust مدیریت خطا با آنچه که در اکثر زبان‌های دیگر دیده‌ایم، فرق دارد. اینجا خبری از مکانیزم‌هایی مثل try … catch و مفاهیمی مثل Exception نیست. جزئیات مربوط به مدیریت خطا در Rust در فصل نهم ارائه خواهد شد.

در Rust برای مدیریت خطاهایی که اصطلاحاً Recoverable یا قابل بازیابی هستند از نوع‌های Result استفاده می‌شود. چون این نوع‌ها در ماژول‌های مختلف هستند، می‌توانند نام یکسان Result را داشته باشند. برای مثال، نوع عمومی Result با نوع io::Result فرق دارد اما کارکرد هر دو این است که گروه مشخصی از خطاها را هندل کنند.

هر نوع Result یک enum است که دارای دو variant یا دو مقدار ثابت با نام‌های OK و Err است. مقدار Ok نشان‌دهنده‌ی موفقیت‌آمیز بودن عملیات است و درون آن مقداری است که با موفقیت تولید شده است و مقدار Err‌ به معنای شکست عملیات است و شامل اطلاعاتی در مورد علت و نحوه‌ی شکست عملیات است. از آنجایی که خواندن ورودی از کنسول یک عمل مستعد خطاست، تابع read_line یک io::Result برمی‌گرداند. بنابراین، قبل از اینکه از خروجی این تابع هر استفاده‌ای بشود، باید Ok یا Err بودن آن مشخص شود. یک راه ساده برای حل این مسئله این است که از متد expect استفاده کنیم.

.expect("Failed to read line");

اگر این نمونه از io::Result یک مقدار Err باشد، متد expect موجب می‌شود تا برنامه اصطلاحاً crash شده و پیامی که به عنوان آرگومان expect تعیین کرده‌ایم، نمایش داده شود. اگر متد read_line یک Err برگرداند، به احتمال زیاد این امر نتیجه‌ی بروز یک خطا در سیستم‌عامل است. اگر این نمونه از io::Result یک مقدار Ok باشد، متد expect مقدار برگشتی که Ok نگه داشته را دریافت کرده و فقط مقداری را که می‌توانید استفاده کنید، برای شما برمی‌گرداند که در اینجا تعداد بایت‌هایی است که کاربر در stdout وارد کرده است.

اگر متد 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

این هشدار بیان می‌کند که شما از مقدار Result برگردانده شده از read_line استفاده نکرده‌اید و لذا برنامه نمی‌تواند یک خطای احتمالی را مدیریت کند.

روش صحیح برای از بین بردن این هشدار، نوشتن مدیرت خطای واقعی است که جزئیاتش را در فصل نهم خواهیم دید. اما چون در اینجا ما فقط می‌خواهیم برنامه در صورت بروز یک خطا crash شود، می‌توانیم از expect استفاده کنیم.

چاپ مقدار متغیرها و عبارات درون رشته‌ها

تنها یک خط دیگر از کد نوشته شده وجود دارد که باید آن را بررسی کنیم و آن حط زیر است:

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

این خط رشته‌ای را که ورودی کاربر را در آن ذخیره کرده‌ایم، چاپ می‌کند. آکلادهایی که در اینجا می‌بینید جانگهدار (placeholder) هستند که برای چاپ مقادیر متغیرها کاربرد دارند. چاپ بیش از یک مقدار در یک فراخوانی println!‌ نیز ممکن است و به شکل زیر انجام می‌شود.

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

آزمایش بخش اول برنامه

قصد داریم پیش از ادامه‌ی کار، بخش اول برنامه‌ی حدس عدد را تست کنیم. برای این منظور، برنامه را با استفاده از دستور 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 به صورت تصادفی انتخاب شود. قابلیت‌ها و امکانات لازم برای تولید اعداد تصادفی هنوز به کتابخانه استاندارد Rust اضافه نشده‌اند اما تیم Rust یک crate با نام rand را به این منظور توسعه داده است.

یادآوری می‌کنم که یک crate یک مجموعه از فایل‌های کد منبع Rust است. پروژه‌ای که ما در حال ایجاد آن هستیم، یک binary crate است که یک اجرایی محسوب می‌شود. اما rand یک library crate است که کد آن برای استفاده ار برنامه‌های دیگر نوشته شده است.

یکی از نقاط قوت Cargo این است که کار با کتابخانه‌های خارجی یا external crates را بسیار ساده می‌کند. برای اینکه بتوانیم از rand استفاده کنیم، ابتدا باید در فایل Cargo.toml نام و ورژن این کتابخانه را به انتهای بخش dependencies اضافه کنیم.

Cargo.toml
[dependencies]
rand = "0.8.5"  

بخش dependencies از فایل Cargo.toml جایی است که ما وابستگی‌های پروژه یعنی کتابخانه‌های خارجی مورد نیاز پروژه را به Cargo معرفی می‌کنیم. Cargo برای دانلود و افزودن یک وابستگی به پروژه، علاوه بر نام آن کتابخانه به ورژن آن نیز نیاز دارد. در این مثال، ما کتابخانه‌ی rand را با ورژن 0.8.5 مشخص کرده‌ایم. این فرمت تعیین ورژن semantic versioning نامیده می‌شود که استانداردی برای تعیین ورژن کتابخانه‌هاست که Rust آن را درک می‌کند. ورژن 0.8.5 در واقع، اختصاری برای ^0.8.5 است که به معنای هر ورژنی است که لااثل 0.8.5 و پایین تر از 0.9.0 باشد. حالا اجازه دهید بدون ایجاد تغییر در کدها، برنامه را با استفاده از دستور cargo build بیلد کنیم:

$ cargo build
  Updating crates.io index
Downloaded rand v0.8.5
Downloaded libc v0.2.127
Downloaded getrandom v0.2.7
Downloaded cfg-if v1.0.0
Downloaded ppv-lite86 v0.2.16
Downloaded rand_chacha v0.3.1
Downloaded rand_core v0.6.3
 Compiling libc v0.2.127
 Compiling getrandom v0.2.7
 Compiling cfg-if v1.0.0
 Compiling ppv-lite86 v0.2.16
 Compiling rand_core v0.6.3
 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

حالا که پرژه‌ی ما یک وابسگی خارجی دارد، Cargo اقدام به دانلود آخرین نسخه از هر چیزی در Registry می‌کند که یک کپی از داده‌های crates.io است. crates.io بخشی از اکوسیستم Rust است که افراد می‌توانند پروژه‌های متن‌باز Rust خود را برای استفاده‌ی سایرین پست کنند.

بعد از به‌روزسانی رجیستری، Cargo بخش dependencies را بررسی کرده و هر کتابخانه‌ای که هنوز اضافه نشده را دانلود می‌کند. در این مثال، با وجودی که ما فقط یک کتابخانه‌ی rand را به عنوان وابستگی تعیین کرده‌ایم اما Cargo سایر کتابخانه‌هایی را که کارکرد rand به آنها وابسته است، دریافت می‌کند. بعد از دانلود کتابخانه‌ها، Rust آنها را کامپایل کرده و سپس پروژه را نیز با این وابستگی‌ها کامپایل می‌کند.

اگر بلافاصله و بدون ایجاد هیچ تغییر دیگری مجدداً‌ دستور cargo build را اجرا کنید، هیچ خروجی به جز خط Finished را دریافت نخواهید کرد. Cargo می‌داند که وابستگی‌ها دانلود و کامپایل شده‌اند و شما تغییری در مورد آنها در فایل Cargo.toml ایجاد نکرده‌اید. در ضمن، Cargo می‌داند که شما هیچ تغییری در ارتباط با کد خود نیز ایجاد نکرده‌اید و بنابراین، اقدام به کامپایل مجدد نخواهد کرد.

اگر فایل main.rs را باز کرده و یک تغییر جزئی را در آن اعمال کنید و مجدداً آن را بیلد کنید، دو خط خروجی را خواهید دید:

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

این خطوط نشان می‌دهد که Cargo فقط بیلد را با تغییر کوچک ایجاد شده در فایل main.rs به‌روزرسانی می‌کند. وابستگی‌های شما هیچ تغییری نکرده‌اند و لذا Cargo می‌داند که می‌تواند از آنچه که برای آنها دانلود و کامپایل شده بود، مجدداً استفاده کند. تنها چیزی که مجدداً بیلد (rebuild) می‌شود، بخش کد برنامه است.

تولید یک عدد تصادفی

حالا که ماژول rand را به فایل Cargo.toml اضافه کردیم، می‌توانیم از آن برای ایجاد یک عدد تصادفی استفاده کنیم. برای این کار، فایل main.rs را هماننذ زیر ویرایش کنید:

Copy Icon src/main.rs
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 به شکل use rand::Rng اضافه کرده‌ایم. Rng نام یک trait است که به rand تعلق دارد. یک trait در Rust مفهومی نزدیک به Interface در زبان‌های دیگر دارد.

سپس، از یک تابع تولید‌کننده‌ی اعداد تصادفی با نام thread_rng استفاده کرده و با فراخوانی تابع gen_range که عضوی از Rng است، بازه‌ی مورد نظر برای انتخاب عدد را تعیین کرده‌ایم.

دستورالعمل‌های استفاده از یک crate در اسناد مربوط به آنها وجود دارد. یکی دیگر از ویژگی‌های بارز Cargo این است که شما می‌توانید با اجرای دستور cargo doc --open به طور آفلاین و از طریق مرورگر خود به اسناد ارائه شده توسط وابستگی‌ها دسترسی پیدا کنید. به عنوان مثال، اگر مایلید اطلاعاتی در مورد سایر قابلیت‌های rand بدست آورید، کافیست این دستور را اجرا کنید و سپس از نوار کناری سمت چپ روی rand کلیک کنید.

خط دومی که به میانه‌ی کدها اضافه کرده‌ایم، عدد محرمانه را چاپ می‌کند. این امر برای تست در زمان توسعه مفید است اما بدیهی است که باید در ورژن نهایی آن را حذف کنیم.

مقایسه حدس کاربر با عدد محرمانه

در این نقطه، ما ورودی کاربر و عدد محرمانه‌ای که کاربر باید حدس بزند را داریم و اکنون باید آنها را با هم مفایسه کنیم. این کار در کدهای زیر انجام شده است:

Copy Icon src/main.rs
use std::io;
use std::cmp::Ordering;
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}");

    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 نیز مانند Result یک شمارشی (enum) است. مقادیر این شمارشی عبارتند ار: Less، Greater و Equal. اینها سه خروجی ممکن از مقایسه‌ی دو مقدار با همدیگر هستند.

متد cmp دو مقدار را با هم مقایسه می‌کند و آن را روی هر چیزی که قابل مقایسه باشد، می‌توان فراخوانی کرد. این متد یک رفرنس از آنچه می‌خواهیم مقایسه کنیم، دریافت می‌کند و یکی از مقادیر مربوط به شمارشی Ordering را برمی‌گرداند. در ادامه از یک عبارت match برای تصمیم در مورد کاری که باید بر اساس مقدار Ordering انجام شود، استفاده کرده‌ایم.

match یک ساختار شرطی است که کم‌و‌بیش مشابه switch در زبان‌های دیگر است. یک عبارت match از چند بازو (arm) تشکیل می‌شود. Rust مقدار داده شده به match را دریافت کرده و به نوبت به الگوی هر بازو نگاه می‌کند. ساختار match قابلیت قدرتمندی از Rust است که به ما امکان می‌دهد که موقعیت‌های مختلفی را که کد ما می‌تواند با آنها روبرو شود، بیان کرده و مطمئن باشیم که همه‌ی آنها را مدیریت کرده‌ایم. در فصول ششم و هجدهم در مورد جزئیات این ویژگی‌ها صحبت خواهیم کرد.

اجازه دهید با یک مثال در مورد آنچه می‌تواند با عبارت match رخ دهد، صحبت کنیم. فرض کنید کاربر عدد 50 را حدس زده و عدد محرمانه‌ی مورد نظر برنامه 38 باشد. وقتی برنامه این دو عد را با هم مقایسه می‌کند، متد cmp مقدار Ordering::Greater را برخواهد‌گرداند چون 50 از 38 بزرگتر است. عبارت match مقدار Ordering::Greater را دریافت کرده و بررسی هر بازو شروع می‌کند. ابتدا به الگوی بازوی اول نگاه می‌کند که Ordering::less است و لذا کد آن را نادیده می‌گیرد و به سراغ بازوی بعدی می‌رود. الگوی بازوی بعدی Ordering::Greater است که با الگوی مورد نظر مطابقت دارد. بنابراین، کد مرتبط با این بازو اجرا شده و عبارت Too big! چاپ می‌شود. عبارت match در همین نقطه به پایان می‌رسد زیرا در این سناریو نیازی به بررسی بازوی آخر نیست.

اما اگر این کد را اجرا کنیم، با خطای زیر روبرو می‌شویم و برنامه کامپایل نخواهد شد:

$ 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:22:21
   |
22 | 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/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/core/src/cmp.rs:836: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 دارای یک سیستم نوع استاتیک بسیار قوی است که همین امر باعث می‌شود که نتواند یک مقدار رشته‌ای را با یک مقدار عددی مقایسه کند. برای حل این مشکل، با اضافه‌کردن یک حط کد دیگر، رشته‌ی ورودی را به یک عدد تبدیل می‌کنیم.

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

در اینجا متغیری با نام guess ایجاد کردیم اما با وجودی که در برنامه‌ی ما یک متغیر با این نام وجود داشت، هیچ خطایی رخ نداد. چرا؟ داستان از این قرار است که در Rust امکان تعریف مجدد یک متغیر و پوشاندن (shadowing) متغیر قبلی وجود دارد. این ویژگی معمولاً در شرایطی به کار می‌آید که بخواهیم مقداری از یک نوع را به نوعی دیگر تبدیل کنیم. تکنیک shadowing به ما امکان می‌دهد که به جای تعریف یک متغیر جدید، از نام متغیر قبلی استفاده‌ی مجدد کنیم. در فصل سوم توضیحات لازم در خصوص shadowing داده خواهد شد.

روی متغیر guess ابتدا یک متد با نام trim و سپس متد دیگری با نام parse فراخوانی شده است. متد trim فاصله‌های سفید اضافی در دو طرف رشته را حذف می‌کند و متد parse آن را به یک نوع عددی تبدیل می‌کند. در Rust چندین نوع عددی داریم و بنابراین، باید نوع مورد نظر را دقیقاً مشخص کنیم و این کاری است که با استفاده از type annotation انجام شده است. به این ترتیب که بعد از نام متغیر یک کاراکتر دونقطه و سپس نوع مورد نظر که در اینحا u32 است، آورده شده است.

parse متدی است که فراخوانی آن می‌تواند با تولید خطا همراه باشد. برای مثال، اگر رشته‌ی مورد نظر شامل کاراکترهای A👍% باشد، راهی برای تبدیل آن به یک عدد نیست. به خاطر وجود احتمال این شکست، متد parse یک نوع Result برمی‌گرداند، همانطور که متد read_line این کار را انجام می‌دهد. این بار هم مشکل را با استفاده از متد expect حل می‌کنیم و باز هم تأکید می‌کنیم که در برنامه‌های واقعی باید استراتژی مدیریت خطای منطقی‌تری داشته باشیم. حالا اجازه دهید برنامه را اجرا کنیم.

$ 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!

تا اینجا همه‌چیز به‌خوبی کار می‌کند اما برنامه‌ی ما هنوز یک چیزی کم دارد. فعلاً این برنامه می‌تواند فقط یک عدد را دریافت کند و با عدد محرمانه مقایسه کند اما ما می‌خواهیم به کاربر فرصت تکرار حدس را بدهیم تا بتواند عدد محرمانه را پیدا کند. پس به استفاده از حلقه‌ها نیاز داریم.

تکرار حدس با حلقه‌ها

در Rust می‌توانیم با استفاده از کلمه کلیدی loop یک حلقه‌ی نامتناهای ایجاد کنیم. دستورات درون بلاک loop به‌صورت تکراری اجرا می‌شوند.

// --snip--

println!("The secret number is: {secret_number}");
        
loop {
    println!("Please input your guess.");
        
    // --snip--
        
    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => { 
            println!("You win!");
            break;
    } 
  }
}

همانطور که می‌بینید، بعد از دریافت ورودی، همه‌ی کدها را درون بلاک loop قرار داده‌ایم و با استفاده از کلمه کلیدی break ترتیبی داده‌ایم که حلقه پس از حدس درست کاربر متوقف شود.

مدیریت ورودی نامعتبر

برنامه‌ی حدس عدد را با یک بهینه‌سازی مربوط به مدیریت ورودی نامعتبر به پایان می‌رسانیم. در حال حاضر، اگر کاربر مقدار نامعتبری را وارد کند که قابل تبدیل به یک مقدار عددی نباشد، برنامه متوقف می‌شود. اما حالا می‌خواهیم کاری کنیم که به حای توقف برنامه، پیغامی مبنی بر نامعتبر بودن ورودی برای کاربر نمایش دهد و به او امکان ورود مجدد عدد را بدهد. برای این کار، کافیست خطی را که متغیر guess از String به u32 تبدیل می‌شود، با کد زیر جایگزین کنیم:

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

اکنون همه چیز در برنامه مطابق انتظار کار می‌کند. اما قبل از اجرا، خطی را که عدد تصادفی تولید شده را چاپ می‌کند، حذف کنید تا به کد نهایی زیر برسید:

Copy Icon 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,
        };
            
        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}