کنترل جریان

توانایی اجرای کدی که وابسته به درست بودن یا نبودن یک شرط است و اجرای مکرر کدی در حالی که یک شرط درست است، از ساختارهای اساسی در بیشتر زبان‌های برنامه‌نویسی محسوب می‌شود. رایج‌ترین ساختارهایی که به شما امکان کنترل جریان اجرای کد در راست را می‌دهند، عبارتند از عبارات if و حلقه‌ها.

عبارات if

یک عبارت if به شما امکان می‌دهد کد خود را بسته به شرایطی شاخه‌بندی کنید. شما یک شرط مشخص می‌کنید و سپس می‌گویید: «اگر این شرط برقرار بود، این بلوک کد اجرا شود. اگر شرط برقرار نبود، این بلوک کد اجرا نشود.»

یک پروژه جدید به نام branches در دایرکتوری projects خود ایجاد کنید تا عبارت if را بررسی کنید. در فایل src/main.rs کد زیر را وارد کنید:

Filename: src/main.rs

fn main() {
    let number = 3;

    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }
}

تمام عبارات if با کلمه کلیدی if شروع می‌شوند و سپس یک شرط دنبال می‌شود. در این مثال، شرط بررسی می‌کند که آیا مقدار متغیر number کمتر از 5 است یا خیر. بلوک کدی که در صورت درست بودن شرط باید اجرا شود، بلافاصله بعد از شرط و داخل کروشه‌ها قرار می‌گیرد. بلوک‌های کدی که با شرایط در عبارات if مرتبط هستند، گاهی بازو (arm) نامیده می‌شوند، همانند بازوهای موجود در عبارات match که در بخش “مقایسه حدس با عدد مخفی” از فصل 2 مورد بحث قرار گرفت.

به‌صورت اختیاری، می‌توانیم یک عبارت else نیز اضافه کنیم، همان‌طور که اینجا انتخاب کردیم، تا به برنامه یک بلوک کد جایگزین برای اجرا ارائه دهیم، در صورتی که شرط به false ارزیابی شود. اگر عبارت else ارائه ندهید و شرط false باشد، برنامه بلوک if را نادیده گرفته و به بخش بعدی کد می‌رود.

این کد را اجرا کنید؛ باید خروجی زیر را مشاهده کنید:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
condition was true

بیایید مقدار number را به مقداری تغییر دهیم که شرط false شود تا ببینیم چه اتفاقی می‌افتد:

fn main() {
    let number = 7;

    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }
}

برنامه را دوباره اجرا کنید و خروجی را مشاهده کنید:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
condition was false

همچنین قابل توجه است که شرط در این کد باید یک bool باشد. اگر شرط یک bool نباشد، خطا دریافت خواهیم کرد. به عنوان مثال، این کد را اجرا کنید:

Filename: src/main.rs

fn main() {
    let number = 3;

    if number {
        println!("number was three");
    }
}

این بار شرط if به مقدار 3 ارزیابی می‌شود و راست خطا می‌دهد:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: mismatched types
 --> src/main.rs:4:8
  |
