یکی از مهمترین دلایلی که Rust را به زبانی ایمن، بدون نیاز به Garbage Collector و در عین حال قدرتمند و سریع تبدیل کرده، سیستم مالکیت حافظه آن است. در این بخش با مفاهیمی آشنا میشویم که اساس ایمنی حافظه در Rust را تشکیل میدهند: مالکیت (Ownership)، وامگیری (Borrowing)، طولعمر (Lifetimes) و مفاهیم مرتبط
در Rust، هر مقدار (value) یک مالک (owner) دارد. وقتی متغیری مقداری را در اختیار میگیرد، مالک آن مقدار میشود.
قوانین اصلی مالکیت:
1- هر مقدار دقیقاً یک مالک دارد.
2 - وقتی مالک از بین برود (از اسکوپ خارج شود)، مقدار نیز آزاد میشود.
3 - اگر مالکیت را منتقل کنید، دیگر نمیتوانید از متغیر قبلی استفاده کنید.
مثال ساده:
fn main() {let name = String::from("Ali");let new_name = name; // مالکیت به new_name منتقل شد// println!("{}", name); // خطا! چون name دیگر مالک نیست}
در این مثال، با انتساب name به new_name، مالکیت رشته منتقل میشود و استفاده از name پس از آن مجاز نیست.
گاهی اوقات میخواهیم به یک مقدار دسترسی داشته باشیم بدون اینکه مالک آن شویم. در این حالت، از مفهوم وامگیری استفاده میکنیم؛ یعنی ارسال یک ارجاع (reference).
ارجاع خواندنی (Immutable Borrow):
fn print_name(name: &String) {println!("Name: {}", name);}fn main() {let my_name = String::from("Sara");print_name(&my_name); // ارجاع دادهایم، نه مالکیتprintln!("{}", my_name); // مشکلی نیست، چون مالکیت همچنان با ماست}
ارجاع قابل تغییر (Mutable Borrow):
برای تغییر یک مقدار، باید وامگیری را به صورت قابل تغییر انجام دهید:
fn append_hello(text: &mut String) {text.push_str(" سلام");}fn main() {let mut msg = String::from("سلام");append_hello(&mut msg);println!("{}", msg); // خروجی: سلام سلام}
محدودیتهای مهم وامگیری:
- در یک زمان فقط یک ارجاع قابل تغییر مجاز است.
- یا چند ارجاع خواندنی، ولی نه هر دو همزمان.
این قوانین باعث میشوند که شرایط خطرناکی مثل race condition بهصورت compile-time شناسایی شوند.
ارجاعها در Rust ممکن است به دادههایی اشاره کنند که دیگر وجود ندارند. برای جلوگیری از این خطاها، Rust از مفهومی به نام طولعمر (lifetime) استفاده میکند.
چرا Lifetimes نیاز داریم؟
fn get_ref() -> &String {let s = String::from("سلام");&s // خطا: s از بین میرود و ارجاع بیاعتبار میشود}
در اینجا ارجاعی برمیگردد به مقداری که در داخل تابع تعریف شده و با پایان تابع پاک میشود.
تعریف طولعمر:
با استفاده از 'a و مشابه آن، میتوان مشخص کرد که طولعمر یک ارجاع چقدر است:
fn longer<'a>(x: &'a str, y: &'a str) -> &'a str {if x.len() > y.len() { x } else { y }}
در این مثال تابع longer مشخص میکند که مقدار برگشتی حداقل تا زمانی زنده است که x و y زنده هستند.
Rust در بسیاری از موارد lifetimes را بهصورت خودکار (lifetime elision) تشخیص میدهد، ولی در توابع پیچیده یا بازگشتی باید بهصراحت مشخص شوند.
Copy Trait:
برخی نوعها مانند i32, bool, char بهصورت پیشفرض قابلیت کپی (Copy) دارند. وقتی این نوعها را منتقل میکنید، نسخهای جدید ساخته میشود، نه اینکه مالکیت منتقل شود.
let a = 10;let b = a; // کپی شده، a همچنان قابل استفاده است
Clone Trait:
برای ساخت کپی از نوعهایی مثل String که بهصورت پیشفرض Move میشوند، باید از متد clone() استفاده کرد:
let s1 = String::from("Hello");let s2 = s1.clone(); // کپی عمیق
Drop Trait:
وقتی یک مقدار از اسکوپ خارج میشود، متد drop برای آزادسازی منابع اجرا میشود. معمولاً خودتان آن را نمینویسید، اما میتوانید آن را override کنید.
struct Log;impl Drop for Log {fn drop(&mut self) {println!("Log حذف شد");}}fn main() {let _l = Log {};} // در اینجا drop اجرا میشود
در این بخش یاد گرفتیم:
- مالکیت (Ownership) مفهوم پایهای مدیریت حافظه در Rust است.
- با وامگیری (Borrowing) میتوان از مقادیر استفاده کرد بدون گرفتن مالکیت آنها.
- طولعمرها (Lifetimes) به Rust کمک میکنند که ارجاعهایی معتبر و ایمن داشته باشیم.
- ویژگیهایی مانند Copy, Clone و Drop رفتار نوعها را هنگام کپی و آزادسازی مشخص میکنند.
این مفاهیم، پایهی ایمنی حافظه در Rust هستند و درک آنها برای ورود به برنامهنویسی حرفهای با این زبان، ضروری است.