دوستدار نرمافزار، فلسفه و ادبیات. وب سایت:http://www.alihoseiny.ir
آموزش زبان برنامهنویسی Rust - قسمت 1: شروع کار با متغیّرها و ثوابت
در قسمت قبل نحوهی نصب 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;
تفاوت متغیّر با ثابت
دیدیم که متغیّرها به صورت پیشفرض غیرقابل تغییر هستند. پس شاید الان برایتان این سؤال پیش آمده باشد که اصلاً دیگر چه نیازی به ثوابت داریم؟
حالا با هم تفاوتهای اصلی متغیّر و ثابت را میبینیم:
1-شما نمیتوانید یک ثابت را mutable کنید. همچنین باید همیشه نوع آن را ذکر کنید. وگرنه با ارور مواجه میشوید.
2- برخلاف متغیّرها, ثوابترا میتوان در تمام 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 محلّی(مثل بدنهی یک تابع) باشند.
3- شما نمیتوانید یک ثابترا با خروجی یک تابع مقدار دهی کنید. امّا حتّی یک متغیّر غیرقابل تغییر را هم میتوان برای اوّلین بار با خروجی یک تابع مقدار دهی کرد.
مثلاً کد زیر را درنظر بگیرید(فعلاً به 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;
}
اگر این برنامهرا کامپایل کنید هیچ اروری نخواهید گرفت.
4- مورد استفادهی متغیّر و ثابت با هم متفاوت است. ثابتها عموماً برای کنترل بخشهای مختلف برنامه استفاده میشوند و هدف استفاده از آنها جلوگیری از hardcode کردن چنین بخشهایی است. امّا متغیّرها مستقیماً برای نگهداری دادههای کاربردی و منطق برنامه استفاده میشوند.
هدف از immutable کردن متغیّرها در Rust چیست؟
حالا بیایید کمی به فلسفهی پشت این تصمیم سازندگان این زبان بپردازیم. چرا باید متغیّرهارا غیرقابل تغییر کنیم؟
مسئله به یک مشکل قدیمی و همیشگی برمیگردد. وقتی داریم یک برنامهی همروند(concurrent) مینویسم, خیلی وقتها حالتی به نام race condition پیش میآید.
در این حالت 2 یا چند بخش از برنامه که دارند به صورت همزمان اجرا میشوند, به صورت همزمان به یک بخش از دادهها دسترسی پیدا میکنند و آنرا تغییر میدهند. یعنی وسط کار یک thread, ناگهان thread بعدی به دادههای مشترک دسترسی پیدا میکند و آنرا تغییر میدهد. وقتی که thread اوّل میخواهد به کارش ادامه بدهد با دادهای که خراب شده است روبهرو میشود و در نتیجه حاصل کارش اشتباه خواهد شد.
حالا سازندگان Rust با غیرقابل تغییر کردن متغیّرها سعی داشتند که تا حد امکان ایمنی دادهها را در برنامههای همروند تضمین کنند. اینطوری هیچ کدام از thread ها نمیتوانند دادهی اصلیرا عوض کنند, پس اشکالی در کار بقیهی threadها پیش نمیآید.
امیدوارم که نوشتههای این جلسه واضح باشند. اگر بخشیرا متوجّه نشدید یا به نظرتان گنگ توضیح داده بودم در بخش نظرات بگویید تا نوشتهرا اصلاح کنم.
جلسهی بعدی:
جلسهی قبلی:
مطلبی دیگر از این انتشارات
کاتلین در برابر جاوا! آیا باید از کاتلین برای توسعه اندروید استفاده کنیم؟
مطلبی دیگر از این انتشارات
آموزش زبان برنامهنویسی Rust - قسمت5: حلقه ها + تمرین
مطلبی دیگر از این انتشارات
ریفکتورینگ - بدهی فنی ( Refactoring - Technical debt) – بخش دوم