کنترل جریان
توانایی اجرای کدی که وابسته به درست بودن یا نبودن یک شرط است و اجرای مکرر کدی در حالی که یک شرط درست است، از ساختارهای اساسی در بیشتر زبانهای برنامهنویسی محسوب میشود. رایجترین ساختارهایی که به شما امکان کنترل جریان اجرای کد در راست را میدهند، عبارتند از عبارات 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 نشان داده شده است.
fn main() { let condition = true; let number = if condition { 5 } else { 6 }; println!("The value of number is: {number}"); }
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
برای اجرای برنامه سه بار، شمارش معکوس در هر بار، و سپس چاپ یک پیام و خروج از حلقه استفاده میکنیم.
fn main() { let mut number = 3; while number != 0 { println!("{number}!"); number -= 1; } println!("LIFTOFF!!!"); }
while
برای اجرای کد تا زمانی که شرط برقرار باشداین سازه مقدار زیادی از تو در تویی که در صورت استفاده از loop
، if
، else
و break
لازم بود را حذف میکند و واضحتر است. تا زمانی که یک شرط به مقدار true
ارزیابی شود، کد اجرا میشود؛ در غیر این صورت، حلقه متوقف میشود.
تکرار از طریق یک مجموعه با for
شما همچنین میتوانید از ساختار while
برای تکرار در عناصر یک مجموعه مانند یک آرایه استفاده کنید. به عنوان مثال، حلقه در Listing 3-4 هر عنصر در آرایه a
را چاپ میکند.
fn main() { let a = [10, 20, 30, 40, 50]; let mut index = 0; while index < 5 { println!("the value is: {}", a[index]); index += 1; } }
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 است.
fn main() { let a = [10, 20, 30, 40, 50]; for element in a { println!("the value is: {element}"); } }
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 صحبت خواهیم کرد که معمولاً در زبانهای برنامهنویسی دیگر وجود ندارد: مالکیت.