rust-basics_آکادمی تخصصی ریسمان
تاریخ انتشار :
میانگین: 5.0

آموزش مبانی Rust؛ یادگیری Rust از صفر تا مفاهیم اصلی

اگر می‌خواهید Rust را از صفر یاد بگیرید، اولین قدم آشنایی با مفاهیم پایه‌ای است که این زبان را از زبان‌هایی مانند PHP، JavaScript و C++ متمایز می‌کند.
در این آموزش، ابتدا با ساختار Rust، نصب ابزارها و Cargo آشنا می‌شوید و سپس نوشتن اولین برنامه Rust را تجربه می‌کنید.
بعد از آن سراغ متغیرها، انواع داده، توابع، شرط‌ها، حلقه‌ها و تفاوت مهم String و &str می‌رویم.
مهم‌ترین بخش آموزش به مفاهیمی مانند Ownership، Borrowing و References اختصاص دارد؛ مفاهیمی که قلب زبان Rust محسوب می‌شوند.
در ادامه با Struct، Enum، Pattern Matching، Option، Result، Collectionها و Iteratorها آشنا خواهید شد.
اگر قصد دارید در ادامه با فناوری‌هایی مانند Tauri، توسعه Backend یا برنامه‌نویسی سیستمی کار کنید، این آموزش می‌تواند پایه مناسبی برای شروع مسیر یادگیری Rust باشد.

Share
Pin
Like
Send
Share
Send
Send
Share

اگر به دنبال یادگیری یک زبان برنامه‌نویسی مدرن، سریع و قابل‌اعتماد هستید، Rust یکی از گزینه‌هایی است که ارزش بررسی جدی دارد. Rust فقط یک زبان دیگر برای نوشتن برنامه‌های معمولی نیست؛ طراحی آن به‌گونه‌ای است که بتوانید به عملکرد نزدیک به زبان‌های سطح پایین برسید و در عین حال بسیاری از خطاهای خطرناک مربوط به حافظه را پیش از اجرای برنامه شناسایی کنید. مستندات رسمی Rust نیز این زبان را برای ساخت نرم‌افزار سریع‌تر و قابل‌اعتمادتر معرفی می‌کنند و نسخه فعلی مستندات رسمی بر Rust 1.97.0 و بالاتر و Edition 2024 تمرکز دارد. در زمان نگارش این مقاله، Rust 1.99.0 در تاریخ ۱ اکتبر ۲۰۲۶ منتشر شده و نسخه پایدار جدید Rust معمولاً هر شش هفته منتشر می‌شود. بنابراین اگر امروز شروع به یادگیری Rust می‌کنید، بهتر است آموزش خود را بر مبنای Rust مدرن و Edition 2024 قرار دهید، نه آموزش‌های قدیمی که ممکن است ساختار یا توصیه‌های آن‌ها با ابزارهای امروزی متفاوت باشد. هدف این مقاله این نیست که صرفاً چند دستور Rust را حفظ کنید؛ قرار است از پایه با طرز فکر Rust آشنا شوید. از متغیر و تابع شروع می‌کنیم، سپس به String، Ownership، Borrowing، Struct، Enum، Option و Result می‌رسیم و در نهایت مسیر مناسبی برای ادامه یادگیری ارائه می‌کنیم.

Rust چیست و چرا باید آن را یاد بگیریم؟

Rust یک زبان برنامه‌نویسی سیستمی است که تلاش می‌کند سه ویژگی مهم را هم‌زمان ارائه کند: کارایی بالا، ایمنی حافظه و کنترل مناسب روی منابع سیستم. اگر با C یا C++ کار کرده باشید، احتمالاً می‌دانید که کنترل مستقیم حافظه قدرت زیادی در اختیار برنامه‌نویس قرار می‌دهد، اما همین قدرت می‌تواند باعث ایجاد خطاهایی مانند استفاده از حافظه آزادشده، دسترسی خارج از محدوده، اشاره‌گرهای نامعتبر و Data Race شود. Rust رویکرد متفاوتی دارد و بخش بزرگی از این مشکلات را با سیستم نوع، Ownership و Borrow Checker در زمان کامپایل بررسی می‌کند. این یعنی بسیاری از خطاهایی که در زبان‌های دیگر ممکن است هنگام اجرای برنامه ظاهر شوند، در Rust اصلاً اجازه عبور از مرحله کامپایل را پیدا نمی‌کنند. البته نباید این موضوع را با «Rust هیچ خطایی ندارد» اشتباه گرفت؛ چنین ادعایی اشتباه است. Rust تضمین نمی‌کند منطق تجاری شما درست باشد، الگوریتم شما بهینه باشد یا برنامه شما هیچ Bugای نداشته باشد؛ بلکه دسته مشخصی از خطاهای ایمنی حافظه و هم‌زمانی را بسیار بهتر کنترل می‌کند. همین ترکیب باعث شده Rust در حوزه‌هایی مانند سیستم‌عامل، ابزارهای خط فرمان، سرویس‌های backend، WebAssembly، نرم‌افزارهای شبکه و برنامه‌های دسکتاپ مورد توجه قرار بگیرد. مستندات رسمی Rust نیز روی ترکیب کنترل سطح پایین، عملکرد و تجربه توسعه تأکید دارند.

Rust چه مشکلی را حل می‌کند؟

برای درک Rust بهتر است به جای حفظ کردن تعریف‌های تبلیغاتی، یک مسئله واقعی را تصور کنیم. فرض کنید برنامه‌ای دارید که اطلاعات بیماران، نوبت‌ها و پرداخت‌ها را مدیریت می‌کند و قرار است این برنامه روی سیستم دسکتاپ اجرا شود. برنامه باید سریع باشد، مصرف منابع آن کنترل‌شده باشد و در عین حال خطاهای حافظه باعث Crashهای غیرقابل‌پیش‌بینی نشوند. در زبان‌هایی مانند C++ می‌توانید چنین برنامه‌ای بسازید، اما مسئولیت بسیاری از جزئیات مدیریت حافظه بر عهده شما خواهد بود. Rust سعی می‌کند این کنترل را حفظ کند، اما با قوانینی مانند Ownership و Borrowing اجازه نمی‌دهد هر نوع دسترسی به داده‌ها آزادانه انجام شود. این محدودیت در ابتدا ممکن است آزاردهنده باشد؛ مخصوصاً اگر از Python، JavaScript یا PHP آمده باشید و عادت کرده باشید متغیرها را تقریباً بدون فکر درباره مالکیت داده جابه‌جا کنید. اما همین محدودیت بخش مهمی از فلسفه Rust است. کامپایلر Rust در واقع نقش یک بازبین بسیار سخت‌گیر را بازی می‌کند که قبل از اجرای برنامه از شما می‌پرسد: «این داده متعلق به چه کسی است؟ چه کسی حق تغییر آن را دارد؟ آیا این Reference هنوز معتبر است؟» اگر پاسخ ساختاری شما غلط باشد، برنامه کامپایل نمی‌شود. این همان جایی است که Rust از یک زبان صرفاً سریع به زبانی تبدیل می‌شود که شیوه تفکر برنامه‌نویس را نیز تغییر می‌دهد.

مهم‌ترین ویژگی‌های زبان Rust

سه ویژگی را باید از همان ابتدا در ذهن داشته باشید: Performance، Memory Safety و Zero-Cost Abstractions. منظور از Performance این است که Rust برای برنامه‌هایی طراحی شده که نیاز به کارایی بالا دارند و برخلاف زبان‌هایی که وابستگی زیادی به Garbage Collector دارند، مدیریت حافظه آن بر پایه مدل Ownership انجام می‌شود. Memory Safety نیز به این معنا نیست که تمام خطاهای برنامه غیرممکن هستند؛ بلکه Rust مجموعه مهمی از خطاهای مرتبط با دسترسی نامعتبر به حافظه را در زمان کامپایل محدود می‌کند. ویژگی سوم یعنی Zero-Cost Abstractions به این ایده مربوط است که بتوانید از ساختارهای سطح بالاتر زبان استفاده کنید بدون اینکه الزاماً هزینه اجرایی غیرضروری به برنامه تحمیل شود. البته اینجا هم باید دقیق باشیم: Zero-Cost به معنی رایگان بودن همه عملیات نیست. اگر یک Iterator پیچیده بنویسید، هزینه محاسباتی عملیات همچنان وجود دارد؛ منظور این است که انتزاع مناسب زبان الزاماً نباید به سربار اضافی در زمان اجرا منجر شود. ابزارهای Rust نیز یکی از نقاط قوت آن هستند. Rust از rustc به عنوان compiler و از Cargo برای ساخت پروژه، مدیریت dependencyها، اجرای تست‌ها و تولید build استفاده می‌کند. مستندات رسمی Rust نیز Cargo را به عنوان ابزار build system و package manager معرفی می‌کنند.

سرعت، امنیت حافظه و کنترل پایین‌سطح

