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

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!حالا به خط فرمان میریم و با دستور rustc برنامه رو کامپایل میکنیم و بعد هم اجراش می‌کنیم:Running the first programمی‌بینید که برنامه ما اجرا میشه و متنی که وارد کرده بودیم، به درستی در کنسول پرینت میشه.آناتومی یک برنامه Rustدر اینجا می‌خواهیم کد بالا رو با جزئیات بررسی کنیم:main functionاین خطوط یک تابع رو در زبان Rust تعریف می‌کنند. تابع main یک تابع خاص هست; همیشه اولین کدی که در هر برنامه اجرایی Rust اجرا میشه، تابع main هست;پس ما اینجا تابعی به نام main تعریف کردیم که هیچ پارامتری نداره و هیچ مقداری رو هم بر نمی‌گردونه. و همون‌طور که می‌بینید بدنه تابع داخل کرلی براکت (curly brackets) ها قرار میگیره.داخل این تابع هم کد زیر رو داریم: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اگر سعی کنید این کد رو کامپایل کنید، کامپایلر خطای زیر رو به شما نمایش میده:Compile error: cannot assign Twice to Immutable variableاین مثال نشون میده که کامپایلر چطوری برای یافتن خطاها به شما کمک میکنه. این خطا به این علت پیش میاد که: ما سعی داریم متغیری رو که قابل تغییر نیست، تغییر بدیم.برای تعریف کردن متغیر های قابل تغییر در Rust باید از کلیدواژه mut استفاده کرد. پس اگر بخواهیم متغیر x در کد بالا تغییرپذیر باشه باید به شکل زیر عمل کنیم:Defining a Mutable variable called xو این کد بدون مشکل کامپایل و اجرا میشه:Compiles with no errorsعدم توانایی در تغییر مقدار یک متغیر ممکنه شما رو یاد مفهوم دیگه ای به نام ثابت ها (Constants) در زبان های برنامه نویسی بندازه... درست مثل متغیرهای تغییر ناپذیر ، ثابت (Constant) ها مقادیری هستند که به یک اسم محدود شده اند و اجازه تغییر ندارند ، اما بین ثابت ها و متغیرها تفاوت های کمی هم وجود داره:برای ثابت‌ها نمی‌تونید از mut استفاده کنید.. چونکه ثابت ها کلا قابل تغییر نیستند، نه اینکه به طور پیش فرض قابل تغییر نباشند.ثابت ها رو با کلیدواژه const تعریف میکنیم و نوع (Type) مقدارشون باید مشخص باشه.ثابت ها رو میشه در هر دامنه ای (حتی گلوبال اسکوپ) تعریف کرد.ثابت ها نمیتونند مقادیری مثل فانکشن کال ها یا هر مقدار دیگه که در حین Run-time محاسبه میشه رو در خودشون ذخیره کنند.به شکل زیر میتونیم یک Constant یا مقدار ثابت رو در Rust تعریف کنیم:Declaring a CONSTANTدر اینجا ما مقدار ثابتی رو به نام MAX_POINTS داریم که نوع داده اون برابر با u32 (Unsigned 32-bit) هست و مقدار اون برابر با 100،000 هست. برای خوانایی بیشتر می‌تونیم در اعداد از آندر اسکور هم استفاده کنیم.در زمینه متغیر ها همچنین مفهوم Variables Shadowing رو داریم. هر گاه متغیری تعریف شده باشه و دوباره اون رو با کلیدواژه let مقداردهی کنیم، Shadowing اتفاق میفته.. که در تصویر زیر اون رو توضیح میدیم:Variables Shadowingدر اینجا ابتدا مقدار x رو برابر با ۵ قرار میدیم. در خط بعد با استفاده دوباره از let ، متغیر اول اصطلاحا در سایه متغیر دوم قرار میگیره (first variable is shadowed by the second).. پس مقدار x برابر با ۶ میشه و در ادامه همون پروسه تکرار میشه و نهایتا مقدار x برابر با ۱۲ میشه. پس اگر این برنامه رو اجرا کنیم.. نتیجه زیر رو خواهیم داشت:Variables Shadowingاین مفهوم کمی با استفاده از mut فرق داره و اگر سعی کنیم مقدار متغیر رو بدون استفاده از let عوض کنیم، خطای کامپایل خواهیم داشت. تفاوت دیگرش اینه که وقتی داریم از let دوباره استفاده می‌کنیم، میتونیم نوع متغیر رو عوض کنیم اما از همون نام قبلی براش استفاده کنیم.انواع داده ها در Rustتمام داده ها در Rust یک نوع (Type) مشخص دارند.. که ما در اینجا ۲ نوع اصلی اسکِیلار (Scalar) و مرکب (Compound) رو بررسی میکنیم. قبلا هم گفتیم Rust یک زبان Statically Typed هست; پس نوع تمام متغیر ها در زمان کامپایل باید مشخص باشه. کامپایلر Rust بر اساس مقادیر و نحوه استفاده ما، معمولا میتونه نوع داده رو تشخیص بده.. اما در مواردی که احتمالات مختلف وجود داره، ما باید خودمون نوع اون داده رو مشخص کنیم.در Rust ما ۴ نوع داده از نوع Scalar داریم: اعداد صحیح، اعداد اعشاری، Boolean ها و کاراکتر ها.انواع اعداد صحیحInteger Types in Rustانواع اعداد صحیح در Rust در جدول بالا جا می‌گیرند، که شامل اعداد مثبت و منفی با اندازه های مختلف میشه.اعداد منفی می‌تونند اعداد رو از بازه (منهای ۲ به توان n-1) تا (۲ به توان n-1 منهای ۱) در خودشون ذخیره کنند و n تعداد بیت ها هست. پس یک نوع داده i8 شامل اعداد ۱۲۸- تا ۱۲۷ میشه.اعداد مثبت هم می‌تونند اعداد رو از بازه 0 تا (۲ به توان n منهای ۱) در خودشون ذخیره کنند. پس یک نوع داده u8 شامل اعداد ۰ تا ۲۵۵ میشه.انواع isize و usize هم بر اساس نوع معماری سیستم عاملی که برنامه داره درش اجرا میشه، مشخص میشند. اگر معماری سیستم شما ۶۴ بیت باشه پس نوع داده شما هم ۶۴ بیت و اگر ۳۲ بیت باشه، نوع داده ۳۲ بیت هست.انواع اعداد اعشاریدو نوع داده اعشاری در Rust داریم که شامل f32 و f64 میشه. همونطور که از نامشون مشخصه این دو نوع در اندازه های ۳۲ و ۶۴ بیتی هستند.Floating Point Typesاین انواع مطابق با استاندارد IEEE-754 هستند که میتونید اون رو بررسی کنید.محاسبات ریاضی معمول در Rust روی تمام انواع داده های عددی ممکنه.. در تصویر زیر هر عبارت از یک عملگر ریاضی استفاده می کنه و یک مقدار واحد را ارزیابی می کنه ، که سپس به یک متغیر محدود میشه.Numeric Operationsنوع Booleanمثل سایر زبان های برنامه نویسی، در زبان Rust هم نوع داده Boolean دو مقدار ممکن داره: true یا false که ۱ بایت حجمشون هست و اونها رو با bool مشخص می‌کنیم.Define a Boolean with explicit type annotationنوع کاراکتربرای ذخیره کاراکتر ها در Rust از سینگل کوتیشن استفاده میشه. این نوع داده ۴ بایت حجمش هست و شامل یک مقدار Unicode میشه که باعث میشه محدودیتی به انواع Ascii نداشته باشیم و بتونیم از کاراکترهای زبانی های مختلف (فارسی، چینی و ...) استفاده کنیم. در نوشته های بعد به شکل کاملتر کار با رشته ها رو بررسی می‌کنیم.The Character Typeبعد از انواع Scalar حالا به ۲ نوع داده Compound می‌رسیم. داده های مرکب می‌تونند چندین مقدار رو در یک نوع خاص گروه بندی کنند، که این داده ها شامل ۲ نوع تاپل (Tuple) و آرایه (Array) میشند.نوع Tupleبرای گروه‌بندی مقادیری با نوع‌ های مختلف می‌تونیم از تاپل ها استفاده کنیم:Tuple with optional type annotationsبرای دسترسی به مقادیر داخل تاپل میتونیم از تطبیق الگو (pattern matching) استفاده کنیم:Pattern Matchingدر واقع به کاری که در بالا انجام دادیم destructuring میگند. مقادیر ما به ترتیب در متغیر های y , x و z قرار می‌گیرند. پس مقدار y برابر با ۶.۴ هست... ما همچنین می‌تونیم به شکل مستقیم هم به المان های یک تاپل دسترسی داشته باشیم: با استفاده از یک نقطه و شماره المان مورد نظر (کم و بیش شبیه آرایه ها):Using respective indicesنوع Arrayیک راه دیگه برای داشتن مجموعه ای از مقادیر متعدد ، یک آرایه است. بر خلاف تاپل ها عناصر داخل یک آرایه باید از یک نوع باشند. آرایه ها در Rust با سایر زبان ها کمی تفاوت دارند چون آرایه ها هم مثل تاپل ها در Rust اندازه مشخصی دارند; آرایه ها زمانی مفید هستند که می خواهید تعداد ثابتی از عناصر را داشته باشید.Declaring an Arrayاگر نیاز داشتیم نوع المان های یک ارایه رو مشخص کنیم، به شکل زیر:Declaring an Array with Type and Sizeدر اینجا برای هر المان از نوع داده i32 استفاده کردیم، و عدد 5 نشان دهنده تعداد المان های این آرایه هست.. و اما برای دسترسی به المان های یک آرایه میتونید از فهرست بندی استفاده کنید:Accessing Array's Elementsما همچنین وکتور (Vector) ها رو داریم که توسط کتابخانه استاندارد Rust ارائه میشند و قابلیت کوچک یا بزرگ شدن رو هم دارند، که اونها رو هم بررسی خواهیم کرد. توابع در Rustتا اینجا با یکی از مهمترین تابع های زبان Rust آشنا شدیم. تابع main که نقطه شروع (entry point) اپلیکیشن ما هست. همچنین با کلیدواژه fn آشنا شدیم، که با کمکش توابع رو تعریف میکنیم. برای نام گذاری هم مثل متغیرها پیشنهاد میشه از snake_case استفاده کنیم.پارامتر یا آرگومان؟هنگامی که یک تابع پارامتر هایی رو داره، شما میتونید مقادیری concrete رو به اون پارامترها پاس بدید و از لحاظ فنی مقادیر concrete رو آرگومان می‌گیم. اما در گفتگوهای معمول توسعه دهنده ها خیلی عادی هست که: از ۲ واژه پارامتر و آرگومان برای مفاهیمی مثل (متغیرهایی که در تعریف یک تابع داریم) و (پاس دادن مقادیر concrete در حین فراخوانی یک تابع) به شکل متناوب استفاده بشه.Functions with specified Typed parametersدر تابع بالا ما ۲ پارامتر داریم که نوع هر کدوم مشخص هست و نتیجه زیر رو برمیگردونه:Results of another_functionنوع برگشتی توابعبرای برگشت دادن نوع داده ای خاص از یک تابع، میتونیم به شکل زیر عمل کنیم: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 Resultsبا وجودی که عدد ۶ هم به ۳ و هم به ۲ تقسیم‌پذیر هست، اما خروجی برنامه ما عدد ۳ رو نشون میده. علتش هم اینه که وقتی Rust به یه کاندیشن صحیح میرسه اون رو اجرا میکنه و دیگه چیزی رو چک نمیکنه.استفاده از if در یک Statementچون if یک expression هست.. ما میتونیم در سمت راست یک let statement ازش استفاده کنیم:Using if in a let Statementانواع حلقه ها در Rustحلقه ها اکثرا استفاده های کاربردی زیادی در کدهای ما دارند و در Rust ما ۳ نوع حلقه داریم: loop, while و for.تکرار کد با loopکلیدواژه loop به Rust میگه که یه قطعه کد رو دائما تکرار کنه; مگر اینکه شما از کلیدواژه break در داخل حلقه استفاده کنید، و بهش بگید دست نگه داره.Repeating Code with loopدر کد بالا، ما یک متغیر (counter) از نوع Mutable با مقدار ۰ تعریف کردیم. بعد داخل حلقه مرتبا داریم مقدار متغیر رو افزایش میدیم و چک میکنیم که آیا مقدارش به عدد ۱۰ رسیده یا نه؟ با رسیدن به عدد ۱۰ از break استفاده میکنیم و مقدار counter رو دو برابر میکنیم و در داخل متغیر قرار میدیم. پس result برابر با عدد ۲۰ خواهد بود.حلقه های شرطی با whileاین حلقه ها بسیار ساده هستند و مادامی که شرط برقرار باشه اجرا میشند و سپس به پایان میرسند:Conditional Loops with while, countdown exampleدر کد بالا، عدد ۳ رو داخل حلقه قرار میدیم و عدد رو پرینت میکنیم و هر بار مقدارش رو یکی کاهش میدیم. و هنگامی که به عدد ۰ برسیم دیگه شرط صادق نیست و حلقه به پایان میرسه.پیمایش یک کالکشن با forدر مثال زیر با استفاده از حلقه for اعداد داخل آرایه رو پیمایش و پرینت میکنیم:Looping Through a Collection with forامنیت و خاص بودن حلقه های for اونها رو به رایج ترین حلقه ها در Rust تبدیل می‌کنند. حتی در مواقعی که می‌خواهیم یک کد رو به تعداد مشخصی تکرار کنیم.. به طور مثال شمارش معکوسی که در مثال حلقه while داشتیم رو میتونیم با استفاده از for و به راحتی پیاده سازی کنیم:Countdown example with for and revراجب rev هنوز صحبت نکردیم، اما می‌بینید که کد بالا کوتاه‌تر و جالب تر از استفاده از while هست. نتیجه گیریبسار خوب.. در این نوشته با مفاهیم معمول برنامه نویسی در زبان Rust آشنا شدیم. در نوشته بعد مفاهیم کلیدی Ownership و Borrowing رو در مدیریت حافظه Rust بررسی می‌کنیم; و اینکه چطور این مفاهیم، زبان Rust رو از وجود هر گونه گاربج کالکشنی (Garbage Collection) بی نیاز می‌کنه و حافظه رو با امنیت بالا مدیریت می‌کنه.