زبان برنامه‌نویسی Rust: مفاهیم متداول برنامه نویسی

Rust Programming Language - Common Programming Concepts
Rust Programming Language - Common Programming Concepts


مقدمه

در نوشته قبل کمی از نقاط قوت زبان 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 یک وارنینگ میده و به شما میگه فایل رو تغییر نام بدید. حالا می‌تونیم کدهای زیر رو به فایلی که ساختیم اضافه کنیم:

A program that prints Hello, world!
A program that prints Hello, world!

حالا به خط فرمان میریم و با دستور rustc برنامه رو کامپایل میکنیم و بعد هم اجراش می‌کنیم:

Running the first program
Running the first program


می‌بینید که برنامه ما اجرا میشه و متنی که وارد کرده بودیم، به درستی در کنسول پرینت میشه.




آناتومی یک برنامه Rust

در اینجا می‌خواهیم کد بالا رو با جزئیات بررسی کنیم:

main function
main function

این خطوط یک تابع رو در زبان Rust تعریف می‌کنند. تابع main یک تابع خاص هست; همیشه اولین کدی که در هر برنامه اجرایی Rust اجرا میشه، تابع main هست;پس ما اینجا تابعی به نام main تعریف کردیم که هیچ پارامتری نداره و هیچ مقداری رو هم بر نمی‌گردونه. و همون‌طور که می‌بینید بدنه تابع داخل کرلی براکت (curly brackets) ها قرار میگیره.

داخل این تابع هم کد زیر رو داریم:

println macro
println macro

این خط تمام کارها رو در برنامه کوچک ما انجام میده و متن رو در کنسول چاپ میکنه. اما نکات کوچکی هم وجود داره... مثلا:

  • برای 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 شما را ترغیب به تغییر ناپذیری می کنه. قطعه کد زیر رو در نظر بگیرید:

Variables are immutable by default
Variables are immutable by default

اگر سعی کنید این کد رو کامپایل کنید، کامپایلر خطای زیر رو به شما نمایش میده:

Compile error: cannot assign Twice to Immutable variable
Compile error: cannot assign Twice to Immutable variable

این مثال نشون میده که کامپایلر چطوری برای یافتن خطاها به شما کمک میکنه. این خطا به این علت پیش میاد که: ما سعی داریم متغیری رو که قابل تغییر نیست، تغییر بدیم.

برای تعریف کردن متغیر های قابل تغییر در Rust باید از کلیدواژه mut استفاده کرد. پس اگر بخواهیم متغیر x در کد بالا تغییرپذیر باشه باید به شکل زیر عمل کنیم:

Defining a Mutable variable called x
Defining a Mutable variable called x

و این کد بدون مشکل کامپایل و اجرا میشه:

Compiles with no errors
Compiles with no errors


عدم توانایی در تغییر مقدار یک متغیر ممکنه شما رو یاد مفهوم دیگه ای به نام ثابت ها (Constants) در زبان های برنامه نویسی بندازه... درست مثل متغیرهای تغییر ناپذیر ، ثابت (Constant) ها مقادیری هستند که به یک اسم محدود شده اند و اجازه تغییر ندارند ، اما بین ثابت ها و متغیرها تفاوت های کمی هم وجود داره:

  • برای ثابت‌ها نمی‌تونید از mut استفاده کنید.. چونکه ثابت ها کلا قابل تغییر نیستند، نه اینکه به طور پیش فرض قابل تغییر نباشند.
  • ثابت ها رو با کلیدواژه const تعریف میکنیم و نوع (Type) مقدارشون باید مشخص باشه.
  • ثابت ها رو میشه در هر دامنه ای (حتی گلوبال اسکوپ) تعریف کرد.
  • ثابت ها نمیتونند مقادیری مثل فانکشن کال ها یا هر مقدار دیگه که در حین Run-time محاسبه میشه رو در خودشون ذخیره کنند.

به شکل زیر میتونیم یک Constant یا مقدار ثابت رو در Rust تعریف کنیم:

Declaring a CONSTANT
Declaring a CONSTANT

در اینجا ما مقدار ثابتی رو به نام MAX_POINTS داریم که نوع داده اون برابر با u32 (Unsigned 32-bit) هست و مقدار اون برابر با 100،000 هست. برای خوانایی بیشتر می‌تونیم در اعداد از آندر اسکور هم استفاده کنیم.