وقتی درباره Rust صحبت می‌کنیم، نباید فقط روی «سریع بودن» تمرکز کنیم. زبان‌های زیادی سریع هستند و انتخاب زبان فقط بر اساس سرعت تصمیم درستی نیست. ارزش Rust در این است که تلاش می‌کند سرعت، کنترل منابع و ایمنی را کنار هم قرار دهد. تصور کنید در حال ساخت یک سرویس شبکه هستید که هزاران درخواست را پردازش می‌کند؛ در چنین شرایطی مدیریت حافظه و رفتار هم‌زمانی اهمیت زیادی پیدا می‌کند. یا تصور کنید بخشی از نرم‌افزار شما با سیستم‌عامل و APIهای native در ارتباط است؛ در اینجا دسترسی به قابلیت‌های سطح پایین اهمیت دارد. Rust برای این سناریوها طراحی شده است. در عین حال، اگر فقط می‌خواهید یک اسکریپت پنجاه‌خطی بنویسید، احتمالاً Rust بهترین انتخاب نیست و استفاده از زبان ساده‌تری می‌تواند بهره‌وری بیشتری داشته باشد. یک برنامه‌نویس حرفه‌ای نباید شیفته یک زبان خاص شود؛ باید مسئله را ببیند و ابزار مناسب را انتخاب کند. بنابراین اگر قصد یادگیری Rust دارید، دلیل مناسب این نیست که «Rust بهترین زبان دنیاست»، بلکه دلیل منطقی این است که Rust به شما کمک می‌کند مفاهیم مهمی مانند مالکیت داده، مدیریت منابع، نوع‌دهی دقیق و طراحی APIهای امن را عمیق‌تر یاد بگیرید.

Rust در چه پروژه‌هایی استفاده می‌شود؟

Rust کاربردهای متنوعی دارد و این تنوع یکی از دلایل جذابیت آن است. می‌توانید با Rust ابزارهای خط فرمان، سرویس‌های backend، نرم‌افزارهای شبکه، کتابخانه‌های native، ابزارهای توسعه، WebAssembly و برنامه‌های دسکتاپ بسازید. یکی از نمونه‌های جالب برای شما می‌تواند Tauri باشد؛ در Tauri بخش native برنامه با Rust نوشته می‌شود و رابط کاربری می‌تواند با فناوری‌هایی مانند React، Vue یا Svelte ساخته شود. این مدل برای ساخت برنامه‌های دسکتاپ سبک‌تر از معماری‌هایی که یک runtime کامل مرورگر را به صورت جداگانه بسته‌بندی می‌کنند، جذاب است. البته یادگیری Tauri بدون درک پایه Rust ممکن است شما را در برابر خطاهای compiler و ownership متوقف کند. اگر مثلاً در کد Tauri با Result، Option، struct، enum، reference یا lifetime مواجه شوید، بدون پایه Rust فقط می‌توانید خطا را کپی و در اینترنت جست‌وجو کنید. اما اگر مبانی را بفهمید، می‌توانید علت خطا را تحلیل کنید. به همین دلیل برای کسی که قصد دارد در ادامه با Tauri کار کند، یادگیری Rust صرفاً یک فعالیت جانبی نیست؛ بلکه یک سرمایه‌گذاری مستقیم روی توانایی حل مشکل در پروژه است. Rust همچنین در پروژه‌های بزرگ و زیرساختی نیز حضور دارد و اکوسیستم آن از طریق crates مختلف قابلیت‌های زیادی در اختیار توسعه‌دهندگان قرار می‌دهد.

Rust در توسعه دسکتاپ، وب و سیستم‌ها

در توسعه دسکتاپ، Rust می‌تواند قسمت native برنامه را بر عهده بگیرد و با یک UI مدرن ترکیب شود. در backend نیز frameworkها و کتابخانه‌هایی برای HTTP، serialization، database، async programming و networking وجود دارند. در حوزه سیستم‌ها نیز Rust انتخاب طبیعی‌تری است، زیرا به شما اجازه می‌دهد کنترل دقیقی روی memory و resource داشته باشید. یک مثال ساده این است که یک ابزار CLI برای پردازش فایل‌های بزرگ طراحی کنید. در PHP یا JavaScript هم می‌توان چنین کاری انجام داد، اما اگر نیاز شما پردازش سریع و کنترل‌شده تعداد زیادی فایل باشد، Rust می‌تواند انتخاب مناسبی باشد. از طرف دیگر، اگر پروژه شما یک پنل مدیریتی ساده با چند فرم CRUD است، استفاده از Rust لزوماً مزیت قطعی ندارد. این نکته مهم است چون یکی از اشتباهات رایج تازه‌کارها این است که بعد از یادگیری یک فناوری، همه مسائل را با همان ابزار حل می‌کنند. Rust ابزار قدرتمندی است، نه پاسخ همه مشکلات. در انتخاب تکنولوژی باید پیچیدگی توسعه، نیازهای عملکردی، تجربه تیم، اکوسیستم و هزینه نگهداری را هم در نظر بگیرید.

نصب Rust و ابزارهای مورد نیاز

برای شروع Rust معمولاً از rustup استفاده می‌شود. rustup ابزار رسمی مدیریت toolchainهای Rust است و امکان نصب و به‌روزرسانی نسخه‌های stable، beta و nightly را فراهم می‌کند. مستندات رسمی نیز نصب Rust از طریق rustup را روش پیشنهادی برای شروع معرفی می‌کنند. پس از نصب، بهتر است در Terminal دستورهای زیر را اجرا کنید:

rustc --version
cargo --version
rustup --version

اگر خروجی نسخه دریافت کردید، ابزارهای اصلی آماده هستند. در سیستم Windows ممکن است بسته به تنظیمات سیستم و ابزارهای native موردنیاز، به Visual Studio Build Tools نیز نیاز داشته باشید. این موضوع را با خود زبان Rust اشتباه نگیرید؛ compiler نصب شده ممکن است کاملاً سالم باشد ولی یک crate خاص هنگام build به ابزارهای native نیاز داشته باشد. همچنین بهتر است نسخه Rust را با rustup مدیریت کنید و برای پروژه‌های جدید از Edition 2024 استفاده کنید. Rust 2024 از Rust 1.85.0 معرفی شده و مستندات رسمی فعلی نیز آن را مبنای پروژه‌های جدید قرار می‌دهند. اگر سیستم شما به دلیل محدودیت شبکه یا proxy در دانلود rustup مشکل دارد، مشکل شبکه را از مشکل Rust جدا کنید؛ ابتدا باید مطمئن شوید rustup واقعاً توانایی دریافت toolchain را دارد.

نصب Rust با rustup

در حالت معمول، پس از نصب rustup، برای به‌روزرسانی toolchain پایدار می‌توانید از این دستور استفاده کنید:

rustup update stable

نکته مهم این است که «آخرین نسخه Rust» یک مقدار ثابت برای همیشه نیست. Rust چرخه انتشار شش‌هفته‌ای دارد و نسخه‌های جدید مرتب منتشر می‌شوند. برای مثال، Rust 1.99.0 در اول اکتبر ۲۰۲۶ منتشر شده است. بنابراین اگر مقاله‌ای قدیمی می‌خوانید که می‌گوید «آخرین نسخه Rust فلان نسخه است»، آن اطلاعات ممکن است امروز دیگر درست نباشد. برای آموزش، بهتر است شماره نسخه را فقط زمانی ذکر کنید که واقعاً لازم است و در موارد دیگر از عبارت «آخرین نسخه stable» استفاده کنید. همچنین اگر در پروژه‌ای از nightly استفاده می‌کنید، بدانید که nightly با stable تفاوت دارد و نباید مثال‌های آموزشی مربوط به ویژگی‌های آزمایشی را بدون توجه به نسخه compiler در پروژه production قرار دهید. برای شروع، stable انتخاب صحیح است.

Cargo چیست و چرا اهمیت دارد؟

اگر Rust را تازه شروع کرده‌اید، Cargo را دست‌کم نگیرید. Cargo فقط یک package manager ساده نیست؛ ابزار اصلی مدیریت پروژه‌های Rust است. با Cargo پروژه می‌سازید، dependency اضافه می‌کنید، برنامه را اجرا می‌کنید، تست می‌نویسید، build می‌گیرید و بسیاری از عملیات استاندارد توسعه را انجام می‌دهید. برای ایجاد یک پروژه جدید می‌توانید بنویسید:

cargo new hello_rust
cd hello_rust
cargo run

Cargo فایل Cargo.toml را برای مشخصات پروژه و dependencyها ایجاد می‌کند و ساختار استاندارد پروژه را نیز آماده می‌سازد. این رویکرد باعث می‌شود پروژه‌ها ساختار قابل پیش‌بینی داشته باشند. اگر قبلاً با Composer در PHP یا npm در JavaScript کار کرده باشید، مفهوم مدیریت dependency برایتان آشناست، اما Cargo بخشی جدایی‌ناپذیر از workflow Rust است. مستندات رسمی Rust نیز Cargo را ابزار build و dependency management معرفی می‌کنند. برای اجرای پروژه از cargo run، برای build از cargo build و برای اجرای تست‌ها از cargo test استفاده می‌کنید. در پروژه واقعی، این ابزارها بخش ثابت چرخه توسعه خواهند بود.

ساخت اولین پروژه Rust

ساختار ابتدایی پروژه‌ای که با cargo new ایجاد می‌شود تقریباً چنین است:

hello_rust/
├── Cargo.toml
└── src/
    └── main.rs

