وبلاگ شخصی
محمّدرضا
علی حسینی

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

آموزش زبان برنامه‌نویسی Rust - قسمت ۱: کار با متغیّرها و ثوابت

آموزش زبان برنامه‌نویسی Rust - قسمت ۱: کار با متغیّرها و ثوابت

در قسمت قبل نحوه‌ی نصب Rust روی سیستم‌عامل‌های مختلف‌ را دیدیم و اوّلین برنامه خودمان را در این زبان نوشتیم.

اگر آن قسمت را نخوانده‌اید همین الان به سراغش بروید. چون بدون دانستن آن چیز زیادی از این قسمت دستگیرتان نخواهد شد.

ابتدای این جلسه، همانطوری که در انتهای جلسه‌ی قبل گفتم، با هم ساختار برنامه‌ی Hello World که ساختار کلّی برنامه‌های دیگر هم به این زبان هست را بررسی می‌کنیم.

ساختار برنامه‌ی Hello World

برنامه‌ای که با هم در قسمت قبل نوشتیم این بود:

fn main() {
    println!("سلام دنیا!");
}
حالا زمان این است که بفهمیم هر خط این برنامه چه معنایی دارد.

برنامه با این خط شروع می‌شود:

fn main() {

همانطور که احتمالاً خودتان هم حدس می‌زنید، fn مخفف کلمه‌ی function است. وقتی که این کلمه‌ی کلیدی به کار برده می‌شود یعنی داریم یک تابع را اعلان می‌کنیم.

اگر قبل از این با زبان C یا cpp کار کرده باشید حتماً به خاطر دارید که وقتی برنامه اجرا می‌شد تنها کدهای درون تابع main بودند که اجرا می‌شدند. در زبان Rust هم همین‌طور است. یعنی کدهایی که داخل تابع main قرار دارند اجرا می‌شوند و اگر کدی درون این تابع فراخوانی نشده باشد، اجرایی نخواهد شد.

هر برنامه‌ای که به زبان Rust نوشته می‌شود باید تابعی به نام main داشته باشد، در غیر این صورت هنگام کامپایل با خطای زیر روبه‌رو می‌شوید:

error[E0601]: `main` function not found in crate

وقتی به این خطا برخورد می‌کنید با یکی دیگر از جذّابیّت‌های زبان Rust روبه‌رو می‌شوید. زیر متن ارور راهنمای زیر نوشته شده است:

|
  = note: consider adding a `main` function to `main.rs`
یکی از نقاط قوّت این زبان پیغام‌های خطای دقیق و واضح آن است که عموماً با راهنمایی‌هایی برای رفع آن خطا همراه اند. این ویژگی را با خطاهای C مقایسه کنید که گاهی اوقات آدم را چندین روز سرکار می‌گذارند و اکثر اوقات مشکل واقعی را نمی‌گویند.

پرانتزهای بعد از اسم تابع محل قرارگیری پارامترهای تابع هستند. از آنجایی که تابع main پارامتر ورودی ندارد، پس درون پرانتزها چیزی نمی‌نویسیم.

مانند خیلی از زبان‌های دیگر، کدهای تابع درون { و } قرار می‌گیرند. پس حالا که به آکولاد باز رسیدیم وقت آن است که سراغ خط بعدی برویم تا ببینیم درون تابع چه اتّفاقی می‌افتد.

println!("سلام دنیا!");

برای اکثر برنامه‌نویس‌ها وجود علامت ! در آخر اسم تابع خیلی عجیب است. شما هم حق دارید که با دیدن این علامت تعجّب کنید، چون !println اصلاً تابع نیست، بلکه یک macro در زبان Rust است. در مورد macro ها بعداً به صورت کامل بحث می‌کنیم. برای الان فقط کافی است بدانید که وقتی پای ! درمیان است، داریم از یک macro استفاده می‌کنیم.

ماکرو !println محتوایش را داخل ترمینال یا cmd به کاربر نمایش می‌دهد. ورودی این ماکرو می‌تواند string یا عدد باشد.

در آخرین خط هم با گذاشتن { تابع را می‌بندیم و کمپایلر می‌فهمد که کدهایی که از اینجا به بعد نوشته شده‌اند به این تابع ربطی ندارند.

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

تغییرپذیر و غیرقابل تغییر

تغییرپذیر (mutable) و غیرقابل تغییر (immutable) دو مفهوم ساده اند که موقع نوشتن برنامه موجب خطاهای زیادی می‌شوند.

یک داده‌ی تغییرپذیر، داده‌ای است که پس از اینکه برای اوّلین بار مقدار دهی شد، می‌توان همچنان مقدار آن را عوض کرد. امّا یک داده‌ی غیرقابل تغییر پس از مقداردهی اوّلیّه دیگر قابلیّت مقداردهی ندارد.

دیدید چقدر ساده بود؟ حالا قرار است با یکی از بزرگترین تفاوت‌های زبان Rust با زبان‌های دیگر روبه‌رو شوید. نفس‌هایتان را حبس کنید.

متغیّر در زبان Rust

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

نحوه‌ی تعریف یک متغیّر

احتمالاً خیلی تعجّب کرده اید. برای اینکه گیج نشوید قبل از ادامه دادن به سراغ syntax متغیّر در زبان Rust می‌رویم:

let variableName: Type = value;

خودم هم می‌دانم که این جور نوشتن گیج‌کننده است، ولی اگر اضافه‌اش نمی‌کردم متن ناقص می‌ماند.

خب برویم سراغ توضیح دادن این syntax. برای تعریف یک متغیّر ابتدا باید کلمه‌ی کلیدی let نوشته شود. با نوشتن این کلمه همه می‌فهمند که قرار است به زودی متغیّری در این مکان زاده شود.

بعد از کلمه‌ی let باید اسم متغیّر نوشته شود. اسم متغیّر نمی‌تواند با عدد شروع شود و تنها می‌تواند شامل اعداد و حروف باشد. در زبان Rust هرجا که در مورد حروف حرف می‌زنیم، منظور تمامی کاراکترهایی است که توسّط UTF-8 پشتیبانی می‌شوند. امّا هنگام اسم‌گذاری متغیّرها و توابع باید دقّت‌کنید. هنوز پشتیبانی از کاراکترهای غیر ASCII خوب نیست و نوشتن اسامی متغیّرها با کاراکترهای غیر اسکی ممکن است دردسرساز شود.

در اکثر موارد خود کامپایلر type متغیّر را تشخیص می‌دهد و لازم نیست که ما هم نوع را بنویسیم. ولی برای مواردی که ممکن است تعریف type لازم باشد، باید بعد از اسم متغیّر علامت : گذاشت و type را نوشت. در مورد type های مختلف در جلسات بعدی صحبت می‌کنیم.

شما می‌توانید یک متغیّر را اعلان کنید ولی همانجا مقداردهیش نکنید. یا می‌توانید همانجا این کار را بکنید.

حالا در کد زیر 3 متغیّر مختلف را می‌نویسم تا حالت‌های مختلف را با هم ببینیم و دیگر چیز گنگی باقی نماند:

let var1;
let var2 = 10;
let var3: i32 = 20;
// متغیّر اول را مقدار دهی نکردیم. پس می•توانیم اینجا به آن مقدار بدهیم:
var1 = 50;
گفتیم که متغیّرهای Rust غیرقابل تغییر هستند. پس ما نمی‌توانیم پس از اوّلین مقداردهی دیگر مقدار آن‌ها را تغییر دهیم.

مثلاً برنامه‌ی زیر را ببینید:

fn main() {
    let x = 10;
    x = 5;
}
فکر می‌کنید با کامپایل این کد چه اتّفاقی می‌افتد؟ همانطور که احتمالاً خودتان هم حدس می‌زنید، ارور می‌گیریم:
error[E0384]: cannot assign twice to immutable variable `x`
 --> main.rs:4:5
   |
   3 |     let x = 10;
     |         - first assignment to `x`
   4 |     x = 5;
     |     ^^^^^ cannot assign twice to immutable variable
همانطور که در متن خطا می‌بینید، ما اجازه‌ی اینکه دوباره به یک متغیّر immutable مقدار بدهیم را نداریم.

چگونه متغیّرهای تغییرپذیر داشته باشیم؟

ما خیلی وقت‌ها به این دلیل از متغیّرها استفاده می‌کنیم که می‌خواهیم در طی زمان تغییراتی روی مقدار آن‌ها اعمال کنیم. امّا وقتی که متغیّر immutable باشد دیگر نمی‌توانیم این کار را بکنیم. پس حالا برای داشتن یک حافظه‌ی تغییرپذیر باید چه کار کرد؟

راه حل ساده است. باید هنگام تعریف متغیّر بگوییم که می‌خواهیم این متغیّر mutable باشد. برای این کار کافی است که کلمه‌ی کلیدی mut را قبل از اسم متغیّر اضافه کنیم.

مثلاً همین برنامه‌ای که چند خط بالاتر نوشتیم را درنظر بگیرید. می‌خواهیم متغیّر x را قابل تغییر کنیم. برنامه این شکلی خواهد شد:

fn main() {
    let mut x = 10;
    x = 5;
}
حالا اگر این برنامه را اجرا کنید دیگر هیچ اروری دریافت نخواهید کرد.

ثابت‌ها در زبان Rust

ثابت (constant) ها، همانطوری که از اسمشان پیداست، قابل تغییر نیستند. در زبان Rust برای تعریف یک ثابت، باید از syntax زیر پیروی کنیم:

const constantName: Type = Value;

تعریف یک ثابت با کلمه‌ی کلیدی const آغاز می‌شود. بعد از این کلمه، اسم ثابت آورده می‌شود. بعد از اسم علامت : قرار می‌گیرد و بعد از آن نوع ثابت ذکر می‌شود. بعد از آن هم با گذاشتن علامت = مقدار آن ثابت را می‌نویسیم.

برخلاف متغیّرها شما باید type ثابت را همیشه ذکر کنید. برای مثال خط زیر یک ثابت عددی در زبان Rust است(در مورد i32 در جلسات بعد توضیح می‌دهم. همین که بدانید یک نوع داده ی عددی است کافی است):

const BUFFER_SIZE: i32 = 10;

تفاوت متغیّر با ثابت

دیدیم که متغیّرها به صورت پیش‌فرض غیرقابل تغییر هستند. پس شاید الان برایتان این سؤال پیش آمده باشد که اصلاً دیگر چه نیازی به ثوابت داریم؟

حالا با هم تفاوت‌های اصلی متغیّر و ثابت را می‌بینیم:

۱− شما نمی‌توانید یک ثابت را mutable کنید. همچنین باید همیشه نوع آن را ذکر کنید. وگرنه با ارور مواجه می‌شوید.

۲− برخلاف متغیّرها، ثوابت را می‌توان در تمام scope ها تعریف کرد. یعنی می‌توان یک ثابت را به صورت عمومی(global) تعریف کرد.

کد زیر را ببینید:

const BUFFER_SIZE: i32 = 10;
let x = 10; 

fn main() {
   println!("سلام دنیا!");
}
اگر برنامه‌ی بالا را کامپایل کنید با خطای زیر مواجه می‌شوید:
error: expected item، found `let`
 --> main.rs:2:1
   |
   2 | let x = 10;
     | ^^^ expected item
یعنی متغیّرها حتماً باید درون یک scope محلّی(مثل بدنه‌ی یک تابع) باشند.

۳− شما نمی‌توانید یک ثابت را با خروجی یک تابع مقداردهی کنید. امّا حتّی یک متغیّر غیرقابل تغییر را هم می‌توان برای اوّلین بار با خروجی یک تابع مقدار دهی کرد.

مثلاً کد زیر را درنظر بگیرید(فعلاً به syntax تابع function توجّه نکنید. این مورد را هم قول می‌دهم که بعداً توضیح بدهم):

const BUFFER_SIZE: i32 = function();

fn main() {
   println!("سلام دنیا!");
}

fn function() -> i32{
    return 10;
}
وقتی که این برنامه را کامپایل کنید با ارور زیر مواجه می‌شوید:
error[E0015]: calls in constants are limited to tuple structs and tuple variants
 --> main.rs:1:26
   |
   1 | const BUFFER_SIZE: i32 = function();
     |                          ^^^^^^^^^^
      |
       note: a limited form of compile-time function 
evaluation is available on a nightly compiler via `const fn`
امّا شما به راحتی می‌توانید مقدار یک متغیّر mutable یا immutable را برابر با خروجی یک تابع بگذارید.

این بار به این کد توجّه کنید:

fn main() {
    println!("سلام دنیا!");
    let x = function();
}

fn function() -> i32{
    return 10;
}
اگر این برنامه را کامپایل کنید هیچ اروری نخواهید گرفت.

۴− مورد استفاده‌ی متغیّر و ثابت با هم متفاوت است. ثابت‌ها عموماً برای کنترل بخش‌های مختلف برنامه استفاده می‌شوند و هدف استفاده از آن‌ها جلوگیری از hardcode کردن چنین بخش‌هایی است. امّا متغیّرها مستقیماً برای نگهداری داده‌های کاربردی و منطق برنامه استفاده می‌شوند.

هدف از immutable کردن متغیّرها در Rust چیست؟

حالا بیایید کمی به فلسفه‌ی پشت این تصمیم سازندگان این زبان بپردازیم. چرا باید متغیّرهارا غیرقابل تغییر کنیم؟

مسئله به یک مشکل قدیمی و همیشگی برمی‌گردد. وقتی داریم یک برنامه‌ی هم‌روند(concurrent) می‌نویسم، خیلی وقت‌ها حالتی به نام race condition پیش می‌آید.

در این حالت ۲ یا چند بخش از برنامه که دارند به صورت همزمان اجرا می‌شوند، به صورت همزمان به یک بخش از داده‌ها دسترسی پیدا می‌کنند و آن را تغییر می‌دهند. یعنی وسط کار یک thread، ناگهان thread بعدی به داده‌های مشترک دسترسی پیدا می‌کند و آن را تغییر می‌دهد. وقتی که thread اوّل می‌خواهد به کارش ادامه بدهد با داده‌ای که خراب شده است روبه‌رو می‌شود و در نتیجه حاصل کارش اشتباه خواهد شد.

حالا سازندگان Rust با غیرقابل تغییر کردن متغیّرها سعی داشتند که تا حد امکان ایمنی داده‌ها را در برنامه‌های هم‌روند تضمین کنند. اینطوری هیچ کدام از thread ها نمی‌توانند داده‌ی اصلی را عوض کنند، پس اشکالی در کار بقیه‌ی threadها پیش نمی‌آید.

امیدوارم که نوشته‌های این جلسه واضح باشند. اگر بخشی را متوجّه نشدید یا به نظرتان گنگ توضیح داده بودم در بخش نظرات بگویید تا نوشته را اصلاح کنم.

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

یا حرفه‌ای شو یا برنامه‌نویسی را رها کن.

چطور می‌شود در بازار کار خالی از نیروی سنیور ایران، بدون کمک دیگران و وقت تلف‌کردن خودت را تبدیل به یک نیروی باتجربه بکنی؟

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

«نوشته‌های مرتبط»

8 پاسخ به “آموزش زبان برنامه‌نویسی Rust – قسمت ۱: کار با متغیّرها و ثوابت”

  1. امیر گفت:

    من از روی کتاب Rust in Action دارم یاد میگیرم و تاک سافت‌ورتاکس رو دیدم بلاگ رو پیدا کردم. دمت گرم بابت وقتی که میذاری.

  2. حمید مسیبی فرد گفت:

    سلام استاد

    یه فهرست برای اموزش ها لطفا درست کنید

    چون فهرست در دست رس نیست

  3. sisco گفت:

    بنظر راست زبان شیرینی میاد 🙂

  4. امیرحسین فضلی گفت:

    درود. لطفا فهرستی از لینک های آموزش رو بزارین. الان لینک قسمت 2 رو از کجا پیدا کنیم؟

    • PerAdmin گفت:

      سلام. می‌توانید از لینک دسته‌بندی «آموزش زبان برنامه‌نویسی rust» در ستون کناری استفاده کنید یا با بخش جست‌وجو به نوشته‌ی دلخواهتان برسید.

  5. حمید مسیبی فرد گفت:

    سلام استاد بزرگوار

    استاد rust قوی تره یا zig ؟؟ لطف می کنید یه توئضیحاتی در مورد تفاول و شباهت های این دو زبان بیان کنید؟؟؟

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

«نوشته‌های ویژه»

«نوشته‌های محبوب»

«دیدگاه کاربران»