در زمینه متغیر ها همچنین مفهوم Variables Shadowing رو داریم. هر گاه متغیری تعریف شده باشه و دوباره اون رو با کلیدواژه let مقداردهی کنیم، Shadowing اتفاق میفته.. که در تصویر زیر اون رو توضیح میدیم:

Variables Shadowing
Variables Shadowing

در اینجا ابتدا مقدار x رو برابر با ۵ قرار میدیم. در خط بعد با استفاده دوباره از let ، متغیر اول اصطلاحا در سایه متغیر دوم قرار میگیره (first variable is shadowed by the second).. پس مقدار x برابر با ۶ میشه و در ادامه همون پروسه تکرار میشه و نهایتا مقدار x برابر با ۱۲ میشه. پس اگر این برنامه رو اجرا کنیم.. نتیجه زیر رو خواهیم داشت:

Variables Shadowing
Variables Shadowing

این مفهوم کمی با استفاده از mut فرق داره و اگر سعی کنیم مقدار متغیر رو بدون استفاده از let عوض کنیم، خطای کامپایل خواهیم داشت. تفاوت دیگرش اینه که وقتی داریم از let دوباره استفاده می‌کنیم، میتونیم نوع متغیر رو عوض کنیم اما از همون نام قبلی براش استفاده کنیم.



انواع داده ها در Rust


تمام داده ها در Rust یک نوع (Type) مشخص دارند.. که ما در اینجا ۲ نوع اصلی اسکِیلار (Scalar) و مرکب (Compound) رو بررسی میکنیم. قبلا هم گفتیم Rust یک زبان Statically Typed هست; پس نوع تمام متغیر ها در زمان کامپایل باید مشخص باشه. کامپایلر Rust بر اساس مقادیر و نحوه استفاده ما، معمولا میتونه نوع داده رو تشخیص بده.. اما در مواردی که احتمالات مختلف وجود داره، ما باید خودمون نوع اون داده رو مشخص کنیم.

در Rust ما ۴ نوع داده از نوع Scalar داریم: اعداد صحیح، اعداد اعشاری، Boolean ها و کاراکتر ها.

انواع اعداد صحیح

Integer Types in Rust
Integer Types in Rust

انواع اعداد صحیح در Rust در جدول بالا جا می‌گیرند، که شامل اعداد مثبت و منفی با اندازه های مختلف میشه.

اعداد منفی می‌تونند اعداد رو از بازه (منهای ۲ به توان n-1) تا (۲ به توان n-1 منهای ۱) در خودشون ذخیره کنند و n تعداد بیت ها هست. پس یک نوع داده i8 شامل اعداد ۱۲۸- تا ۱۲۷ میشه.

اعداد مثبت هم می‌تونند اعداد رو از بازه 0 تا (۲ به توان n منهای ۱) در خودشون ذخیره کنند. پس یک نوع داده u8 شامل اعداد ۰ تا ۲۵۵ میشه.

انواع isize و usize هم بر اساس نوع معماری سیستم عاملی که برنامه داره درش اجرا میشه، مشخص میشند. اگر معماری سیستم شما ۶۴ بیت باشه پس نوع داده شما هم ۶۴ بیت و اگر ۳۲ بیت باشه، نوع داده ۳۲ بیت هست.


انواع اعداد اعشاری

دو نوع داده اعشاری در Rust داریم که شامل f32 و f64 میشه. همونطور که از نامشون مشخصه این دو نوع در اندازه های ۳۲ و ۶۴ بیتی هستند.

Floating Point Types
Floating Point Types

این انواع مطابق با استاندارد IEEE-754 هستند که میتونید اون رو بررسی کنید.

محاسبات ریاضی معمول در Rust روی تمام انواع داده های عددی ممکنه.. در تصویر زیر هر عبارت از یک عملگر ریاضی استفاده می کنه و یک مقدار واحد را ارزیابی می کنه ، که سپس به یک متغیر محدود میشه.

Numeric Operations
Numeric Operations


نوع Boolean

مثل سایر زبان های برنامه نویسی، در زبان Rust هم نوع داده Boolean دو مقدار ممکن داره: true یا false که ۱ بایت حجمشون هست و اونها رو با bool مشخص می‌کنیم.