فایل Cargo.toml اطلاعات پروژه را نگه می‌دارد و src/main.rs نقطه شروع یک binary application است. اگر پروژه library بسازید، معمولاً فایل اصلی src/lib.rs خواهد بود. این تفاوت در پروژه‌های بزرگ اهمیت دارد. مثلاً در یک برنامه Tauri ممکن است بخش اصلی منطق Rust در lib.rs قرار داشته باشد و فایل‌های دیگری برای commandها و moduleهای مختلف داشته باشید. تازه‌کارها گاهی تصور می‌کنند همه چیز باید در main.rs نوشته شود؛ این روش در پروژه جدی خیلی زود به یک فایل شلوغ و غیرقابل نگهداری تبدیل می‌شود. از همان ابتدا بهتر است با مفهوم module و تفکیک مسئولیت آشنا شوید. یک فایل کوچک و واضح بهتر از فایلی است که صدها تابع و type مختلف را در خود جای داده باشد. Cargo نیز با ساختار module و crateهای Rust هماهنگ است و به شما اجازه می‌دهد پروژه را به شکل منظم رشد دهید.

ساختار یک پروژه Rust

درک ساختار پروژه قبل از یادگیری syntax اهمیت زیادی دارد. یک پروژه Rust معمولاً از یک crate تشکیل می‌شود و crate می‌تواند binary یا library باشد. src/main.rs معمولاً نقطه ورود برنامه اجرایی است، در حالی که src/lib.rs برای library crate استفاده می‌شود. وقتی پروژه بزرگ‌تر می‌شود، می‌توانید moduleهای مختلف ایجاد کنید:

src/
├── main.rs
├── models.rs
├── database.rs
├── services.rs
└── utils.rs

البته صرفاً تقسیم فایل‌ها کافی نیست؛ باید مرز مسئولیت‌ها را نیز درست طراحی کنید. مثلاً در یک نرم‌افزار مدیریت کلینیک بهتر است مدل Patient، عملیات database و منطق business را بدون دلیل در یک فایل قرار ندهید. Rust به شما ابزارهای مناسبی برای visibility، module و encapsulation می‌دهد. کلمه pub برای عمومی کردن itemها استفاده می‌شود و درک درست visibility بعداً در طراحی libraryها بسیار مهم خواهد بود. در پروژه‌های واقعی، خوانایی و ساختار اهمیت بیشتری از کوتاه بودن کد دارد. اگر کدی فقط با ده خط نوشته شود ولی فهم آن دشوار باشد، لزوماً کد خوبی نیست. هدف Rust این نیست که کوتاه‌ترین کد ممکن را بنویسید؛ هدف این است که کد قابل اعتماد، قابل نگهداری و قابل فهم داشته باشید.

اولین برنامه Hello World در Rust

ساده‌ترین برنامه Rust را می‌توان چنین نوشت:

fn main() {
    println!("Hello, world!");
}

در اینجا fn برای تعریف تابع استفاده شده و main نقطه ورود برنامه اجرایی است. println! نیز یک macro است؛ علامت ! در انتهای آن اهمیت دارد و نشان می‌دهد با function معمولی طرف نیستیم. همین مثال کوچک چند نکته مهم دارد: Rust از ; برای پایان بسیاری از statementها استفاده می‌کند و indentation نیز برای خوانایی است، نه تعیین محدوده مانند Python. اگر برنامه را با cargo run اجرا کنید، Cargo ابتدا پروژه را build می‌کند و سپس binary را اجرا می‌کند. مستندات رسمی Rust نیز آموزش خود را با نصب، Hello World و سپس Cargo شروع می‌کنند. شاید این برنامه بیش از حد ساده به نظر برسد، اما بهتر است از همین‌جا یک عادت درست بسازید: هر مفهوم جدید را با یک مثال کوچک اجرا کنید. خواندن ده صفحه درباره Ownership بدون نوشتن کد، ارزش بسیار کمتری از نوشتن چند برنامه کوچک و مشاهده خطاهای compiler دارد.

متغیرها و مفهوم mut

در Rust متغیرها به صورت پیش‌فرض immutable هستند. این یکی از تفاوت‌های مهم Rust با زبان‌هایی مانند JavaScript یا PHP است. مثلاً:

fn main() {
    let age = 30;
    println!("{age}");
}

اگر بعداً بنویسید:

age = 31;

کد خطا خواهد داد، چون age immutable است. برای تغییر مقدار باید از mut استفاده کنید:

let mut age = 30;
age = 31;

این رفتار در ابتدا ممکن است محدودکننده به نظر برسد، اما یک مزیت مهم دارد: وقتی متغیری قرار نیست تغییر کند، کامپایلر و خواننده کد این موضوع را می‌دانند. چنین تصمیمی از تغییرات ناخواسته جلوگیری می‌کند. Rust همچنین مفهوم shadowing دارد که با mut متفاوت است:

let age = 30;
let age = age + 1;

در اینجا متغیر جدیدی با همان نام ایجاد شده است. این موضوع مخصوصاً برای تبدیل نوع یا تغییر معنای یک مقدار مفید است. مثلاً می‌توانید یک String خام داشته باشید و سپس همان نام را برای نتیجه پردازش استفاده کنید. تفاوت mut و shadowing را دقیق یاد بگیرید؛ یکی اجازه تغییر مقدار همان binding را می‌دهد و دیگری binding جدیدی ایجاد می‌کند.

ثابت‌ها و تفاوت آن‌ها با متغیرها

ثابت‌ها با const تعریف می‌شوند:

const MAX_USERS: u32 = 1000;

ثابت با متغیر immutable یکی نیست. let یک binding ایجاد می‌کند، اما const برای مقادیری است که به عنوان constant تعریف می‌شوند و باید type آن‌ها مشخص باشد. در پروژه‌های واقعی، برای مقادیری مانند محدودیت ثابت، اندازه مشخص buffer یا configuration compile-time می‌توان از const استفاده کرد. اما نباید هر مقدار تغییرناپذیری را صرفاً به const تبدیل کنید. انتخاب بین let، let mut و const باید بر اساس نقش داده باشد. همچنین Rust دارای static نیز هست که مفهوم دیگری دارد و بیشتر با داده‌هایی مرتبط است که طول عمر آن‌ها کل اجرای برنامه است. در مباحث ابتدایی فعلاً تمرکز خود را روی let و const بگذارید. مهم‌تر از حفظ syntax این است که ذهنیت immutable-by-default را بپذیرید. اگر داده‌ای نباید تغییر کند، تغییرپذیری را بی‌دلیل فعال نکنید.

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

Rust یک زبان strongly typed است و compiler باید نوع داده‌ها را در زمان کامپایل مشخص کند. انواع پایه شامل integerها، floating-pointها، boolean و character هستند. برای مثال:

let count: i32 = 10;
let price: f64 = 25.5;
let active: bool = true;
let letter: char = 'A';

انواع integer شامل i8، i16، i32، i64، i128 و انواع unsigned مانند u8 تا u128 هستند. نوع usize نیز در بسیاری از عملیات مرتبط با index و اندازه collectionها کاربرد دارد. برای اعداد اعشاری معمولاً f32 و f64 وجود دارند. bool فقط true یا false است و char در Rust یک Unicode scalar value است و صرفاً محدود به یک بایت نیست. این تفاوت با زبان‌هایی مانند C بسیار مهم است. علاوه بر انواع scalar، Rust انواع compound مانند tuple و array نیز دارد. type inference باعث می‌شود همیشه مجبور نباشید type را بنویسید، اما این به معنی بی‌نوع بودن متغیر نیست. Compiler نوع را استنتاج می‌کند و در صورت وجود ambiguity یا ناسازگاری، خطا می‌دهد. یادگیری type system Rust پایه‌ای برای درک Ownership، Generic و Trait خواهد بود.

انواع عددی، بولی و کاراکتر

فرض کنید می‌خواهید تعداد بیماران را ذخیره کنید:

let patients: u32 = 120;

اگر قرار نیست عدد منفی باشد، unsigned type می‌تواند منطقی باشد؛ ولی این به معنی آن نیست که همیشه باید برای اعداد مثبت u32 استفاده کنید. انتخاب type باید با محدوده داده و API موردنظر سازگار باشد. برای indexهای collection معمولاً usize استفاده می‌شود. در محاسبات مالی نیز استفاده مستقیم از floating point مانند f64 می‌تواند تصمیم مناسبی نباشد، زیرا نمایش اعشاری binary با مفهوم دقیق پول یکی نیست. این نکته نمونه‌ای از چیزی است که Rust نمی‌تواند برای شما تصمیم بگیرد؛ compiler فقط بخشی از خطاها را کنترل می‌کند، اما مدل داده همچنان مسئولیت شماست. بنابراین یادگیری Rust باید همراه با یادگیری اصول مهندسی نرم‌افزار باشد. زبان خوب، جای طراحی بد را نمی‌گیرد.

String و &str

یکی از اولین بخش‌هایی که تازه‌کارهای Rust را سردرگم می‌کند، تفاوت String و &str است. String یک رشته مالک‌شده و قابل رشد است، در حالی که &str یک string slice است که به داده‌ای از رشته اشاره می‌کند. مثلاً:

let name: String = String::from("Ali");
let message: &str = "Hello";

String معمولاً زمانی استفاده می‌شود که برنامه مالک داده رشته‌ای باشد یا بخواهد آن را تغییر دهد و رشد دهد. &str اغلب برای مشاهده یک بخش از داده متنی بدون گرفتن مالکیت استفاده می‌شود. این تفاوت مستقیماً با Ownership و Borrowing ارتباط دارد. اگر این دو مفهوم را صرفاً با حفظ کردن جدول تفاوت‌ها یاد بگیرید، احتمالاً در اولین تابع واقعی دوباره گیج خواهید شد. باید بفهمید چرا String مالک heap data است و چرا &str فقط یک view روی داده است. برای مثال:

