مقدمه

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

متغیرهای Rust

در Rust متغیرها مثل هر زبان دیگر برای ذخیره‌ی مقادیر کاربرد دارند اما متغیرهای Rust در حالت پیش‌فرض Immutable یا تغییرناپذیر هستند و این ویژگی را جز در زبان‌های full-functional مانند Haskell کمتر دیده‌ایم. تغییرناپذیری متغیرها به این معناست که وقتی مقداری به یک متغیر داده شد، امکان تغییر آن وجود ندارد. البته تأکید می‌کنم که این فقط رفتار پیش‌فرض است و امکان تعریف متغیرهای تغییرپذیر یا Mutable هم وجود دارد.

یکی از مزایای ویژه‌ی Rust این است که در این زبان مدل همزمانی (concurrency) نسبت به زبان‌های دیگر فرم ساده‌تری دارد و ریشه‌ی این سادگی در تغییرناپذیری متغیرهاست. علاوه بر این، تغییرناپذیری متغیرها امکان رخ دادن خیلی از خطاهای رایج در برنامه‌نویسی را از بین برده یا کاهش می‌دهد.

با استفاده از دستور زیر یک پروژه با نام variables در دایرکتوری projects ایجاد کنید:

$ cargo new variables

حالا فایل main.rs در دایرکتوری src را باز کرده و کدهای زیر را در آن وارد کنید:

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

فایل را ذخیره گرده و با استفاده از دستور cargo run برنامه را اجرا می‌کنیم. با این کار، خطایی رخ داده و پیام زیر نمایش داده می‌شود.

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
error[E0384]: cannot assign twice to immutable variable `x`
 --> src/main.rs:4:5
  |
2 |     let x = 5;
  |         -
  |         |
  |         first assignment to `x`
  |         help: consider making this binding mutable: `mut x`
3 |     println!("The value of x is: {x}");
4 |     x = 6;
  |     ^^^^^ cannot assign twice to immutable variable

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

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

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

اما در Rust وقتی متغیری تعریف شد، امکان تغییرش وجود ندارد و بنابراین، چنین باگ‌هایی رخ نخواهند داد. از طرفی، اگر بخواهیم متغیری را به صورت تغییرپذیر تعریف کنیم، کافیست از کلمه کلیدی mut قبل از نام متغیر استفاده کنیم. چنین متغیری می‌تواند مثل یک متغیر معمولی مقادیر مختلفی را در طول برنامه دریافت کند.

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

Copy Icon src/main.rs
fn main() {
  let mut x = 5;
  println!("The value of x is: {x}");
  x = 6;
  println!("The value of x is: {x}");
}

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

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

به دلیل استفاده از کلمه کلیدی mut در تعریف متغیر x ما مجاز به تغییر مقدار این متغیر از 5 به 6 هستیم.

ثابت‌ها در Rust

کلمه کلیدی const برای تعریف ثابت‌ها (constants) یعنی متغیرهایی که امکان تغییر مقدارشان وجود ندارد، کاربرد دارد. با وجود شباهتی که بین ثابت‌ها و متغیرهای تغییرناپذیر به نظر می‌رسد، بین آنها تفاوت‌های مهمی وجود دارد که کاربری آنها را از هم متمایز می‌کند:

  • اول اینکه متغیرهایی که با let تعریف می‌شوند، فقط به طور پیش‌فرض تغییرناپذیرند و می‌توان آنها را تغییرپذیر تعریف کرد اما متغیرهای تعریف‌شده با const یا همان ثابت‌ها همواره تغییرناپذیرند ونمی‌توان از کلمه کلیدی mut برای آنها استفاده کرد.
  • دوم اینکه در تعریف یک ثابت باید حتماً نوع داده ذکر شود. اعلام نوع متغیرها در Rust با استفاده از تکنیکی به نام Type annotation انجام می‌شود. در این روش، بعد از نام متغیر یک کاراکتر دونقطه و سپس، نوع آن آورده می‌شود. مثلاً اگر بخواهیم یک ثابت با نام C برای ذخیره‌ی سرعت نور ایجاد کنیم، باید از یک گزاره‌ی const C: u32 = 299792458; استفاده کنیم. اما در مورد متغیرهای تعریف‌شده با let اعلام نوع متغیر اختیاری است و اگر این کار را نکنیم، کامپایلر از روی مقدار متغیر، نوع آن را استنتاج می‌کند.
  • دیگر اینکه ثابت‌ها را می‌توان در هر محدوده (scope) تعریف کرد از جمله محدوده‌ی global و این امر آنها را به یک انتخاب مناسب برای زمانی که بخش‌های مختلفی از کد نیاز به اطلاع از مقدار یک متغیر دارند، تبدیل می‌کند.
  • و بالاخره اینکه مقدار یک ثابت باید در زمان کامپایل مشخص باشد و نمی‌توان تعیین مقدار آن را به زمان اجرا موکول کرد. بنابراین، اگر بخواهیم متغیری تعریف کنیم که یک مقدار را در زمان اجرا مثلاً از یک فایل یا دیتابیس بخواند و یا از کاربر دریافت کند، نمی‌توانیم از ثابت‌ها استفاده کنیم.

همانطور که در مثال زیر می‌بینید، مقدار یک ثابت می‌تواند یک مقدار محاسباتی باشد اما تأکید می‌کنم که این محاسبه باید در زمان کامپایل انجام شود نه در زمان اجرا.

const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;

نامگذاری متغیرها

طبق قرارداد، برای نامگذاری متغیرها (و توابع) از شیوه‌ی snake_case استفاده می‌شود. در این شیوه نام‌ها با حروف کوچک نوشته شده و برای جداسازی کلمات از کاراکتر _ استفاده می‌شود. اما ثابت‌ها را به صورت SNAKE_CASE نامگذاری می‌کنیم؛ یعنی به‌جای حروف کوچک از حروف بزرگ استفاده می‌کنیم.

مفهوم Shadowing

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

Copy Icon src/main.rs
fn main() {
  let x = 5;
          
  let x = x + 1;
          
  {
      let x = x * 2;
      println!("The value of x in the inner scope is: {x}");
  }
          
  println!("The value of x is: {x}");
}

اجرای این مثال منجر به تولید خروجی زیر می‌شود:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/variables`
The value of x in the inner scope is: 12
The value of x is: 6
          

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

let spaces = "   ";
let spaces = spaces.len();        

این ساختار مجاز است زیرا اولین متغیر spaces از نوع رشته است و دومین متغیر spaces که یک متغیر جدید است که فقط همنام با متغیر قبلی است، یک نوع عددی است. به این ترتیب، استفاده از shadowing ما را از داشتن نام‌های متفاوتی مانند spaces_str و spaces_num بی‌نیاز کرده و در عوض می‌توانیم از نام spaces دو بار استفاده کنیم. با این حال، اگر سعی کنیم برای این مثال از mut استفاده کنیم، با خطا مواجه خواهیم شد:

let mut spaces = "   ";
spaces = spaces.len();        

پیام خطا می‌گوید که ما نمی‌توانیم نوع یک متغیر را تغییر دهیم:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
error[E0308]: mismatched types
 --> src/main.rs:3:14
  |
2 |     let mut spaces = "   ";
  |                      ----- expected due to this value
3 |     spaces = spaces.len();
  |              ^^^^^^^^^^^^ expected `&str`, found `usize`

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

          

حالا که با متغیرها و ویژگی‌های آنها در Rust آشنا شدیم، درس بعد را به یک مفهوم مرتبط و بسیار مهم یعنی نوع‌های داده اختصاص می‌دهیم.