توسعه دهنده ارشد وب
زبان برنامهنویسی Rust: مفاهیم متداول برنامه نویسی
مقدمه
در نوشته قبل کمی از نقاط قوت زبان Rust گفتیم و خیلی ساده اون رو نصب کردیم.. در این نوشته اولین برنامه رو با زبان Rust رو مینویسیم و آناتومی اون رو مورد بررسی قرار میدیم.
در این حین کمی با پکیج منیجر زبان Rust به نام Cargo آشنا میشیم.
سپس به سراغ مفاهیم متداول برنامه نویسی میریم که لیست اونها رو در زیر میبینید:
- متغیر ها و تغییر پذیری اونها
- انواع داده های Scalar
- انواع داده های Compound
- توابع در Rust
- عبارات شرطی در Rust
- انواع حلقه ها در Rust
نوشتن اولین برنامه Rust و اجرای آن
وقتی داریم یک زبان برنامه نویسی جدید یاد میگیریم، نوشتن یک برنامه که کلمات "!Hello, world" رو چاپ میکنه بسیار مرسوم هست. از زمانی که اولین نسخه زبان C نوشته شد، نوشتن برنامه Hello world راهی برای آزمایش کردن کامپایلر و نوشتن یک برنامه واقعی بوده. پس ما هم از این روش پیروی میکنیم.
در هر آدرسی که دوست دارید میتونید یک فولدر projects
برای ذخیره برنامه های Rust بسازید. سپس داخل این فولدر، به یک فولدر پروژه به نام hello_world
نیاز داریم. حالا یک سورس فایل جدید در فولدر پروژه میسازیم و نامش رو main.rs
میگذاریم. فایلهای Rust همیشه پسوند rs.
رو دارند و اگر نام فایل بیش از یک کلمه هست بهتره کلمات رو با آندر اِسکور از هم جدا کنید. مثلا بجای helloworld.rs
از hello_world.rs
استفاده کنید. در غیر این صورت کامپایلر Rust یک وارنینگ میده و به شما میگه فایل رو تغییر نام بدید. حالا میتونیم کدهای زیر رو به فایلی که ساختیم اضافه کنیم:
حالا به خط فرمان میریم و با دستور rustc
برنامه رو کامپایل میکنیم و بعد هم اجراش میکنیم:
میبینید که برنامه ما اجرا میشه و متنی که وارد کرده بودیم، به درستی در کنسول پرینت میشه.
آناتومی یک برنامه Rust
در اینجا میخواهیم کد بالا رو با جزئیات بررسی کنیم:
این خطوط یک تابع رو در زبان Rust تعریف میکنند. تابع main
یک تابع خاص هست; همیشه اولین کدی که در هر برنامه اجرایی Rust اجرا میشه، تابع main
هست;پس ما اینجا تابعی به نام main
تعریف کردیم که هیچ پارامتری نداره و هیچ مقداری رو هم بر نمیگردونه. و همونطور که میبینید بدنه تابع داخل کرلی براکت (curly brackets) ها قرار میگیره.
داخل این تابع هم کد زیر رو داریم:
این خط تمام کارها رو در برنامه کوچک ما انجام میده و متن رو در کنسول چاپ میکنه. اما نکات کوچکی هم وجود داره... مثلا:
- برای Indent کردن درRust از ۴ تا اسپیس استفاده میشه و نه از تَب.
- علامت تعجب
!
بیانگر این هست، که داریم از یک ماکرو استفاده میکنیم و نه یک تابع. - کلمات ما (Hello world) به عنوان یک پارامتر به ماکرو پاس داده میشند و چاپ میشند.
- استیتمنت های ما با یک سمی کالن
;
به پایان میرسند.
جدا بودن مراحل کامپایل و اجرا
قبل از اجرای یک برنامه Rust باید ابتدا توسط کامپایلر (rustc) برنامه رو کامپایل کنید. اگر پیش زمینه ای از زبانهای C یا ++C داشته باشید، متوجه میشید که این فرایند شبیه به استفاده از gcc
یا clang
هست. بعد از یک کامپایل موفق، Rust یک فایل باینری قابل اجرا برای ما میسازه.
اگر با زبانهای داینامیک مثل Javascript, Ruby, Python, PHP بیشتر سر و کار داشته اید، ممکنه به کامپایل کردن و اجرای برنامه در مراحل جداگانه عادت نداشته باشید. پروسه کامپایل در Rust از نوع Ahead of Time هست. به این معنی که شما میتونید یک برنامه رو کامپایل کنید و فایل اجرایی رو به شخص دیگه ای بدید... و اون شخص برای اجرای برنامه شما نیازی نداره که Rust رو نصب کرده باشه. اما از طرف مقابل اگر شما یک فایل php. یا py, .rb, .js. رو به شخصی بدید، اون شخص برای اجرای کدهای شما نیاز به مفسر (interpreter) مربوط به اون زبان داره.
کامپایل کردن برنامه های ساده با rustc
یک راه حل خوب هست.. اما با بزرگ شدن پروژه بهتره از پکیج منیجر زبان Rust به نام Cargo استفاده کنیم.
پکیج منیجر Cargo
یکی از مفاهیمی که امروزه در اکثر زبان ها یافت میشه، پکیج منیجر هست. با رشد کردن اکو سیستم هر زبان و ساخته شدن پکیج های زیادی که میتونید به راحتی در پروژهتون استفاده کنید، سر و کله پکیج منیجر ها هم پیدا شده.. هر توسعه دهنده Javascript معمولا از npm استفاده میکنه; PHP کارها از Composer ... روبی کارها از RubyGems و الا آخر.. با استفاده از این ابزارها میتونیم وابستگی های پروژه رو مدیریت کنیم. ابزار Cargo هم پکیج منیجر زبان Rust هست. که با استفاده ازش میتونیم به راحتی یک پروژه جدید بسازیم:
cargo new new_project
این دستور یک فولدر به نام new_project میسازه که داخلش یک فایل Cargo.toml هست و یه فولدر src که داخلش یک فایل main.rs قرار گرفته. (داخل فولدر پروژه یک فایل gitignore هم دیده میشه).
فایل Cargo.toml
شامل وابستگی های پروژه ما میشه. میتونید مثل فایل package.json
در یک پروژه جاوااسکریپتی بهش نگاه کنید. فایل Cargo.toml در فرمت TOML (Tom’s Obvious, Minimal Language) هست که میتونید در گیت اون رو بررسی کنید. از مهمترین دستوراتی که Cargo در اختیارمون قرار میده، میشه به موارد زیر اشاره کرد:
cargo build
این دستور مرحله کامپایل رو برای ما انجام میده
cargo run
و دستور run پروسه کامپایل رو انجام و سپس برنامه رو اجرا میکنه
cargo check
و دستور check رو داریم که قابلیت کامپایل شدن کد شما رو بررسی میکنه
و در پایان هنگامی که پروژه شما آماده انتشار میشه میتونید از دستور زیر استفاده کنید:
cargo build --release
این دستور باعث میشه پروسه کامپایل با یک سری بهینه سازیها همراه بشه، که این بهینه سازیها باعث میشند کد شما سریعتر اجرا بشه اما پروسه کامپایل کمی بیشتر زمان ببره.
متغیر ها و تغییر پذیری
برای تعریف یک متغیر در Rust باید از کلیدواژه let
استفاده کنیم; نکته ای که وجود داره اینه که: در زبان Rust، متغیر ها به شکل پیش فرض غیر قابل تغییر (immutable) هستند. با این حال، شما هنوز هم این گزینه رو دارید که متغیرهای خودتون را تغییر پذیر (mutable) کنید. بیاید بررسی کنیم که چگونه و چرا Rust شما را ترغیب به تغییر ناپذیری می کنه. قطعه کد زیر رو در نظر بگیرید:
اگر سعی کنید این کد رو کامپایل کنید، کامپایلر خطای زیر رو به شما نمایش میده:
این مثال نشون میده که کامپایلر چطوری برای یافتن خطاها به شما کمک میکنه. این خطا به این علت پیش میاد که: ما سعی داریم متغیری رو که قابل تغییر نیست، تغییر بدیم.
برای تعریف کردن متغیر های قابل تغییر در Rust باید از کلیدواژه mut
استفاده کرد. پس اگر بخواهیم متغیر x در کد بالا تغییرپذیر باشه باید به شکل زیر عمل کنیم:
و این کد بدون مشکل کامپایل و اجرا میشه:
عدم توانایی در تغییر مقدار یک متغیر ممکنه شما رو یاد مفهوم دیگه ای به نام ثابت ها (Constants) در زبان های برنامه نویسی بندازه... درست مثل متغیرهای تغییر ناپذیر ، ثابت (Constant) ها مقادیری هستند که به یک اسم محدود شده اند و اجازه تغییر ندارند ، اما بین ثابت ها و متغیرها تفاوت های کمی هم وجود داره:
- برای ثابتها نمیتونید از mut استفاده کنید.. چونکه ثابت ها کلا قابل تغییر نیستند، نه اینکه به طور پیش فرض قابل تغییر نباشند.
- ثابت ها رو با کلیدواژه
const
تعریف میکنیم و نوع (Type) مقدارشون باید مشخص باشه. - ثابت ها رو میشه در هر دامنه ای (حتی گلوبال اسکوپ) تعریف کرد.
- ثابت ها نمیتونند مقادیری مثل فانکشن کال ها یا هر مقدار دیگه که در حین Run-time محاسبه میشه رو در خودشون ذخیره کنند.
به شکل زیر میتونیم یک Constant یا مقدار ثابت رو در Rust تعریف کنیم:
در اینجا ما مقدار ثابتی رو به نام MAX_POINTS داریم که نوع داده اون برابر با u32 (Unsigned 32-bit) هست و مقدار اون برابر با 100،000 هست. برای خوانایی بیشتر میتونیم در اعداد از آندر اسکور هم استفاده کنیم.
در زمینه متغیر ها همچنین مفهوم Variables Shadowing رو داریم. هر گاه متغیری تعریف شده باشه و دوباره اون رو با کلیدواژه let مقداردهی کنیم، Shadowing اتفاق میفته.. که در تصویر زیر اون رو توضیح میدیم:
در اینجا ابتدا مقدار x رو برابر با ۵ قرار میدیم. در خط بعد با استفاده دوباره از let ، متغیر اول اصطلاحا در سایه متغیر دوم قرار میگیره (first variable is shadowed by the second).. پس مقدار x برابر با ۶ میشه و در ادامه همون پروسه تکرار میشه و نهایتا مقدار x برابر با ۱۲ میشه. پس اگر این برنامه رو اجرا کنیم.. نتیجه زیر رو خواهیم داشت:
این مفهوم کمی با استفاده از mut فرق داره و اگر سعی کنیم مقدار متغیر رو بدون استفاده از let عوض کنیم، خطای کامپایل خواهیم داشت. تفاوت دیگرش اینه که وقتی داریم از let دوباره استفاده میکنیم، میتونیم نوع متغیر رو عوض کنیم اما از همون نام قبلی براش استفاده کنیم.
انواع داده ها در Rust
تمام داده ها در Rust یک نوع (Type) مشخص دارند.. که ما در اینجا ۲ نوع اصلی اسکِیلار (Scalar) و مرکب (Compound) رو بررسی میکنیم. قبلا هم گفتیم Rust یک زبان Statically Typed هست; پس نوع تمام متغیر ها در زمان کامپایل باید مشخص باشه. کامپایلر Rust بر اساس مقادیر و نحوه استفاده ما، معمولا میتونه نوع داده رو تشخیص بده.. اما در مواردی که احتمالات مختلف وجود داره، ما باید خودمون نوع اون داده رو مشخص کنیم.
در Rust ما ۴ نوع داده از نوع Scalar داریم: اعداد صحیح، اعداد اعشاری، Boolean ها و کاراکتر ها.
انواع اعداد صحیح
انواع اعداد صحیح در Rust در جدول بالا جا میگیرند، که شامل اعداد مثبت و منفی با اندازه های مختلف میشه.
اعداد منفی میتونند اعداد رو از بازه (منهای ۲ به توان n-1) تا (۲ به توان n-1 منهای ۱) در خودشون ذخیره کنند و n تعداد بیت ها هست. پس یک نوع داده i8
شامل اعداد ۱۲۸- تا ۱۲۷ میشه.
اعداد مثبت هم میتونند اعداد رو از بازه 0 تا (۲ به توان n منهای ۱) در خودشون ذخیره کنند. پس یک نوع داده u8
شامل اعداد ۰ تا ۲۵۵ میشه.
انواع isize
و usize
هم بر اساس نوع معماری سیستم عاملی که برنامه داره درش اجرا میشه، مشخص میشند. اگر معماری سیستم شما ۶۴ بیت باشه پس نوع داده شما هم ۶۴ بیت و اگر ۳۲ بیت باشه، نوع داده ۳۲ بیت هست.
انواع اعداد اعشاری
دو نوع داده اعشاری در Rust داریم که شامل f32
و f64
میشه. همونطور که از نامشون مشخصه این دو نوع در اندازه های ۳۲ و ۶۴ بیتی هستند.
این انواع مطابق با استاندارد IEEE-754 هستند که میتونید اون رو بررسی کنید.
محاسبات ریاضی معمول در Rust روی تمام انواع داده های عددی ممکنه.. در تصویر زیر هر عبارت از یک عملگر ریاضی استفاده می کنه و یک مقدار واحد را ارزیابی می کنه ، که سپس به یک متغیر محدود میشه.
نوع Boolean
مثل سایر زبان های برنامه نویسی، در زبان Rust هم نوع داده Boolean دو مقدار ممکن داره: true یا false که ۱ بایت حجمشون هست و اونها رو با bool
مشخص میکنیم.
نوع کاراکتر
برای ذخیره کاراکتر ها در Rust از سینگل کوتیشن استفاده میشه. این نوع داده ۴ بایت حجمش هست و شامل یک مقدار Unicode میشه که باعث میشه محدودیتی به انواع Ascii نداشته باشیم و بتونیم از کاراکترهای زبانی های مختلف (فارسی، چینی و ...) استفاده کنیم. در نوشته های بعد به شکل کاملتر کار با رشته ها رو بررسی میکنیم.
بعد از انواع Scalar حالا به ۲ نوع داده Compound میرسیم.
داده های مرکب میتونند چندین مقدار رو در یک نوع خاص گروه بندی کنند، که این داده ها شامل ۲ نوع تاپل (Tuple) و آرایه (Array) میشند.
نوع Tuple
برای گروهبندی مقادیری با نوع های مختلف میتونیم از تاپل ها استفاده کنیم:
برای دسترسی به مقادیر داخل تاپل میتونیم از تطبیق الگو (pattern matching) استفاده کنیم:
در واقع به کاری که در بالا انجام دادیم destructuring میگند. مقادیر ما به ترتیب در متغیر های y , x و z قرار میگیرند. پس مقدار y
برابر با ۶.۴ هست... ما همچنین میتونیم به شکل مستقیم هم به المان های یک تاپل دسترسی داشته باشیم: با استفاده از یک نقطه و شماره المان مورد نظر (کم و بیش شبیه آرایه ها):
نوع Array
یک راه دیگه برای داشتن مجموعه ای از مقادیر متعدد ، یک آرایه است. بر خلاف تاپل ها عناصر داخل یک آرایه باید از یک نوع باشند. آرایه ها در Rust با سایر زبان ها کمی تفاوت دارند چون آرایه ها هم مثل تاپل ها در Rust اندازه مشخصی دارند; آرایه ها زمانی مفید هستند که می خواهید تعداد ثابتی از عناصر را داشته باشید.
اگر نیاز داشتیم نوع المان های یک ارایه رو مشخص کنیم، به شکل زیر:
در اینجا برای هر المان از نوع داده i32
استفاده کردیم، و عدد 5
نشان دهنده تعداد المان های این آرایه هست.. و اما برای دسترسی به المان های یک آرایه میتونید از فهرست بندی استفاده کنید:
ما همچنین وکتور (Vector) ها رو داریم که توسط کتابخانه استاندارد Rust ارائه میشند و قابلیت کوچک یا بزرگ شدن رو هم دارند، که اونها رو هم بررسی خواهیم کرد.
توابع در Rust
تا اینجا با یکی از مهمترین تابع های زبان Rust آشنا شدیم. تابع main
که نقطه شروع (entry point) اپلیکیشن ما هست. همچنین با کلیدواژه fn
آشنا شدیم، که با کمکش توابع رو تعریف میکنیم. برای نام گذاری هم مثل متغیرها پیشنهاد میشه از snake_case
استفاده کنیم.
پارامتر یا آرگومان؟
هنگامی که یک تابع پارامتر هایی رو داره، شما میتونید مقادیری concrete رو به اون پارامترها پاس بدید و از لحاظ فنی مقادیر concrete رو آرگومان میگیم. اما در گفتگوهای معمول توسعه دهنده ها خیلی عادی هست که: از ۲ واژه پارامتر
و آرگومان
برای مفاهیمی مثل (متغیرهایی که در تعریف یک تابع داریم
) و (پاس دادن مقادیر concrete در حین فراخوانی یک تابع
) به شکل متناوب استفاده بشه.
در تابع بالا ما ۲ پارامتر داریم که نوع هر کدوم مشخص هست و نتیجه زیر رو برمیگردونه:
نوع برگشتی توابع
برای برگشت دادن نوع داده ای خاص از یک تابع، میتونیم به شکل زیر عمل کنیم:
تابع ما در بالا یک پارامتر از نوع i32
میپذیره و نوع برگشتی این تابع هم از نوع i32
هست. داخل بدنه تابع plus_one
قسمت x + 1
در واقع یک عبارت (expression) هست.. برای همین از ;
استفاده نکردیم . اکر از سمی کالن استفاده کنیم این قسمت از کدمون تبدیل به یک استیتمنت (statement) میشه که بدون استفاده از کلیدواژه return
باعث میشه با خطا روبرو شیم.
عبارات شرطی در Rust
عبارت های شرطی یک جز اصلی از هر زبان برنامه نویسی هستند; که در Rust هم مثل سایر زبان ها به صورت ۳ عبارت else
, if
و else if
قابل دسترسی و استفاده هستند. در مثال زیر از هر ۳ نوع عبارت استفاده شده تا تقسیم پذیری یک عدد رو به عددهای ۲، ۳ و ۴ بررسی کنیم:
که خروجی این کد به شکل زیر خواهد بود:
با وجودی که عدد ۶ هم به ۳ و هم به ۲ تقسیمپذیر هست، اما خروجی برنامه ما عدد ۳ رو نشون میده. علتش هم اینه که وقتی Rust به یه کاندیشن صحیح میرسه اون رو اجرا میکنه و دیگه چیزی رو چک نمیکنه.
استفاده از if در یک Statement
چون if یک expression هست.. ما میتونیم در سمت راست یک let statement ازش استفاده کنیم:
انواع حلقه ها در Rust
حلقه ها اکثرا استفاده های کاربردی زیادی در کدهای ما دارند و در Rust ما ۳ نوع حلقه داریم: loop
, while
و for
.
تکرار کد با loop
کلیدواژه loop
به Rust میگه که یه قطعه کد رو دائما تکرار کنه; مگر اینکه شما از کلیدواژه break
در داخل حلقه استفاده کنید، و بهش بگید دست نگه داره.
در کد بالا، ما یک متغیر (counter) از نوع Mutable با مقدار ۰ تعریف کردیم. بعد داخل حلقه مرتبا داریم مقدار متغیر رو افزایش میدیم و چک میکنیم که آیا مقدارش به عدد ۱۰ رسیده یا نه؟ با رسیدن به عدد ۱۰ از break استفاده میکنیم و مقدار counter رو دو برابر میکنیم و در داخل متغیر قرار میدیم. پس result برابر با عدد ۲۰ خواهد بود.
حلقه های شرطی با while
این حلقه ها بسیار ساده هستند و مادامی که شرط برقرار باشه اجرا میشند و سپس به پایان میرسند:
در کد بالا، عدد ۳ رو داخل حلقه قرار میدیم و عدد رو پرینت میکنیم و هر بار مقدارش رو یکی کاهش میدیم. و هنگامی که به عدد ۰ برسیم دیگه شرط صادق نیست و حلقه به پایان میرسه.
پیمایش یک کالکشن با for
در مثال زیر با استفاده از حلقه for اعداد داخل آرایه رو پیمایش و پرینت میکنیم:
امنیت و خاص بودن حلقه های for اونها رو به رایج ترین حلقه ها در Rust تبدیل میکنند. حتی در مواقعی که میخواهیم یک کد رو به تعداد مشخصی تکرار کنیم.. به طور مثال شمارش معکوسی که در مثال حلقه while داشتیم رو میتونیم با استفاده از for و به راحتی پیاده سازی کنیم:
راجب rev
هنوز صحبت نکردیم، اما میبینید که کد بالا کوتاهتر و جالب تر از استفاده از while هست.
نتیجه گیری
بسار خوب.. در این نوشته با مفاهیم معمول برنامه نویسی در زبان Rust آشنا شدیم. در نوشته بعد مفاهیم کلیدی Ownership و Borrowing رو در مدیریت حافظه Rust بررسی میکنیم; و اینکه چطور این مفاهیم، زبان Rust رو از وجود هر گونه گاربج کالکشنی (Garbage Collection) بی نیاز میکنه و حافظه رو با امنیت بالا مدیریت میکنه.
مطلبی دیگر از این انتشارات
خرید مناسب ترین باتری موبایل از فونی شاپ
مطلبی دیگر از این انتشارات
آموزش پایتون-کلاس ها و Namespaces در Python
مطلبی دیگر از این انتشارات
مدیریت تغییرات دیتابیس با Liquibase