fn greet(name: &str) {
    println!("Hello, {name}");
}

این تابع مالک name نیست؛ فقط به داده آن دسترسی دارد. بنابراین می‌تواند با String یا string literal مورد استفاده قرار گیرد. این یکی از الگوهای مهم Rust است: تا وقتی نیاز ندارید مالکیت داده را منتقل کنید، معمولاً بهتر است آن را borrow کنید.

تفاوت String و &str

تفاوت را با یک تشبیه ساده می‌توان فهمید. String شبیه این است که یک کتاب را در اختیار دارید و مسئولیت آن با شماست؛ &str شبیه این است که فقط اجازه دارید کتاب را بخوانید. اگر کتاب را به شخص دیگری منتقل کنید، دیگر مالک قبلی آن نیستید. Rust با Ownership دقیقاً چنین روابطی را مدل می‌کند. البته این تشبیه کامل نیست و نباید جای تعریف فنی را بگیرد، اما برای شروع مفید است. String روی heap داده نگهداری می‌کند و می‌تواند تغییر اندازه دهد. &str یک reference به بخشی از رشته است. اگر در تابع فقط می‌خواهید متن را بخوانید، &str معمولاً انتخاب مناسبی است. اگر تابع باید یک رشته جدید تولید و مالک آن شود، ممکن است String را برگرداند. همین تصمیم‌های کوچک، پایه طراحی APIهای Rust هستند.

توابع و مقدار بازگشتی

تابع در Rust با fn تعریف می‌شود:

fn add(a: i32, b: i32) -> i32 {
    a + b
}

اینجا a + b بدون return مقدار بازگشتی تابع است. این موضوع یکی از نکات مهم syntax Rust است. در Rust، آخرین expression یک block می‌تواند مقدار آن block باشد، به شرطی که ; نداشته باشد. اگر بنویسید:

a + b;

آن expression دیگر مقدار i32 را به عنوان خروجی block تولید نمی‌کند و در نتیجه با امضای تابع بالا ناسازگار خواهد شد. این مسئله برای تازه‌کارها بسیار رایج است. Rust بین statement و expression تفاوت مهمی قائل می‌شود. مثلاً let x = 5; یک statement است، اما if condition { 10 } else { 20 } یک expression است که مقدار تولید می‌کند. این مدل بعداً در match و closureها نیز بسیار مهم می‌شود. بنابراین اگر می‌خواهید Rust را حرفه‌ای یاد بگیرید، فقط syntax را حفظ نکنید؛ مفهوم expression-oriented بودن زبان را بفهمید.

شرط‌ها و حلقه‌ها

Rust برای شرط از if استفاده می‌کند:

let age = 20;

if age >= 18 {
    println!("Adult");
} else {
    println!("Minor");
}

یکی از ویژگی‌های مهم این است که if یک expression است:

let status = if age >= 18 {
    "adult"
} else {
    "minor"
};

برای تکرار نیز loop، while و for وجود دارند:

for number in 1..5 {
    println!("{number}");
}

در Rust، 1..5 یعنی محدوده‌ای که ۵ را شامل نمی‌شود. برای شامل کردن انتهای محدوده می‌توانید از 1..=5 استفاده کنید. match نیز یکی از ابزارهای بسیار مهم Rust برای pattern matching است و بعداً هنگام کار با Option و Result اهمیت آن چند برابر می‌شود. بهتر است از همان ابتدا به جای زنجیره‌های طولانی شرطی، زمانی که داده ساختار مشخصی دارد، استفاده از match را نیز یاد بگیرید. Rust به شما اجازه می‌دهد بسیاری از حالت‌های ممکن را به صورت صریح پوشش دهید و compiler می‌تواند در بعضی شرایط بررسی کند که match شما exhaustive است.

Ownership؛ مهم‌ترین مفهوم Rust

اگر قرار باشد فقط یک مفهوم را از مبانی Rust عمیق یاد بگیرید، آن مفهوم Ownership است. Ownership سیستمی از قوانین برای تعیین مالکیت داده‌هاست. هر مقدار در Rust یک owner دارد و وقتی owner از scope خارج می‌شود، Rust می‌تواند منابع مربوط به آن را آزاد کند. این مدل بدون نیاز به Garbage Collector امکان مدیریت خودکار حافظه را فراهم می‌کند. سه قانون ساده برای شروع کافی است: هر مقدار یک owner دارد، در هر لحظه یک owner اصلی وجود دارد، و وقتی owner از scope خارج شود مقدار از بین می‌رود. حالا مسئله جالب‌تر می‌شود: اگر یک String را به متغیر دیگری نسبت دهید، بسته به نوع داده و رفتار آن، ممکن است ownership منتقل شود. مثلاً:

let s1 = String::from("hello");
let s2 = s1;

پس از این عملیات، دیگر نباید به شکل معمول از s1 استفاده کنید، چون مالکیت به s2 منتقل شده است. اگر انتظار دارید مثل زبان‌های دیگر هر دو متغیر مستقل باشند، compiler شما را متوقف خواهد کرد. این رفتار در ابتدا سخت است، اما دلیل مهمی دارد: Rust نمی‌خواهد دو owner مستقل داشته باشند که در نهایت باعث double free یا رفتار نامشخص شوند. Ownership قلب مدل حافظه Rust است.

Borrowing و Reference

Borrowing راهی است برای استفاده از داده بدون گرفتن مالکیت آن. با & یک reference ایجاد می‌کنیم:

fn print_name(name: &String) {
    println!("{name}");
}

البته در این مثال معمولاً بهتر است &str بگیرید:

fn print_name(name: &str) {
    println!("{name}");
}

برای reference قابل تغییر از &mut استفاده می‌شود:

fn change_name(name: &mut String) {
    name.push_str(" Ali");
}

Rust قوانین دقیقی برای reference دارد. به صورت ساده، نمی‌توانید هم‌زمان یک mutable reference و referenceهای immutable ناسازگار به یک داده داشته باشید. این محدودیت برای جلوگیری از Race Condition و تغییرات غیرقابل پیش‌بینی طراحی شده است. تازه‌کارها معمولاً در اینجا با compiler درگیر می‌شوند و تصور می‌کنند compiler «سخت‌گیری بی‌دلیل» می‌کند. چنین برداشتی اشتباه است. کامپایلر دقیقاً همان رابطه‌ای را بررسی می‌کند که شما باید در طراحی برنامه در ذهن داشته باشید: چه کسی داده را می‌خواند، چه کسی تغییر می‌دهد و این دسترسی تا چه زمانی معتبر است. وقتی Borrowing را بفهمید، بخش بزرگی از پیام‌های خطای Rust قابل تفسیر می‌شوند.

Struct، Enum و Pattern Matching

برای ساخت typeهای سفارشی از struct استفاده می‌کنیم:

struct Patient {
    name: String,
    age: u32,
}

سپس:

let patient = Patient {
    name: String::from("Ali"),
    age: 35,
};

این مدل برای پروژه‌های واقعی بسیار مهم است. مثلاً در یک نرم‌افزار نوبت‌دهی می‌توانید Patient، Appointment، Service و Insurance را به صورت typeهای مشخص تعریف کنید. enum برای زمانی مناسب است که یک مقدار یکی از چند حالت مشخص باشد:

enum Status {
    Pending,
    Confirmed,
    Cancelled,
}

این ساختار زمانی بسیار قدرتمند می‌شود که با match ترکیب شود. به جای اینکه یک رشته مانند "pending" را در ده‌ها بخش برنامه مقایسه کنید، می‌توانید type مشخص داشته باشید. این کار بسیاری از خطاهای typo و حالت‌های نامعتبر را کاهش می‌دهد. Rust در اینجا شما را تشویق می‌کند داده را با ساختار دقیق‌تری مدل کنید. در پروژه‌های بزرگ، type system فقط برای جلوگیری از خطا نیست؛ نوع داده بخشی از مستندات کد شما نیز محسوب می‌شود.

Result و Option برای مدیریت خطا

دو type بسیار مهم Rust عبارت‌اند از Option و Result. Option برای مقداری استفاده می‌شود که ممکن است وجود داشته باشد یا نداشته باشد:

enum Option {
    Some(T),
    None,
}

مثلاً اگر دنبال بیمار خاصی در database هستید، ممکن است نتیجه وجود داشته باشد یا نداشته باشد. به جای اینکه مثل زبان‌هایی که null دارند یک null عمومی در همه‌جا پخش کنید، می‌توانید نبود مقدار را به صورت None مدل کنید. Result نیز برای عملیات موفق یا ناموفق استفاده می‌شود:

enum Result {
    Ok(T),
    Err(E),
}

برای مثال خواندن فایل ممکن است موفق شود یا خطا داشته باشد. Rust شما را مجبور می‌کند این حالت‌ها را جدی بگیرید. این موضوع یکی از تفاوت‌های مهم Rust با رویکردهایی است که exception یا null را به شکل گسترده استفاده می‌کنند. در پروژه واقعی، استفاده درست از Result باعث می‌شود مسیر خطاها واضح‌تر باشد. البته نباید همه خطاها را کورکورانه با unwrap() پنهان کنید. unwrap() در نمونه‌های آموزشی مفید است، اما در کد production اگر خطا قابل وقوع باشد، باید آن را آگاهانه مدیریت کنید.

Collectionها و Iteratorها

Rust مجموعه‌های استانداردی مانند Vec, HashMap و String دارد. Vec یکی از مهم‌ترین collectionهاست:

