مقدمه

کنترل جریان برنامه با تکیه بر ساختارهای شرط و تکرار، یکی دیگر از ویژگی‌های پایه‌ای است که در زبان‌های برنامه‌نویسی مختلف وجود دارد و ما در این درس قصد داریم این مورد را از منظر Rust مورد بررسی قرار دهیم. ابتدا در مورد ایجاد ساختارهای شرطی با استفاده از عبارت if صحبت می‌کنیم و سپس، حلقه‌های تکرار loop، while و for را معرفی و بررسی می‌کنیم.

عبارات if در Rust

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

برای بررسی عبارت if در Rust، یک پروژه‌ی جدید با نام branches را درون دایرکتوری projects که در فصل اول آن را ساختیم، ایجاد کنید. سپس، کد زیر را درون فایل main.rs وارد کنید.

Copy Icon src/main.rs
fn main() {
  let number = 3;
          
  if number < 5 {
    println!("condition was true");
  } else {
    println!("condition was false");
  }
}

هر عبارت if با کلمه کلیدی if آغاز شده و در ادامه، شرط مورد نظر آورده می‌شود. دقت کنید که شرط عبارت if نباید درون پرانتز نوشته شود. در این مثال، شرط مورد نظر این است که متغیر number از 5 کوچکتر باشد. کدها یا دستوراتی که مایلیم در صورت برقراری شرط اجرا شوند، درون بلاک مربوط به if نوشته می‌شود.

یک عبارت if می‌تواند یک بخش else هم داشته باشد که شامل کدهایی است که باید در صورت برقرار نبودن شرط اجرا شوند. اما در غیاب بخش else، اگر شرط مورد نظر برقرار نباشد، برنامه عبارت 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 را طوری تغییر دهیم که شرط مورد نظر برقرار نباشد و ببینیم چه اتفاقی می افتد.

Copy Icon RUST
let number = 7;

مجدداً برنامه را اجرا کنید و به خروجی آن نگاه کنید.

$ 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 باشد. در غیر این صورت، مانند مثال زیر با خطا مواجه می‌شویم.

Copy Icon 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         
          

این خطا نشان می‌دهد که Rust انتظار یک bool را داشته اما در عوض، با یک عدد صحیح روبرو شده است. بر خلاف زبان‌هایی مانند Ruby و حاوااسکریپت، کامپایلر Rust سعی در تبدیل نوع‌های غیر بولین به بولین نمی‌کند. باید صریح باشید و همیشه یک عبارت بولین را به عنوان شرط عبارت if فراهم کنید. به عنوان مثال، اگر بخواهیم ترتیبی دهیم که بلاک کد مربوط به if تنها در صورتی اجرا شود که یک عدد مخالف صفر باشد، می‌توانیم عبارت if را به صورت زیر تغییر دهیم.

Copy Icon src/main.rs
fn main() {
  let number = 3;
          
  if number != 0 {
    println!("number was something other than zero");
  }
}

کاربرد عبارات else if

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

Copy Icon 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         
          

با وجودی که علاوه بر شرط دوم، شرط سوم هم برقرار است اما فقط بلاک مربوط به شرط دوم است که اجرا می‌شود. پس Rust فقط اولین بلاک درست را اجرا می‌کند و وقتی آن را پیدا کرد، بررسی را متوقف می‌کند.

استفاده از عبارات else if متعدد می‌تواند باعث آشفتگی و بی‌نظمی کدهای ما شود. در فصل ششم یک ساختار شرطی بسیار قدرتمند با نام match را معرفی می‌کنیم که نسبت به استفاده از عبارات else if متعدد، بسیار تر و تمیزتر و قوی‌تر عمل می‌کند.

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

از آنجایی که if یک عبارت (expression) است، می‌توانیم مانند کد زیر از آن در سمت راست یک گزاره‌ی let استفاده کنیم.

Copy Icon src/main.rs
fn main() {
  let condition = true;
  let number = if condition { 5 } else { 6 };
          
  println!("The value of number is: {number}");
}

در اینجا متغیر number می‌تواند بر اساس خروجی عبارت if، یکی از دو مقدار 5 یا 6 را دریافت کند. این کد را اجرا کنید تا ببینید چه اتفاقی می‌افتد.

$ 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 را دارند، الزاماً از یک نوع باشند. در کد بالا نتایج هر دو بازوی if و else از نوع i32 است. اگر این نوع‌ها مثل مثال زیر ناسازگاری داشته باشند، با خطا روبرو می‌شویم.

Copy Icon 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 به یک رشته ارزیابی می‌شود. این امر مشکل ایجاد می‌کند؛ جون متغیرها باید از یک نوع باشند تا Rust نوع متغیر number را در زمان کامپایل بداند.

حلقه‌های تکرار در Rust

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

در Rust سه نوع حلقه داریم که عبارتند از loop، while و for. در ادامه با هر یک از این حلقه‌ها آشنا خواهیم شد.

تکرار کد با حلقه loop

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

Copy Icon src/main.rs
fn main() {
  loop {
    println!("again!");
  }
}

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

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

نماد ^C نشان‌دهنده‌ی جایی است که کلید CTRL-C را فشار داده‌ایم. بسته به جایی از حلقه که کد در هنگام دریافت سیگنال توقف برنامه در آنجا قرار داشته، ممکن است بعد از این نماد عبارت again! را ببینید یا نبینید.

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

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

Copy Icon src/main.rs
fn main() {
  let mut counter = 0;
          
  let result = loop {
    counter += 1;
          
    if counter == 10 {
        break counter * 2;
    }
  };
          
  println!("The result is {result}");
}

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

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

Copy Icon src/main.rs
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 است و حلقه‌ی درونی فاقد لیبل است. اولین 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

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

Copy Icon src/main.rs
fn main() {
  let mut number = 3;
          
  while number != 0 {
    println!("{number}!");
          
    number -= 1;
  }
          
  println!("LIFTOFF!!!");
}

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

تکرار روی یک کالکشن با for

امکان استفاده از while برای انجام تکرار روی یک کالکشن (مثل آرایه یا تاپل) وجود دارد. برای مثال، کد زیر هر عنصر موجود در آرایه‌ی a را چاپ می‌کند.

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

در اینجا کد روی عناصر یک آرایه تکرار می‌شود. کار از اندیس صفر شروع شده و تا رسیدن به اندیس نهایی در آرایه ادامه پیدا می‌کند. اجرای این کد باعث چاپ همه‌ی عناصر آرایه می‌شود.

$ 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
          

مطابق انتظار، همه‌ی عناصر آرایه در خروجی نمایش داده شده‌اند. به محض رسیدن به اندیس 5 حلقه متوقف می‌شود.

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

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

Copy Icon src/main.rs
fn main() {
  let a = [10, 20, 30, 40, 50];
          
  for element in a {
    println!("the value is: {element}");
  }
}

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

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

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

Copy Icon src/main.rs
fn main() {
  for number in (1..4).rev() {
    println!("{number}!");
  }
  println!("LIFTOFF!!!");
}

در این کد دو مورد وجود دارد که قبلاً آنها را ندیده بودیم. یکی سینتکس (a..b) است که باعث تولید اعداد صحیح بین a تا b (به جز خود b) می‌شود. دیگری هم متد rev است که بازه‌ی تولید شده را معکوس می‌کند و باعث می‌شود، پیمایش از آخر به اول انجام شود.

در پایان، توصیه می‌کنم فقط وقتی سراغ فصل بعدی بروید که مطمئن شده باشید مطالب فصل جاری را به خوبی درک کرده‌اید و به اندازه‌ی کافی روی آنها مسلط هستید.