مقدمه

به فصل «اشاره‌گرهای هوشمند» خوش آمدید. «اشاره‌گر» (pointer) یک مفهوم کلی برای متغیری است که آدرس یک مقدار دیگر در حافظه را نگهداری می‌کند. ما قبلاً با یکی از انواع اشاره‌گرها در Rust، یعنی رفرنس‌ها (&)، آشنا شده‌ایم. اما کتابخانه استاندارد Rust انواع دیگری از اشاره‌گرها را نیز ارائه می‌دهد که قابلیت‌های بیشتری نسبت به رفرنس‌های ساده دارند. این نوع‌ها را «اشاره‌گرهای هوشمند» (Smart Pointers) می‌نامند، زیرا علاوه بر نگهداری آدرس، دارای متادیتا و قابلیت‌های اضافی هستند.

در این فصل، به بررسی مهم‌ترین اشاره‌گرهای هوشمند در Rust می‌پردازیم. اولین و ساده‌ترین آنها، Box<T> است که به ما اجازه می‌دهد داده‌ها را به جای stack، در heap تخصیص دهیم.

استفاده از Box<T> برای ذخیره داده در Heap

ساده‌ترین کاربرد Box<T>، گرفتن یک مقدار و تخصیص آن روی heap است. وقتی شما یک مقدار را در یک Box قرار می‌دهید، خود اشاره‌گر Box (که شامل آدرس داده در heap است) روی stack باقی می‌ماند، اما داده‌ی واقعی به heap منتقل می‌شود.

Copy Icon src/main.rs
fn main() {
    // Create a new box. The value `5` will be allocated on the heap.
    let b = Box::new(5);
    println!("b = {}", b);
} // `b` goes out of scope here. The box and its data are deallocated.

در این مثال، عدد 5 که معمولاً روی stack قرار می‌گیرد، به داخل یک Box منتقل شده و در نتیجه روی heap تخصیص داده می‌شود. وقتی متغیر b از حوزه خارج می‌شود، Box به صورت خودکار حافظه تخصیص داده شده در heap را آزاد می‌کند. این یکی از قابلیت‌های اشاره‌گرهای هوشمند است: پیاده‌سازی Trait مربوط به Drop برای مدیریت خودکار منابع.

کاربرد اصلی Box<T>: نوع‌های بازگشتی

شاید بپرسید چرا باید یک مقدار ساده مانند i32 را که سایز مشخصی دارد، روی heap قرار دهیم؟ کاربرد اصلی Box<T> زمانی مشخص می‌شود که با نوع‌هایی سروکار داریم که کامپایلر نمی‌تواند سایز آنها را در زمان کامپایل تعیین کند. یکی از مهم‌ترین این موارد، «نوع‌های بازگشتی» (recursive types) است.

یک نوع بازگشتی، نوعی است که در تعریف خود، به خودش ارجاع می‌دهد. برای مثال، enum زیر را که یک لیست پیوندی (cons list) را مدل می‌کند، در نظر بگیرید.

Copy Icon src/main.rs
// This code will NOT compile!
/*
enum List {
    Cons(i32, List),
    Nil,
}
*/

این کد کامپایل نمی‌شود، زیرا کامپایلر Rust نمی‌تواند سایز نوع List را محاسبه کند. یک List شامل یک Cons است که خود شامل یک List دیگر است و این زنجیره تا بی‌نهایت ادامه دارد. برای حل این مشکل، ما باید از یک نوع اشاره‌گر استفاده کنیم تا این بازگشت را بشکنیم. با قرار دادن List بازگشتی در داخل یک Box، ما به کامپایلر می‌گوییم که Cons به جای نگهداری مستقیم یک List دیگر، یک اشاره‌گر به آن را نگهداری می‌کند. سایز یک اشاره‌گر همیشه در زمان کامپایل مشخص است.

Copy Icon src/main.rs
enum List {
    Cons(i32, Box<List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(1, Box::new(Cons(2, Box::new(Nil))));
}

اکنون کد ما به درستی کامپایل می‌شود. Box<T> به ما اجازه داد تا یک نوع با سایز نامشخص (بازگشتی) بسازیم و داده‌های آن را روی heap مدیریت کنیم.

سایر کاربردهای Box<T>

علاوه بر نوع‌های بازگشتی، دو کاربرد رایج دیگر برای Box وجود دارد:

  • انتقال مالکیت داده‌های حجیم: اگر یک struct بزرگ دارید و می‌خواهید مالکیت آن را بدون کپی کردن کل داده‌ها منتقل کنید، می‌توانید آن را در یک Box قرار دهید. در این صورت، تنها اشاره‌گر (که سایز کوچکی دارد) روی stack کپی می‌شود.
  • Trait Objects: زمانی که می‌خواهید یک مقدار داشته باشید که یک trait خاص را پیاده‌سازی می‌کند، اما نوع دقیق آن را در زمان کامپایل نمی‌دانید، می‌توانید از یک "trait object" مانند Box<dyn MyTrait> استفاده کنید. این مبحث پیشرفته در فصل‌های بعدی بررسی خواهد شد.

در این درس با Box<T> به عنوان ساده‌ترین اشاره‌گر هوشمند در Rust آشنا شدیم. دیدیم که کاربرد اصلی آن، تخصیص داده‌ها روی heap برای ساخت نوع‌های بازگشتی یا انتقال مالکیت داده‌های بزرگ است. در درس بعدی، به بررسی Deref Trait خواهیم پرداخت و یاد می‌گیریم که چگونه رفتار عملگر dereference (*) را برای اشاره‌گرهای هوشمند خود سفارشی کنیم.