اگر به دنبال یادگیری یک زبان برنامهنویسی مدرن، سریع و قابلاعتماد هستید، 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 را یاد بگیرید. هر مفهوم را با کد واقعی امتحان کنید.