let numbers = vec![10, 20, 30];

برای پیمایش:

for number in &numbers {
    println!("{number}");
}

استفاده از &numbers مهم است، چون در این حالت ownership collection را برای iteration به شکل destructive منتقل نمی‌کنید. Iteratorها نیز یکی از قابلیت‌های قدرتمند Rust هستند:

let result: Vec = numbers
    .iter()
    .map(|n| n * 2)
    .collect();

اینجا iter()، map() و collect() با هم یک pipeline ایجاد می‌کنند. اگر از زبان‌های functional یا JavaScript استفاده کرده باشید، این سبک برایتان آشنا خواهد بود. اما در Rust باید Ownership و نوع iterator را نیز بفهمید. iter() معمولاً reference می‌دهد، در حالی که into_iter() بسته به context می‌تواند ownership را مصرف کند. این تفاوت در پروژه‌های واقعی بسیار مهم است. یادگیری Collectionها را به بعد از Ownership موکول نکنید، زیرا در عمل تقریباً همه برنامه‌های واقعی با collectionها سروکار دارند.

مسیر یادگیری Rust برای پروژه‌های واقعی

اگر هدف شما فقط آشنایی نیست و می‌خواهید با Rust پروژه واقعی بسازید، مسیر یادگیری باید مرحله‌ای باشد. ابتدا syntax پایه شامل variable، type، function، condition و loop را یاد بگیرید. سپس سریعاً وارد Ownership، Borrowing و References شوید؛ اشتباه است که ماه‌ها روی syntax ساده بمانید و Ownership را عقب بیندازید، زیرا بخش زیادی از Rust به همین مفاهیم متصل است. بعد از آن Struct، Enum، Pattern Matching، Option، Result، Collection و Iterator را یاد بگیرید. در مرحله بعد Generic، Trait، Lifetime، Module، Error Handling پیشرفته، Testing و Concurrency اهمیت پیدا می‌کنند. پس از تسلط نسبی می‌توانید سراغ ecosystem بروید؛ مثلاً اگر هدف شما ساخت برنامه دسکتاپ است، Tauri را شروع کنید. اگر هدف backend است، frameworkهای مناسب Rust را بررسی کنید. مستندات رسمی Rust نیز مسیر آموزشی را از مفاهیم پایه به Ownership، Struct، Enum، Error Handling و موضوعات پیشرفته‌تر می‌برد و تأکید دارد که بخش‌های بعدی بر مفاهیم قبلی ساخته می‌شوند.

اشتباهات رایج در یادگیری Rust

بزرگ‌ترین اشتباه این است که Rust را با روش یادگیری JavaScript یا PHP یاد بگیرید. در Rust، اگر فقط syntax را حفظ کنید ولی Ownership را نفهمید، خیلی زود متوقف می‌شوید. اشتباه دوم این است که هر خطای compiler را در اینترنت جست‌وجو کنید و اولین راه‌حل را کپی کنید. این روش ممکن است امروز کد را کامپایل کند، اما فردا در پروژه‌ای دیگر دوباره همان مشکل را خواهید داشت. باید پیام compiler را بخوانید و مشخص کنید مشکل مربوط به type، ownership، borrowing، lifetime، trait یا scope است. اشتباه سوم استفاده افراطی از clone() برای فرار از Ownership است. clone() گاهی کاملاً درست است، اما اگر هر جا compiler از شما reference خواست، داده را clone کنید، احتمالاً دارید مشکل طراحی را پنهان می‌کنید و هزینه اضافی به برنامه می‌دهید. اشتباه دیگر استفاده زیاد از unwrap() است. unwrap() ابزار مناسبی برای نمونه‌های ساده یا شرایطی است که واقعاً invariant را تضمین کرده‌اید، اما تبدیل کردن تمام خطاها به panic در یک برنامه production طراحی خوبی نیست. در نهایت، بدون نوشتن پروژه نمی‌توانید Rust را یاد بگیرید. هر مفهوم را با کد واقعی امتحان کنید.

 

Ownership و Borrowing در Rust را عمیق‌تر یاد بگیرید

چرا Ownership مهم‌ترین مفهوم Rust است؟

اگر مقاله اصلی را مطالعه کرده‌اید، احتمالاً متوجه شده‌اید که Ownership تفاوت اصلی Rust با بسیاری از زبان‌های برنامه‌نویسی رایج است. شاید در نگاه اول قوانین آن سخت‌گیرانه و حتی عجیب به نظر برسند، اما دلیل این سخت‌گیری کاملاً فنی است. Rust می‌خواهد بدون استفاده از Garbage Collector، مدیریت حافظه را تا حد زیادی در زمان کامپایل کنترل کند و برای رسیدن به این هدف باید بداند هر داده متعلق به چه بخشی از برنامه است. وقتی این مدل را درک کنید، خطاهای compiler دیگر تصادفی یا آزاردهنده به نظر نمی‌رسند؛ هر خطا معمولاً به یک قانون مشخص درباره مالکیت، محدوده یا دسترسی به داده اشاره دارد. بنابراین اگر می‌خواهید Rust را حرفه‌ای یاد بگیرید، Ownership چیزی نیست که بتوانید از آن عبور کنید و بعداً سراغش برگردید.

انتقال مالکیت در String

برای درک بهتر Ownership، بهتر است با String شروع کنیم، چون String داده‌ای دارد که معمولاً روی heap قرار می‌گیرد و قوانین مالکیت در آن به‌وضوح دیده می‌شوند. در کد زیر، بعد از انتساب s1 به s2، مالکیت داده منتقل می‌شود و استفاده معمول از s1 دیگر مجاز نیست. دلیل این رفتار جلوگیری از وجود چند مالک مستقل برای یک بخش حافظه است؛ در غیر این صورت هنگام آزادسازی حافظه احتمال خطاهایی مانند double free وجود داشت. این رفتار ممکن است برای برنامه‌نویسی که از PHP یا JavaScript آمده عجیب باشد، چون در آن زبان‌ها مدل حافظه متفاوت است. در Rust باید از ابتدا عادت کنید هنگام انتقال داده از خودتان بپرسید: آیا مالکیت را منتقل می‌کنم یا فقط می‌خواهم داده را قرض بگیرم؟

fn main() {
    let s1 = String::from("Rust");
    let s2 = s1;

    println!("{s2}");
}

در این مثال s1 دیگر مالک داده نیست و s2 مالک آن شده است. اگر بعد از آن println!("{s1}") بنویسید، compiler خطا خواهد داد. این خطا نشانه خراب بودن compiler نیست؛ برعکس، compiler دقیقاً جلوی استفاده از یک binding را می‌گیرد که دیگر مالک داده نیست. اگر واقعاً نیاز داشته باشید یک کپی مستقل داشته باشید، می‌توانید از clone() استفاده کنید، اما باید بدانید که clone کردن یک کپی واقعی از داده ایجاد می‌کند و می‌تواند هزینه حافظه و پردازش داشته باشد. استفاده از clone() برای فرار از هر خطای Ownership روش حرفه‌ای نیست. اول باید بفهمید چرا مالکیت منتقل شده و آیا طراحی تابع شما نیاز به انتقال مالکیت دارد یا borrowing.

Borrowing بدون انتقال مالکیت

در بسیاری از توابع اصلاً نمی‌خواهیم داده را تصاحب کنیم؛ فقط می‌خواهیم آن را بخوانیم. اینجا Borrowing وارد می‌شود. با استفاده از & می‌توانیم reference ایجاد کنیم و داده را بدون انتقال مالکیت در اختیار تابع قرار دهیم. مثلاً تابع زیر یک String را می‌خواند اما مالکیت آن را دریافت نمی‌کند. بعد از اجرای تابع، متغیر اصلی همچنان قابل استفاده است و این دقیقاً همان چیزی است که در بسیاری از APIهای واقعی نیاز داریم.

fn print_name(name: &String) {
    println!("Name: {name}");
}

fn main() {
    let name = String::from("Ali");

    print_name(&name);

    println!("Again: {name}");
}

با این حال، در این مثال یک بهبود حرفه‌ای وجود دارد: بهتر است تابع به جای &String از &str استفاده کند. دلیل آن این است که تابع فقط به محتوای متنی نیاز دارد و نباید خود را به نوع مالک‌شده String محدود کند. نسخه انعطاف‌پذیرتر تابع چنین است:

fn print_name(name: &str) {
    println!("Name: {name}");
}

حالا تابع می‌تواند با یک String یا حتی string literal استفاده شود. این یکی از الگوهای مهم طراحی API در Rust است: تا زمانی که نیاز به مالکیت ندارید، مالکیت را نگیرید.

Mutable Borrowing چه زمانی لازم است؟

گاهی فقط خواندن داده کافی نیست و می‌خواهیم همان داده را تغییر دهیم. در این حالت از &mut استفاده می‌کنیم. برای مثال فرض کنید تابعی داریم که نام بیمار را تغییر می‌دهد. تابع باید اجازه تغییر مقدار رشته را داشته باشد، بنابراین reference آن باید mutable باشد. همچنین متغیر اصلی نیز باید با mut تعریف شده باشد.

fn change_name(name: &mut String) {
    name.push_str(" Ahmadi");
}

fn main() {
    let mut name = String::from("Ali");

    change_name(&mut name);

    println!("{name}");
}