Define a Boolean with explicit type annotation
Define a Boolean with explicit type annotation


نوع کاراکتر

برای ذخیره کاراکتر ها در Rust از سینگل کوتیشن استفاده میشه. این نوع داده ۴ بایت حجمش هست و شامل یک مقدار Unicode میشه که باعث میشه محدودیتی به انواع Ascii نداشته باشیم و بتونیم از کاراکترهای زبانی های مختلف (فارسی، چینی و ...) استفاده کنیم. در نوشته های بعد به شکل کاملتر کار با رشته ها رو بررسی می‌کنیم.

The Character Type
The Character Type


بعد از انواع Scalar حالا به ۲ نوع داده Compound می‌رسیم.

داده های مرکب می‌تونند چندین مقدار رو در یک نوع خاص گروه بندی کنند، که این داده ها شامل ۲ نوع تاپل (Tuple) و آرایه (Array) میشند.

نوع Tuple

برای گروه‌بندی مقادیری با نوع‌ های مختلف می‌تونیم از تاپل ها استفاده کنیم:

Tuple with optional type annotations
Tuple with optional type annotations

برای دسترسی به مقادیر داخل تاپل میتونیم از تطبیق الگو (pattern matching) استفاده کنیم:

Pattern Matching
Pattern Matching

در واقع به کاری که در بالا انجام دادیم destructuring میگند. مقادیر ما به ترتیب در متغیر های y , x و z قرار می‌گیرند. پس مقدار y برابر با ۶.۴ هست... ما همچنین می‌تونیم به شکل مستقیم هم به المان های یک تاپل دسترسی داشته باشیم: با استفاده از یک نقطه و شماره المان مورد نظر (کم و بیش شبیه آرایه ها):

Using respective indices
Using respective indices


نوع Array

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

Declaring an Array
Declaring an Array

اگر نیاز داشتیم نوع المان های یک ارایه رو مشخص کنیم، به شکل زیر:

Declaring an Array with Type and Size
Declaring an Array with Type and Size


در اینجا برای هر المان از نوع داده i32 استفاده کردیم، و عدد 5 نشان دهنده تعداد المان های این آرایه هست.. و اما برای دسترسی به المان های یک آرایه میتونید از فهرست بندی استفاده کنید:

Accessing Array's Elements
Accessing Array's Elements


ما همچنین وکتور (Vector) ها رو داریم که توسط کتابخانه استاندارد Rust ارائه میشند و قابلیت کوچک یا بزرگ شدن رو هم دارند، که اونها رو هم بررسی خواهیم کرد.



توابع در Rust

تا اینجا با یکی از مهمترین تابع های زبان Rust آشنا شدیم. تابع main که نقطه شروع (entry point) اپلیکیشن ما هست. همچنین با کلیدواژه fn آشنا شدیم، که با کمکش توابع رو تعریف میکنیم. برای نام گذاری هم مثل متغیرها پیشنهاد میشه از snake_case استفاده کنیم.

پارامتر یا آرگومان؟

هنگامی که یک تابع پارامتر هایی رو داره، شما میتونید مقادیری concrete رو به اون پارامترها پاس بدید و از لحاظ فنی مقادیر concrete رو آرگومان می‌گیم. اما در گفتگوهای معمول توسعه دهنده ها خیلی عادی هست که: از ۲ واژه پارامتر و آرگومان برای مفاهیمی مثل (متغیرهایی که در تعریف یک تابع داریم) و (پاس دادن مقادیر concrete در حین فراخوانی یک تابع) به شکل متناوب استفاده بشه.

Functions with specified Typed parameters
Functions with specified Typed parameters

در تابع بالا ما ۲ پارامتر داریم که نوع هر کدوم مشخص هست و نتیجه زیر رو برمیگردونه:

Results of another_function
Results of another_function

نوع برگشتی توابع

برای برگشت دادن نوع داده ای خاص از یک تابع، میتونیم به شکل زیر عمل کنیم:

Functions with Return Values
Functions with Return Values


تابع ما در بالا یک پارامتر از نوع i32 می‌پذیره و نوع برگشتی این تابع هم از نوع i32 هست. داخل بدنه تابع plus_one قسمت x + 1 در واقع یک عبارت (expression) هست.. برای همین از ; استفاده نکردیم . اکر از سمی کالن استفاده کنیم این قسمت از کدمون تبدیل به یک استیتمنت (statement) میشه که بدون استفاده از کلیدواژه return باعث میشه با خطا روبرو شیم.