4 |     if number {
  |        ^^^^^^ expected `bool`, found integer

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

خطا نشان می‌دهد که راست انتظار یک bool داشت اما یک عدد صحیح دریافت کرد. برخلاف زبان‌هایی مانند Ruby و JavaScript، راست به‌صورت خودکار تلاش نمی‌کند انواع غیر bool را به یک bool تبدیل کند. شما باید صریح باشید و همیشه یک bool را به‌عنوان شرط به if بدهید. اگر می‌خواهید بلوک کد if فقط زمانی اجرا شود که یک عدد برابر 0 نباشد، می‌توانید عبارت if را به این صورت تغییر دهید:

Filename: src/main.rs

fn main() {
    let number = 3;

    if number != 0 {
        println!("number was something other than zero");
    }
}

اجرای این کد number was something other than zero را چاپ خواهد کرد.

مدیریت شرایط متعدد با else if

شما می‌توانید با ترکیب if و else در یک عبارت else if، شرایط متعددی را مدیریت کنید. به عنوان مثال:

Filename: src/main.rs

fn main() {
    let number = 6;

    if number % 4 == 0 {
        println!("number is divisible by 4");
    } else if number % 3 == 0 {
        println!("number is divisible by 3");
    } else if number % 2 == 0 {
        println!("number is divisible by 2");
    } else {
        println!("number is not divisible by 4, 3, or 2");
    }
}

این برنامه چهار مسیر ممکن برای اجرا دارد. پس از اجرای آن، باید خروجی زیر را مشاهده کنید:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
number is divisible by 3

هنگامی که این برنامه اجرا می‌شود، هر عبارت if را به ترتیب بررسی کرده و اولین بلوکی که شرط آن به true ارزیابی شود، اجرا می‌کند. توجه داشته باشید که حتی با وجود اینکه 6 بر 2 بخش‌پذیر است، خروجی number is divisible by 2 را نمی‌بینیم و همچنین متن number is not divisible by 4, 3, or 2 از بلوک else را نیز نمی‌بینیم. این به این دلیل است که راست فقط بلوک مربوط به اولین شرط درست را اجرا می‌کند و پس از یافتن آن، بقیه را بررسی نمی‌کند.

استفاده از تعداد زیادی عبارت else if می‌تواند کد شما را شلوغ کند، بنابراین اگر بیش از یک مورد دارید، ممکن است بخواهید کد خود را بازنویسی کنید. فصل 6 یک ساختار شاخه‌بندی قدرتمند در راست به نام match را برای این موارد توضیح می‌دهد.

استفاده از if در یک عبارت let

از آنجایی که if یک عبارت است، می‌توانیم از آن در سمت راست یک عبارت let برای تخصیص نتیجه به یک متغیر استفاده کنیم، همان‌طور که در لیست 3-2 نشان داده شده است.

Filename: src/main.rs
fn main() {
    let condition = true;
    let number = if condition { 5 } else { 6 };

    println!("The value of number is: {number}");
}
Listing 3-2: تخصیص نتیجه یک عبارت if به یک متغیر

متغیر number به مقداری بر اساس نتیجه عبارت if متصل خواهد شد. این کد را اجرا کنید تا ببینید چه اتفاقی می‌افتد:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/branches`
The value of number is: 5

به خاطر داشته باشید که بلوک‌های کد به آخرین عبارت در آن‌ها ارزیابی می‌شوند و اعداد به تنهایی نیز عبارات محسوب می‌شوند. در این حالت، مقدار کل عبارت if بستگی به این دارد که کدام بلوک کد اجرا شود. این بدان معناست که مقادیری که می‌توانند نتایج هر بازوی if باشند، باید از یک نوع باشند. در لیست 3-2، نتایج بازوی if و بازوی else هر دو اعداد صحیح i32 بودند. اگر انواع ناسازگار باشند، مانند مثال زیر، خطایی دریافت خواهیم کرد:

Filename: src/main.rs

fn main() {
    let condition = true;

    let number = if condition { 5 } else { "six" };

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

هنگامی که تلاش می‌کنیم این کد را کامپایل کنیم، خطایی دریافت می‌کنیم. بازوهای if و else دارای انواع مقداری ناسازگار هستند و راست دقیقاً نشان می‌دهد که مشکل در برنامه کجاست:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: `if` and `else` have incompatible types
 --> src/main.rs:4:44
  |
4 |     let number = if condition { 5 } else { "six" };
  |                                 -          ^^^^^ expected integer, found `&str`
  |                                 |
  |                                 expected because of this

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

عبارت موجود در بلوک if به یک عدد صحیح ارزیابی می‌شود و عبارت موجود در بلوک else به یک رشته ارزیابی می‌شود. این کار نمی‌کند زیرا متغیرها باید یک نوع مشخص داشته باشند و راست باید در زمان کامپایل بداند که نوع متغیر number چیست. دانستن نوع number به کامپایلر این امکان را می‌دهد که بررسی کند نوع آن در هر جایی که از number استفاده می‌کنیم معتبر است. راست نمی‌توانست این کار را انجام دهد اگر نوع number تنها در زمان اجرا مشخص می‌شد. کامپایلر پیچیده‌تر می‌شد و تضمین‌های کمتری درباره کد ارائه می‌داد اگر مجبور بود انواع فرضی مختلفی را برای هر متغیر پیگیری کند.

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

اغلب مفید است که یک بلوک کد بیش از یک بار اجرا شود. برای این کار، Rust چندین حلقه ارائه می‌دهد که کد داخل بدنه حلقه را اجرا کرده و سپس بلافاصله به ابتدای حلقه بازمی‌گردند. برای آزمایش با حلقه‌ها، یک پروژه جدید به نام loops ایجاد کنید.

Rust سه نوع حلقه دارد: loop، while و for. بیایید هر کدام را امتحان کنیم.

تکرار کد با loop

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

به عنوان مثال، فایل src/main.rs را در دایرکتوری loops خود به شکل زیر تغییر دهید:

Filename: src/main.rs

fn main() {
    loop {
        println!("again!");
    }
}

وقتی این برنامه را اجرا کنیم، again! بارها و بارها به طور مداوم چاپ می‌شود تا زمانی که برنامه را به صورت دستی متوقف کنیم. اکثر ترمینال‌ها از میانبر صفحه کلید ctrl-c برای متوقف کردن برنامه‌ای که در یک حلقه بی‌پایان گیر کرده است، پشتیبانی می‌کنند. آن را امتحان کنید:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
     Running `target/debug/loops`
again!
again!
again!
again!
^Cagain!

نماد ^C نشان می‌دهد که شما ctrl-c را فشار داده‌اید. ممکن است کلمه again! پس از ^C چاپ شود یا نشود، بسته به اینکه کد در حلقه در چه مرحله‌ای بوده است که سیگنال قطع دریافت شده است.

خوشبختانه، Rust همچنین روشی برای خروج از یک حلقه با استفاده از کد ارائه می‌دهد. شما می‌توانید کلمه کلیدی break را درون حلقه قرار دهید تا به برنامه بگویید که چه زمانی اجرای حلقه را متوقف کند. به یاد داشته باشید که این کار را در بازی حدس عدد در بخش “خروج پس از یک حدس درست” در فصل 2 انجام دادیم تا زمانی که کاربر با حدس درست بازی را برنده شد، برنامه خاتمه یابد.

ما همچنین از continue در بازی حدس عدد استفاده کردیم که در یک حلقه به برنامه می‌گوید هر کد باقی‌مانده در این تکرار حلقه را نادیده بگیرد و به تکرار بعدی برود.

بازگرداندن مقادیر از حلقه‌ها

یکی از کاربردهای loop این است که یک عملیات را که ممکن است شکست بخورد دوباره امتحان کنید، مثلاً بررسی کنید که آیا یک نخ (thread) کار خود را تمام کرده است یا نه. همچنین ممکن است نیاز داشته باشید نتیجه این عملیات را از حلقه به بقیه کد خود منتقل کنید. برای انجام این کار، می‌توانید مقداری که می‌خواهید برگردانده شود را پس از عبارت break اضافه کنید. این مقدار از حلقه بازگردانده می‌شود تا بتوانید از آن استفاده کنید، همان‌طور که در اینجا نشان داده شده است:

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    println!("The result is {result}");
}

قبل از حلقه، یک متغیر به نام counter اعلام می‌کنیم و مقدار آن را 0 مقداردهی اولیه می‌کنیم. سپس یک متغیر به نام result اعلام می‌کنیم تا مقدار بازگشتی از حلقه را نگه دارد. در هر تکرار حلقه، مقدار 1 را به متغیر counter اضافه می‌کنیم و سپس بررسی می‌کنیم که آیا مقدار counter برابر با 10 است یا نه. زمانی که این شرط برقرار باشد، از کلمه کلیدی break با مقدار counter * 2 استفاده می‌کنیم. پس از حلقه، با استفاده از یک سمی‌کالن، مقدار به result تخصیص داده می‌شود. در نهایت، مقدار result را چاپ می‌کنیم که در این مثال برابر با 20 است.

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

برچسب حلقه‌ها برای رفع ابهام بین چندین حلقه

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

fn main() {
    let mut count = 0;
    'counting_up: loop {
        println!("count = {count}");
        let mut remaining = 10;

        loop {
            println!("remaining = {remaining}");
            if remaining == 9 {
                break;
            }
            if count == 2 {
                break 'counting_up;
            }
            remaining -= 1;
        }

        count += 1;
    }
    println!("End count = {count}");
}

حلقه بیرونی دارای برچسب 'counting_up است و از 0 تا 2 شمارش می‌کند. حلقه داخلی بدون برچسب از 10 تا 9 شمارش معکوس می‌کند. اولین break که برچسبی مشخص نمی‌کند فقط از حلقه داخلی خارج می‌شود. عبارت break 'counting_up; از حلقه بیرونی خارج می‌شود. این کد موارد زیر را چاپ می‌کند:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.58s
     Running `target/debug/loops`
count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2

حلقه‌های شرطی با while

یک برنامه اغلب نیاز دارد که یک شرط را درون یک حلقه ارزیابی کند. تا زمانی که شرط true باشد، حلقه اجرا می‌شود. زمانی که شرط دیگر true نباشد، برنامه با فراخوانی break، حلقه را متوقف می‌کند. امکان پیاده‌سازی چنین رفتاری با استفاده از ترکیب loop، if، else و break وجود دارد. می‌توانید این را اکنون در یک برنامه امتحان کنید، اگر مایل هستید. با این حال، این الگو آن‌قدر رایج است که Rust یک سازه زبان داخلی برای آن دارد که به آن حلقه while گفته می‌شود. در Listing 3-3، از while برای اجرای برنامه سه بار، شمارش معکوس در هر بار، و سپس چاپ یک پیام و خروج از حلقه استفاده می‌کنیم.

Filename: src/main.rs
fn main() {
    let mut number = 3;

    while number != 0 {
        println!("{number}!");

        number -= 1;
    }

    println!("LIFTOFF!!!");
}
Listing 3-3: استفاده از یک حلقه while برای اجرای کد تا زمانی که شرط برقرار باشد

این سازه مقدار زیادی از تو در تویی که در صورت استفاده از loop، if، else و break لازم بود را حذف می‌کند و واضح‌تر است. تا زمانی که یک شرط به مقدار true ارزیابی شود، کد اجرا می‌شود؛ در غیر این صورت، حلقه متوقف می‌شود.

تکرار از طریق یک مجموعه با for

شما همچنین می‌توانید از ساختار while برای تکرار در عناصر یک مجموعه مانند یک آرایه استفاده کنید. به عنوان مثال، حلقه در Listing 3-4 هر عنصر در آرایه a را چاپ می‌کند.

Filename: src/main.rs
fn main() {
    let a = [10, 20, 30, 40, 50];
    let mut index = 0;

    while index < 5 {
        println!("the value is: {}", a[index]);

        index += 1;
    }
}
Listing 3-4: تکرار در هر عنصر یک مجموعه با استفاده از حلقه while

در اینجا، کد از طریق عناصر آرایه شمارش می‌کند. از اندیس (index)0 شروع می‌کند و سپس تا زمانی که به آخرین اندیس (index)در آرایه برسد (یعنی وقتی که index < 5 دیگر true نباشد) حلقه می‌زند. اجرای این کد هر عنصر در آرایه را چاپ می‌کند:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.32s
     Running `target/debug/loops`
the value is: 10
the value is: 20
the value is: 30
the value is: 40
the value is: 50

همه پنج مقدار آرایه همانطور که انتظار می‌رود در ترمینال ظاهر می‌شوند. حتی اگر index در نهایت به مقدار 5 برسد، حلقه قبل از تلاش برای گرفتن مقدار ششم از آرایه متوقف می‌شود.

با این حال، این روش مستعد خطاست؛ ما می‌توانیم باعث شویم برنامه در صورت اشتباه بودن مقدار اندیس (index)یا شرط آزمایشی متوقف شود. به عنوان مثال، اگر تعریف آرایه a را به چهار عنصر تغییر دهید اما فراموش کنید شرط را به while index < 4 به‌روزرسانی کنید، کد متوقف خواهد شد. همچنین این روش کند است، زیرا کامپایلر کد زمان اجرا را برای انجام بررسی شرطی در مورد اینکه آیا اندیس (index)در محدوده آرایه است یا نه در هر تکرار حلقه اضافه می‌کند.

به عنوان یک جایگزین مختصرتر، می‌توانید از حلقه for استفاده کنید و برای هر مورد در یک مجموعه، کدی اجرا کنید. یک حلقه for شبیه کدی در Listing 3-5 است.

Filename: src/main.rs
fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a {
        println!("the value is: {element}");
    }
}
Listing 3-5: تکرار در هر عنصر یک مجموعه با استفاده از حلقه for

وقتی این کد را اجرا می‌کنیم، خروجی مشابه Listing 3-4 را مشاهده خواهیم کرد. مهم‌تر اینکه، اکنون ایمنی کد را افزایش داده‌ایم و احتمال خطاهایی که ممکن است ناشی از فراتر رفتن از انتهای آرایه یا عدم دسترسی به برخی از آیتم‌ها باشد را حذف کرده‌ایم.

با استفاده از حلقه for، نیازی به به خاطر سپردن تغییر کد دیگری ندارید اگر تعداد مقادیر در آرایه را تغییر دهید، همانطور که با روش استفاده شده در Listing 3-4 باید انجام می‌دادید.

ایمنی و مختصر بودن حلقه‌های for آنها را به رایج‌ترین سازه حلقه‌ای در Rust تبدیل کرده است. حتی در موقعیت‌هایی که می‌خواهید کدی را تعداد مشخصی از دفعات اجرا کنید، مانند مثال شمارش معکوس که از حلقه while در Listing 3-3 استفاده می‌کرد، اکثر برنامه‌نویسان Rust از حلقه for استفاده می‌کنند. روش انجام این کار استفاده از Range، که توسط کتابخانه استاندارد ارائه می‌شود، است که تمام اعداد را به ترتیب از یک عدد شروع کرده و قبل از عدد دیگری به پایان می‌رساند.

این چیزی است که شمارش معکوس با استفاده از یک حلقه for و روش دیگری که هنوز در مورد آن صحبت نکرده‌ایم، یعنی rev برای معکوس کردن محدوده، به نظر می‌رسد:

Filename: src/main.rs

fn main() {
    for number in (1..4).rev() {
        println!("{number}!");
    }
    println!("LIFTOFF!!!");
}

این کد کمی بهتر نیست؟

خلاصه

شما موفق شدید! این یک فصل بزرگ بود: شما درباره متغیرها، انواع داده اسکالر و مرکب، توابع، نظرات، عبارات if و حلقه‌ها یاد گرفتید! برای تمرین با مفاهیم مطرح‌شده در این فصل، سعی کنید برنامه‌هایی برای انجام موارد زیر بسازید:

  • تبدیل دما بین فارنهایت و سلسیوس.
  • تولید عدد nام دنباله فیبوناچی.
  • چاپ متن سرود کریسمس “The Twelve Days of Christmas”، با استفاده از تکرار موجود در این آهنگ.

وقتی آماده شدید تا به مرحله بعد بروید، ما درباره مفهومی در Rust صحبت خواهیم کرد که معمولاً در زبان‌های برنامه‌نویسی دیگر وجود ندارد: مالکیت.