نکته مهم این است که Rust اجازه نمی‌دهد دسترسی‌های mutable و immutable ناسازگار را آزادانه کنار هم قرار دهید. این محدودیت ممکن است در ابتدا دست‌وپاگیر باشد، اما از مشکلات جدی در برنامه‌های concurrent جلوگیری می‌کند. فرض کنید یک thread در حال خواندن داده باشد و هم‌زمان thread دیگری همان داده را تغییر دهد؛ بدون قوانین مشخص، نتیجه می‌تواند غیرقابل پیش‌بینی شود. Rust بسیاری از این مسائل را در زمان کامپایل بررسی می‌کند. به همین دلیل باید Borrowing را فقط یک syntax ساده ندانید؛ این مفهوم بخشی از مدل ایمنی هم‌زمانی Rust است.

Scope و طول عمر مالکیت

یکی از مفاهیم مهم مرتبط با Ownership، Scope است. هر متغیر در یک محدوده مشخص معتبر است و وقتی از آن محدوده خارج شود، Rust می‌تواند منابع متعلق به آن را آزاد کند. مثلاً:

{
    let message = String::from("Hello Rust");
    println!("{message}");
}

پس از پایان block، message دیگر در دسترس نیست. این رفتار برای حافظه heap اهمیت زیادی دارد، زیرا Rust می‌داند چه زمانی باید منابع مربوط به مقدار را آزاد کند. در زبان‌هایی با Garbage Collector، سیستم runtime معمولاً زمان آزادسازی دقیق حافظه را خودش مدیریت می‌کند. در Rust این موضوع بخش مهمی از مدل مالکیت زبان است و compiler در زمان ساخت برنامه درباره آن استدلال می‌کند. وقتی این مفهوم را بفهمید، بعداً Lifetime نیز بسیار قابل فهم‌تر خواهد شد؛ چون Lifetime در اصل درباره مدت معتبر بودن referenceهاست، نه درباره مدت عمر خود همه داده‌ها به یک شکل.

یک مثال واقعی با اطلاعات بیمار

فرض کنید در نرم‌افزار مدیریت کلینیک، ساختار ساده‌ای برای بیمار داریم:

struct Patient {
    name: String,
    age: u32,
}

fn show_patient(patient: &Patient) {
    println!("{} - {}", patient.name, patient.age);
}

تابع show_patient مالک بیمار نیست و فقط اطلاعات آن را می‌خواند. بنابراین می‌توانیم همان بیمار را چند بار به توابع مختلف ارسال کنیم، بدون اینکه مالکیت آن را منتقل کنیم. این مدل در برنامه‌های واقعی بسیار کاربردی است، چون معمولاً یک entity مانند Patient باید توسط چند بخش برنامه خوانده شود. اگر در هر تابع داده را clone یا move کنیم، هم کد پیچیده‌تر می‌شود و هم ممکن است هزینه اضافی ایجاد شود. استفاده صحیح از referenceها باعث می‌شود APIهای برنامه واضح‌تر باشند. در واقع، بسیاری از طراحی‌های خوب Rust از همین سؤال ساده شروع می‌شوند: «این تابع واقعاً مالک داده است یا فقط مصرف‌کننده موقت آن؟»

چه زمانی از clone استفاده کنیم؟

clone() ابزار بدی نیست؛ استفاده بی‌فکر از آن بد است. اگر واقعاً به یک کپی مستقل از داده نیاز دارید، clone می‌تواند انتخاب صحیحی باشد. برای مثال شاید بخواهید یک مقدار را در دو ساختار مستقل ذخیره کنید و تغییر یکی نباید روی دیگری تأثیر داشته باشد. در چنین شرایطی کپی کردن داده منطقی است. مشکل زمانی ایجاد می‌شود که برنامه‌نویس هر بار با خطای Ownership مواجه می‌شود و بدون تحلیل clone() اضافه می‌کند تا compiler ساکت شود. این روش در پروژه‌های بزرگ به‌تدریج مصرف حافظه و زمان پردازش را افزایش می‌دهد. بنابراین قبل از clone کردن، همیشه بپرسید: آیا واقعاً به دو نسخه مستقل از داده نیاز دارم یا فقط باید borrowing انجام دهم؟

تمرین پیشنهادی برای هفته اول

برای تثبیت Ownership، یک برنامه کوچک بنویسید که اطلاعات یک بیمار را در struct نگه دارد و سه تابع مختلف روی آن کار کنند. تابع اول فقط اطلاعات بیمار را بخواند، تابع دوم یک فیلد را تغییر دهد و تابع سوم مالکیت یک مقدار را دریافت کند. سپس عمداً چند خطای Ownership ایجاد کنید و پیام compiler را بخوانید. هدف تمرین این نیست که برنامه بدون خطا اجرا شود؛ هدف این است که بفهمید چرا compiler هر خطا را تولید می‌کند. بعد از اینکه مشکل را تحلیل کردید، آن را با borrowing یا تغییر طراحی حل کنید. اگر بتوانید بدون استفاده افراطی از clone() این برنامه را طراحی کنید، قدم مهمی در درک مدل Rust برداشته‌اید.

طراحی داده و مدیریت خطا در Rust

چرا باید داده‌ها را با Typeهای مناسب مدل کنیم؟

یکی از تفاوت‌های مهم برنامه‌نویسی حرفه‌ای با کدنویسی سطحی، نحوه مدل‌سازی داده‌هاست. در Rust بهتر است اطلاعات مهم برنامه را با typeهای مشخص نمایش دهید، نه اینکه همه چیز را به String و عددهای پراکنده تبدیل کنید. اگر وضعیت نوبت بیمار فقط یک رشته مانند "confirmed" باشد، احتمال اشتباه تایپی وجود دارد و compiler نمی‌تواند بسیاری از خطاها را تشخیص دهد. اما اگر همین وضعیت را با enum تعریف کنید، مجموعه حالت‌های معتبر مشخص می‌شود. این موضوع باعث می‌شود type system بخشی از منطق برنامه شما را بیان کند. در پروژه‌هایی مانند نرم‌افزار مدیریت کلینیک، این روش می‌تواند تعداد زیادی از خطاهای منطقی را قبل از اجرای برنامه کاهش دهد.

Struct برای مدل‌سازی موجودیت‌ها

struct یکی از اصلی‌ترین ابزارهای Rust برای ساخت typeهای سفارشی است. فرض کنید می‌خواهیم بیمار را مدل کنیم:

struct Patient {
    id: u32,
    name: String,
    age: u8,
}

حالا می‌توانیم نمونه‌ای از آن بسازیم:

let patient = Patient {
    id: 1,
    name: String::from("Ali"),
    age: 35,
};

این روش بسیار بهتر از نگهداری سه متغیر مستقل است، زیرا رابطه بین داده‌ها را مشخص می‌کند. وقتی پروژه بزرگ‌تر شود، می‌توانیم متدهایی نیز برای Patient تعریف کنیم. مثلاً بررسی کنیم آیا بیمار بزرگسال است یا اطلاعات خلاصه او را نمایش دهیم. Rust اجازه می‌دهد داده و رفتار مرتبط را کنار هم قرار دهیم. همین موضوع بعدها هنگام طراحی مدل‌های database و domain logic اهمیت زیادی پیدا می‌کند.

Enum برای حالت‌های مشخص

حالا فرض کنید وضعیت نوبت می‌تواند فقط یکی از چهار حالت مشخص باشد:

enum AppointmentStatus {
    Pending,
    Confirmed,
    Completed,
    Cancelled,
}

این ساختار از استفاده از string بسیار امن‌تر است. اگر کسی "comfirmed" را به جای "confirmed" بنویسد، در مدل string ممکن است خطا تا زمان اجرا باقی بماند، اما enum چنین مقداری را اساساً معتبر نمی‌داند. همچنین می‌توانیم با match تمام حالت‌ها را بررسی کنیم:

match status {
    AppointmentStatus::Pending => println!("در انتظار"),
    AppointmentStatus::Confirmed => println!("تأیید شده"),
    AppointmentStatus::Completed => println!("تکمیل شده"),
    AppointmentStatus::Cancelled => println!("لغو شده"),
}

یکی از مزایای مهم این روش این است که compiler در بسیاری از شرایط بررسی می‌کند همه variantها پوشش داده شده‌اند. بنابراین اضافه شدن یک حالت جدید به enum می‌تواند compiler را وادار کند بخش‌هایی از برنامه را که باید اصلاح شوند به شما نشان دهد.

Enum می‌تواند داده هم نگه دارد

قدرت واقعی enum زمانی مشخص می‌شود که بدانید variantها می‌توانند داده داشته باشند. مثلاً:

enum PaymentResult {
    Success(u64),
    Failed(String),
}

حالا Success می‌تواند مبلغ یا شناسه پرداخت را نگه دارد و Failed دلیل خطا را. این روش بسیار قدرتمندتر از ترکیب چند متغیر nullable است. شما عملاً ساختار داده را طوری تعریف می‌کنید که حالت‌های ممکن را بیان کند. این مدل در کتابخانه‌ها و APIهای Rust بسیار رایج است. در پروژه‌های واقعی، enumهای خوب طراحی‌شده می‌توانند بخش قابل توجهی از منطق validation را به type system منتقل کنند. این یکی از دلایلی است که Rust در پروژه‌های پیچیده بسیار جذاب می‌شود.

Option برای مقدارهای اختیاری

یکی از کاربردهای مهم enum، Option<T> است. فرض کنید یک بیمار ممکن است شماره تلفن دوم داشته باشد یا نداشته باشد:

struct Patient {
    name: String,
    second_phone: Option<String>,
}

اگر شماره وجود داشته باشد مقدار Some و اگر وجود نداشته باشد None خواهیم داشت. این مدل بهتر از استفاده از یک رشته خالی است، چون تفاوت بین «شماره‌ای وجود ندارد» و «شماره‌ای وجود دارد ولی خالی است» را مشخص می‌کند. برای کار با Option می‌توان از match استفاده کرد:

match patient.second_phone {
    Some(phone) => println!("Phone: {phone}"),
    None => println!("No second phone"),
}

همچنین روش‌های کاربردی دیگری مانند unwrap_or، map و and_then وجود دارند. در پروژه‌های واقعی بهتر است به جای استفاده بی‌مورد از unwrap()، مسیر نبود مقدار را آگاهانه مدیریت کنید.

Result برای مدیریت خطا

Result<T, E> برای عملیات‌هایی استفاده می‌شود که ممکن است موفق یا ناموفق باشند. برای نمونه، تابعی که اطلاعات بیمار را از فایل می‌خواند ممکن است فایل را پیدا کند یا با خطا مواجه شود. به جای اینکه برنامه ناگهان متوقف شود، تابع می‌تواند Result برگرداند:

fn load_patient() -> Result<String, String> {
    Ok(String::from("Patient data"))
}

در برنامه واقعی، نوع خطا معمولاً از یک type مناسب‌تر استفاده می‌کند و بهتر است خطاهای مختلف را از هم تفکیک کنیم. نکته مهم این است که Result شما را مجبور می‌کند مسیر خطا را ببینید. این ویژگی در برنامه‌هایی که با database، فایل، شبکه یا API خارجی کار می‌کنند اهمیت زیادی دارد.

چرا unwrap همیشه راه‌حل مناسبی نیست؟

ممکن است هنگام کار با Option یا Result به unwrap() برسید:

let value = result.unwrap();

اگر مقدار Err یا None باشد، unwrap() می‌تواند باعث panic شود. استفاده از آن در مثال‌های آموزشی کاملاً قابل قبول است، اما در کد production باید بدانید که این مقدار واقعاً چرا نباید خطا داشته باشد. اگر خطا ممکن است رخ دهد، بهتر است آن را با match، ? یا روش مناسب دیگری مدیریت کنید. استفاده کورکورانه از unwrap() تقریباً شبیه این است که چراغ هشدار خودرو را با چسب بپوشانید؛ مشکل ناپدید نشده، فقط دیگر آن را نمی‌بینید. در Rust حرفه‌ای باید مسیر خطا را طراحی کنید، نه اینکه آن را مخفی کنید.

یک مثال ترکیبی در پروژه واقعی

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

struct Appointment {
    patient_id: u32,
    status: AppointmentStatus,
    note: Option<String>,
}

enum AppointmentStatus {
    Pending,
    Confirmed,
    Cancelled,
}

حالا note اختیاری است و وضعیت نوبت فقط می‌تواند یکی از حالت‌های مشخص باشد. این طراحی از نظر domain بسیار واضح‌تر از ساختاری است که همه چیز را String نگه می‌دارد. اگر بعداً نیاز داشته باشیم نتیجه ذخیره نوبت را نیز مشخص کنیم، می‌توانیم Result را اضافه کنیم. این نوع طراحی دقیقاً همان چیزی است که باید هنگام ساخت پروژه‌های واقعی Rust تمرین کنید. هدف فقط یادگیری syntax نیست؛ باید یاد بگیرید مسئله واقعی را به typeهای دقیق تبدیل کنید.

 

از کدنویسی ساده Rust تا ساخت پروژه واقعی

چرا بعد از یادگیری syntax باید پروژه بسازیم؟

تا اینجا با متغیرها، توابع، Ownership، Borrowing، Struct، Enum و مدیریت خطا آشنا شده‌اید؛ اما هنوز یک مشکل وجود دارد: این مفاهیم زمانی ارزش واقعی پیدا می‌کنند که در یک پروژه چندفایلی استفاده شوند. نوشتن مثال‌های ده‌خطی برای یادگیری syntax مفید است، ولی نرم‌افزار واقعی معمولاً ده‌ها یا صدها فایل، dependency و module دارد. بنابراین مرحله بعدی یادگیری Rust باید عبور از مثال‌های جداگانه و ورود به ساختار پروژه واقعی باشد. در این مرحله Cargo، Module، Trait و Testing اهمیت بیشتری پیدا می‌کنند. اگر هدف شما ساخت برنامه‌ای مانند یک نرم‌افزار دسکتاپ با Tauri است، این مرحله حتی مهم‌تر می‌شود، زیرا منطق Rust دیگر محدود به یک فایل main.rs نخواهد بود.

Cargo قلب مدیریت پروژه Rust

Cargo ابزار اصلی مدیریت پروژه‌های Rust است و برای ساخت پروژه، مدیریت dependencyها، اجرای تست‌ها و build کردن برنامه استفاده می‌شود. یک پروژه جدید را می‌توانید با دستور زیر ایجاد کنید:

cargo new clinic_app
cd clinic_app
cargo run

فایل Cargo.toml اطلاعات مهم پروژه را نگه می‌دارد. وقتی یک crate خارجی به پروژه اضافه می‌کنید، dependency آن نیز در همین فایل مشخص می‌شود. Cargo سپس dependencyها را دریافت و پروژه را build می‌کند. این ساختار باعث می‌شود پروژه‌های Rust استاندارد و قابل بازتولید باشند. اگر قبلاً با composer.json در PHP یا package.json در Node.js کار کرده باشید، مفهوم کلی برایتان آشناست، اما Cargo بخشی عمیق‌تر از workflow Rust است و ابزارهای تست و build را نیز در خود جای می‌دهد.

Module برای جلوگیری از شلوغی پروژه

فرض کنید تمام منطق برنامه را در main.rs قرار دهید. در ابتدا مشکلی وجود ندارد، اما با بزرگ شدن پروژه فایل شما به سرعت غیرقابل مدیریت می‌شود. Rust برای حل این مشکل سیستم module دارد. می‌توانید مثلاً ساختار زیر را ایجاد کنید:

src/
├── main.rs
├── models.rs
├── services.rs
├── database.rs
└── utils.rs

در models.rs می‌توانید typeهایی مانند Patient و Appointment را قرار دهید و در services.rs منطق business را نگهداری کنید. سپس moduleها را از فایل اصلی معرفی می‌کنید. این تفکیک فقط برای زیبایی نیست؛ مرزهای منطقی پروژه را مشخص می‌کند و باعث می‌شود تغییر یک قسمت کمترین تأثیر را روی قسمت‌های دیگر داشته باشد. در پروژه‌های بزرگ، همین نظم اولیه می‌تواند هزینه نگهداری را به شدت کاهش دهد.

مفهوم Trait در Rust

اگر Struct را برای تعریف داده و Enum را برای حالت‌های مختلف یاد گرفته‌اید، مرحله بعدی آشنایی با Trait است. Trait را می‌توان به صورت یک قرارداد رفتاری در نظر گرفت. مثلاً می‌توانیم بگوییم هر موجودیتی که قابلیت نمایش دارد باید یک رفتار مشخص ارائه کند. یک مثال ساده:

trait Printable {
    fn print(&self);
}

حالا یک Struct می‌تواند این Trait را پیاده‌سازی کند:

struct Patient {
    name: String,
}

impl Printable for Patient {
    fn print(&self) {
        println!("{}", self.name);
    }
}

Traitها یکی از پایه‌های abstraction در Rust هستند و در کتابخانه‌های این زبان کاربرد گسترده‌ای دارند. بسیاری از مفاهیمی که در frameworkها و crateهای Rust مشاهده می‌کنید، بر Traitها بنا شده‌اند. بنابراین اگر هدف شما استفاده حرفه‌ای از Rust است، بعد از تسلط روی Ownership باید Trait را به‌صورت جدی یاد بگیرید.

Genericها و کدنویسی قابل استفاده مجدد

Generic به شما اجازه می‌دهد کدی بنویسید که با چند نوع داده کار کند. مثلاً:

fn first<T>(items: &[T]) -> Option<&T> {
    items.first()
}

این تابع وابسته به نوع خاصی نیست و می‌تواند روی sliceهای مختلف استفاده شود. البته generic فقط زمانی مفید است که abstraction شما واقعاً منطقی باشد. یکی از اشتباهات برنامه‌نویسان تازه‌کار این است که برای نشان دادن مهارت، همه چیز را generic می‌کنند. این کار می‌تواند کد را بی‌دلیل پیچیده کند. Generic باید زمانی استفاده شود که یک الگوریتم یا رفتار واقعاً مستقل از نوع مشخص باشد. بعداً Traitها می‌توانند محدودیت‌هایی روی genericها ایجاد کنند و قدرت type system Rust را بیشتر نشان دهند.

Testing را از ابتدا جدی بگیرید

Rust ابزار تست را مستقیماً در workflow خود دارد. می‌توانید یک تست ساده بنویسید:

#[cfg(test)]
mod tests {
    #[test]
    fn addition_works() {
        assert_eq!(2 + 2, 4);
    }
}

سپس:

cargo test