عبارات شرطی در Rust

عبارت های شرطی یک جز اصلی از هر زبان برنامه نویسی هستند; که در Rust هم مثل سایر زبان ها به صورت ۳ عبارت else , if و else if قابل دسترسی و استفاده هستند. در مثال زیر از هر ۳ نوع عبارت استفاده شده تا تقسیم پذیری یک عدد رو به عددهای ۲، ۳ و ۴ بررسی کنیم:

Handling Multiple Conditions with else if
Handling Multiple Conditions with else if

که خروجی این کد به شکل زیر خواهد بود:

Handling Multiple Conditions Results
Handling Multiple Conditions Results

با وجودی که عدد ۶ هم به ۳ و هم به ۲ تقسیم‌پذیر هست، اما خروجی برنامه ما عدد ۳ رو نشون میده. علتش هم اینه که وقتی Rust به یه کاندیشن صحیح میرسه اون رو اجرا میکنه و دیگه چیزی رو چک نمیکنه.

استفاده از if در یک Statement

چون if یک expression هست.. ما میتونیم در سمت راست یک let statement ازش استفاده کنیم:

Using if in a let Statement
Using if in a let Statement



انواع حلقه ها در Rust

حلقه ها اکثرا استفاده های کاربردی زیادی در کدهای ما دارند و در Rust ما ۳ نوع حلقه داریم: loop, while و for.

تکرار کد با loop

کلیدواژه loop به Rust میگه که یه قطعه کد رو دائما تکرار کنه; مگر اینکه شما از کلیدواژه break در داخل حلقه استفاده کنید، و بهش بگید دست نگه داره.

Repeating Code with loop
Repeating Code with loop


در کد بالا، ما یک متغیر (counter) از نوع Mutable با مقدار ۰ تعریف کردیم. بعد داخل حلقه مرتبا داریم مقدار متغیر رو افزایش میدیم و چک میکنیم که آیا مقدارش به عدد ۱۰ رسیده یا نه؟ با رسیدن به عدد ۱۰ از break استفاده میکنیم و مقدار counter رو دو برابر میکنیم و در داخل متغیر قرار میدیم. پس result برابر با عدد ۲۰ خواهد بود.

حلقه های شرطی با while

این حلقه ها بسیار ساده هستند و مادامی که شرط برقرار باشه اجرا میشند و سپس به پایان میرسند:

Conditional Loops with while, countdown example
Conditional Loops with while, countdown example

در کد بالا، عدد ۳ رو داخل حلقه قرار میدیم و عدد رو پرینت میکنیم و هر بار مقدارش رو یکی کاهش میدیم. و هنگامی که به عدد ۰ برسیم دیگه شرط صادق نیست و حلقه به پایان میرسه.


پیمایش یک کالکشن با for

در مثال زیر با استفاده از حلقه for اعداد داخل آرایه رو پیمایش و پرینت میکنیم:

Looping Through a Collection with for
Looping Through a Collection with for

امنیت و خاص بودن حلقه های for اونها رو به رایج ترین حلقه ها در Rust تبدیل می‌کنند. حتی در مواقعی که می‌خواهیم یک کد رو به تعداد مشخصی تکرار کنیم.. به طور مثال شمارش معکوسی که در مثال حلقه while داشتیم رو میتونیم با استفاده از for و به راحتی پیاده سازی کنیم:

Countdown example with for and rev
Countdown example with for and rev

راجب rev هنوز صحبت نکردیم، اما می‌بینید که کد بالا کوتاه‌تر و جالب تر از استفاده از while هست.




نتیجه گیری

بسار خوب.. در این نوشته با مفاهیم معمول برنامه نویسی در زبان Rust آشنا شدیم. در نوشته بعد مفاهیم کلیدی Ownership و Borrowing رو در مدیریت حافظه Rust بررسی می‌کنیم; و اینکه چطور این مفاهیم، زبان Rust رو از وجود هر گونه گاربج کالکشنی (Garbage Collection) بی نیاز می‌کنه و حافظه رو با امنیت بالا مدیریت می‌کنه.