آموزش زبان Rust | مدیریت حافظه در Rust

مالکیت در Rust

مالکیت (Ownership) مهم‌ترین و متمایزترین ویژگی زبان Rust است. این مفهوم به Rust اجازه می‌دهد بدون نیاز به زباله‌روب (Garbage Collector) و بدون خطاهای رایجی مثل دسترسی به حافظه‌ی آزادشده، برنامه‌ها را ایمن و کارآمد نگه دارد. اگر مالکیت را به‌درستی یاد بگیرید، بقیه‌ی مفاهیم Rust بسیار ساده‌تر خواهند شد.

قوانین اصلی مالکیت

Rust سه قانون ساده ولی بسیار قدرتمند دارد:

  1. هر مقدار در Rust یک «مالک» (owner) دارد.
  2. در هر لحظه فقط یک مالک برای هر مقدار وجود دارد.
  3. وقتی مالک از حوزه‌ی دید (scope) خارج شود، مقدار به‌طور خودکار آزاد می‌شود.

این سه قانون اساس تمام رفتار Rust در مدیریت حافظه هستند.

مثال ساده از مالکیت

به کد زیر توجه کنید:

fn main() {
    let s = String::from("سلام");
    println!("{}", s);
}

در اینجا متغیر s مالک رشته‌ی ساخته‌شده است. وقتی تابع main به پایان می‌رسد، s از حوزه‌ی دید خارج می‌شود و Rust به‌طور خودکار حافظه‌ی مربوطه را آزاد می‌کند. نیازی به فراخوانی دستی تابعی مثل free نیست.

انتقال مالکیت (Move)

وقتی یک مقدار به متغیر دیگری نسبت داده می‌شود، مالکیت منتقل می‌شود:

fn main() {
    let s1 = String::from("سلام");
    let s2 = s1;
    println!("{}", s2);
}

اینجا s1 مالک رشته بود، اما با نوشتن let s2 = s1; مالکیت به s2 منتقل شد. از این لحظه به بعد، استفاده از s1 خطای کامپایل ایجاد می‌کند، چون دیگر مالک چیزی نیست. این رفتار از خطاهای رایجی مثل استفاده‌ی همزمان از یک منبع جلوگیری می‌کند.

اگر بخواهید هر دو متغیر معتبر بمانند، باید از clone استفاده کنید:

let s1 = String::from("سلام");
let s2 = s1.clone();
println!("{} و {}", s1, s2);

در این حالت یک کپی کامل از داده روی حافظه‌ی heap ساخته می‌شود و هر متغیر مالک نسخه‌ی خودش است.

مالکیت و توابع

مالکیت وقتی یک متغیر به تابعی پاس داده می‌شود هم منتقل می‌شود:

fn main() {
    let s = String::from("سلام");
    takes_ownership(s);
}

fn takes_ownership(some_string: String) {
    println!("{}", some_string);
}

بعد از فراخوانی takes_ownership، دیگر نمی‌توانید از s استفاده کنید، چون مالکیت آن به تابع منتقل شده و در پایان تابع، مقدار آزاد می‌شود.

برای برگرداندن مالکیت، می‌توان مقدار را از تابع بازگرداند:

fn main() {
    let s1 = gives_ownership();
    println!("{}", s1);
}

fn gives_ownership() -> String {
    String::from("سلام")
}

تفاوت Copy و Move

انواع داده‌ای که اندازه‌ی ثابت و مشخصی دارند (مثل اعداد صحیح، بولین‌ها، کاراکترها و تاپل‌هایی که فقط شامل این نوع داده‌ها باشند) از تِرِیت Copy پیروی می‌کنند. برای این نوع داده‌ها، هنگام انتساب یا پاس دادن به تابع، به‌جای انتقال مالکیت یک کپی ساده انجام می‌شود:

fn main() {
    let x = 5;
    let y = x;
    println!("{} و {}", x, y); // هر دو معتبرند
}

اما انواعی مثل String که داده‌ی خود را روی heap نگه می‌دارند، از Copy پیروی نمی‌کنند و همیشه مالکیتشان منتقل (move) می‌شود.

مالکیت در حوزه‌های دید

مالکیت فقط به متغیرهای ساده محدود نیست. هر بلوکی که با آکولاد {} مشخص می‌شود، یک حوزه‌ی دید جدید ایجاد می‌کند:

fn main() {
    let s1 = String::from("بیرون");
    {
        let s2 = String::from("داخل");
        println!("{} و {}", s1, s2);
    }
    // println!("{}", s2); // خطا می‌دهد چون s2 اینجا آزاد شده
    println!("{}", s1);
}

متغیر s2 فقط داخل بلوک داخلی معتبر است و به‌محض بسته شدن بلوک، حافظه‌اش آزاد می‌شود. اما s1 در حوزه‌ی بیرونی همچنان معتبر است. (توجه کنید که استفاده از s2 در بلوک داخلی مشکلی ندارد، چون فقط «قرض» گرفته شده، نه اینکه مالکیت منتقل شود. این نکته در مبحث قرض‌گیری بیشتر توضیح داده می‌شود.)

چرا مالکیت مهم است؟

مدل مالکیت باعث می‌شود:

  • خطاهای مربوط به دسترسی به حافظه‌ی آزادشده (use-after-free) در زمان کامپایل شناسایی شوند.
  • خطاهای ناشی از دو اشاره‌گر همزمان که داده را تغییر می‌دهند (data race) در سطح کامپایلر جلوگیری شوند.
  • نیازی به زباله‌روب نباشد و کارایی برنامه‌ها بالا بماند.

به همین دلیل است که مالکیت را «قلب» زبان Rust می‌دانند و درک آن، پیش‌نیاز یادگیری مباحث بعدی مثل قرض‌گیری و عمرهاست.

پرسش و پاسخ این درس

برای ثبت پرسش ابتدا در سایت وارد شوید.

  • 1
  • 2
  • 3
  • 4