را اجرا کنید. تست فقط برای پروژه‌های بزرگ نیست. حتی در پروژه‌های کوچک نیز کمک می‌کند مطمئن شوید تغییرات جدید رفتار قبلی را خراب نکرده‌اند. در پروژه‌ای مانند سیستم مدیریت کلینیک می‌توانید برای محاسبه هزینه خدمات، اعتبارسنجی نوبت، وضعیت بیمه و منطق پرداخت تست بنویسید. این کار مخصوصاً زمانی مهم است که چند ماه بعد دوباره به پروژه برگردید. کد بدون تست ممکن است امروز درست کار کند، اما هیچ تضمینی برای فردا ندارد. Rust با ابزارهای استاندارد خود شروع تست‌نویسی را نسبتاً ساده می‌کند.

Rust و ارتباط با SQLite

اگر هدف شما ساخت یک برنامه دسکتاپ است، احتمالاً در نهایت به database نیاز خواهید داشت. SQLite برای بسیاری از برنامه‌های دسکتاپ گزینه مناسبی است و Rust نیز crateها و کتابخانه‌های متعددی برای کار با database دارد. در این مرحله نباید فوراً سراغ abstractionهای پیچیده بروید؛ ابتدا باید ارتباط بین Rust، connection، query و Result را بفهمید. عملیات database ذاتاً می‌تواند خطا داشته باشد، بنابراین مدل Result<T, E> در اینجا بسیار کاربردی است. اگر query شکست بخورد، باید مسیر خطا مشخص باشد. همین‌جا مفاهیمی که در هفته دوم یاد گرفتید وارد پروژه واقعی می‌شوند. یادگیری زمانی تثبیت می‌شود که بتوانید مفاهیم زبان را به یک نیاز واقعی متصل کنید.

آماده شدن برای Tauri

اگر هدف نهایی شما Tauri + React + Rust است، بعد از این سه هفته می‌توانید وارد مرحله جدی‌تری شوید. در Tauri معمولاً رابط کاربری با فناوری‌هایی مانند React ساخته می‌شود و بخش native برنامه با Rust کار می‌کند. در این محیط ممکن است با struct، enum، Result، State، commandها، serialization و async programming مواجه شوید. اگر Ownership و Borrowing را نفهمیده باشید، خطاهای Tauri می‌توانند بسیار گیج‌کننده شوند. اما اگر مبانی Rust را به‌درستی یاد گرفته باشید، بسیاری از این خطاها قابل تحلیل خواهند بود. به همین دلیل توصیه می‌شود قبل از ساخت یک پروژه بزرگ Tauri، چند پروژه کوچک Rust مستقل بسازید.

پروژه نهایی پیشنهادی برای جمع‌بندی

برای جمع‌بندی سه هفته، یک پروژه کوچک مدیریت بیماران طراحی کنید. پروژه باید یک Patient داشته باشد، بتواند چند بیمار را در یک Vec نگهداری کند، وضعیت عملیات را با Result مدیریت کند و برای داده‌های اختیاری از Option استفاده کند. کد را در چند module تقسیم کنید و برای حداقل دو تابع تست بنویسید. سپس یک قابلیت ساده مانند جستجوی بیمار بر اساس نام اضافه کنید. در این پروژه نباید هدف اصلی ظاهر برنامه باشد؛ هدف باید تمرین Rust واقعی باشد. اگر توانستید این پروژه را بدون استفاده بی‌دلیل از clone() و unwrap() طراحی کنید، یعنی مفاهیم پایه را به شکل قابل قبولی فهمیده‌اید.

قدم بعدی چیست؟

بعد از این سه هفته، دیگر نباید صرفاً به دنبال آموزش‌های مقدماتی Rust باشید. مسیر بعدی باید بر اساس هدف شما انتخاب شود. اگر می‌خواهید با Tauri کار کنید، به سراغ async/await، commandها، State Management، serialization، مدیریت خطا و ارتباط با SQLite بروید. اگر هدف شما Backend است، باید HTTP، async runtime، networking و frameworkهای مناسب را بررسی کنید. اگر به برنامه‌نویسی سیستمی علاقه دارید، باید موضوعاتی مانند concurrency، thread، channel، smart pointerها و lifetimeها را عمیق‌تر یاد بگیرید. مهم این است که از مرحله «آموزش دیدن» به مرحله ساختن منتقل شوید. در Rust، خواندن مستندات و حل خطاهای واقعی compiler بخش جدایی‌ناپذیر مسیر حرفه‌ای شدن است.

جمع‌بندی

یادگیری مبانی Rust در ابتدا ممکن است دشوارتر از بسیاری از زبان‌های سطح بالا به نظر برسد و این موضوع را نباید پنهان کرد. Rust از شما می‌خواهد درباره مالکیت داده، طول عمر referenceها، نوع داده و مدیریت خطا دقیق‌تر فکر کنید. همین سخت‌گیری دلیل اصلی قدرت آن است. وقتی Ownership و Borrowing را بفهمید، پیام‌های compiler دیگر صرفاً مانع نیستند؛ تبدیل به ابزاری برای طراحی بهتر کد می‌شوند. برای شروع، یک پروژه کوچک بسازید، از Cargo استفاده کنید و هر روز بخشی از زبان را با اجرای کد تمرین کنید. مستندات رسمی فعلی Rust بر نسخه‌های جدید و Edition 2024 تمرکز دارند و بهترین نقطه شروع برای ادامه مطالعه محسوب می‌شوند. اگر هدف نهایی شما Tauri است، بعد از مبانی می‌توانید به صورت مستقیم وارد مباحث Rust موردنیاز Tauri مانند Struct، Enum، Result، Command، State، Async و ارتباط با SQLite شوید. اگر هم هدف شما backend یا ابزارهای سیستمی است، مسیر مشابهی با تمرکز بیشتر روی async، networking و concurrency خواهید داشت. مهم این است که Rust را با حفظ کردن دستورها یاد نگیرید؛ مدل ذهنی زبان را یاد بگیرید. وقتی بفهمید چرا compiler یک کد را قبول نمی‌کند، آن زمان واقعاً Rust را شروع کرده‌اید.

 

منابع به‌روز

برای ادامه یادگیری، مرجع اصلی باید مستندات رسمی Rust باشد. نسخه فعلی کتاب رسمی Rust، نسخه‌های جدید زبان و Edition 2024 را پوشش می‌دهد. همچنین صفحه مستندات رسمی Rust مجموعه‌ای از منابع مانند Rust Book، Rust Reference، Cargo Book، Rustdoc و Clippy را در اختیار قرار می‌دهد.

The Rust Programming Language — کتاب رسمی Rust

Rust Reference — مرجع رسمی زبان

Rust Edition Guide — راهنمای Edition 2024

پرسش و پاسخ

1 . آیا یادگیری Rust برای برنامه‌نویس مبتدی مناسب است؟
بله، اما اگر فرد کاملاً با برنامه‌نویسی آشنا نباشد، Rust می‌تواند شروع نسبتاً سختی باشد. بهتر است ابتدا مفاهیم عمومی مانند variable، function، condition، loop و data structure را یاد بگیرید و سپس وارد Rust شوید. اگر تجربه PHP، JavaScript، C# یا زبان دیگری دارید، می‌توانید مستقیماً Rust را شروع کنید؛ فقط باید آماده باشید که Ownership و Borrowing را جدی یاد بگیرید.

2 . مهم‌ترین مفهوم Rust برای یادگیری چیست؟
اگر بخواهیم فقط یک مورد را انتخاب کنیم، Ownership مهم‌ترین مفهوم است. Borrowing، Reference و بخش قابل توجهی از مدل حافظه Rust نیز به آن مرتبط هستند. تا زمانی که ownership را درک نکرده باشید، بسیاری از خطاهای compiler برایتان بی‌معنی خواهند بود.

3 . آیا Rust از C++ بهتر است؟
این سؤال پاسخ مطلق ندارد. Rust در زمینه memory safety و جلوگیری از برخی خطاهای رایج حافظه مزیت مهمی دارد، اما C++ اکوسیستم بسیار بزرگ و سابقه طولانی دارد و در بسیاری از پروژه‌های موجود انتخاب منطقی است. انتخاب بین این دو باید بر اساس پروژه، تیم، کتابخانه‌های موردنیاز و محدودیت‌های فنی انجام شود.

4 . آیا برای یادگیری Tauri باید Rust بلد باشیم؟
برای ساخت پروژه‌های بسیار ساده ممکن است مقدار زیادی از Rust را ندانید، اما برای پروژه واقعی بهتر است مبانی Rust را جدی یاد بگیرید. مفاهیمی مانند struct، enum، Result، Option، ownership، borrowing، async و error handling در پروژه‌های Tauri بسیار کاربردی هستند. یادگیری Rust قبل از ورود جدی به Tauri باعث می‌شود وابستگی شما به کپی کردن کدهای آماده کمتر شود.

5 . بهترین روش تمرین Rust چیست؟
بهترین روش، ترکیب مطالعه و پروژه کوچک است. یک روز درباره Ownership مطالعه کنید و همان روز برنامه‌ای بنویسید که String را به چند تابع ارسال کند. روز بعد Borrowing را با &str و &mut String تمرین کنید. سپس پروژه‌هایی مانند CLI مدیریت کارها، دفترچه مخاطبین، سیستم ساده مدیریت بیماران یا برنامه خواندن فایل بسازید. کدنویسی واقعی مهم‌تر از تماشای ده‌ها ساعت آموزش ویدئویی است.

دیدگاه ها

برای ارسال دیدگاه وارد حساب کاربری خود شوید.