مقدمه
به فصل «اشارهگرهای هوشمند» خوش آمدید. «اشارهگر» (pointer) یک مفهوم کلی برای متغیری است که آدرس
یک مقدار دیگر در حافظه را نگهداری میکند. ما قبلاً با یکی از انواع اشارهگرها در Rust، یعنی
رفرنسها (&)، آشنا شدهایم. اما کتابخانه استاندارد Rust انواع دیگری از اشارهگرها را نیز ارائه
میدهد که قابلیتهای بیشتری نسبت به رفرنسهای ساده دارند. این نوعها را «اشارهگرهای هوشمند»
(Smart Pointers) مینامند، زیرا علاوه بر نگهداری آدرس، دارای متادیتا و قابلیتهای اضافی هستند.
در این فصل، به بررسی مهمترین اشارهگرهای هوشمند در Rust میپردازیم. اولین و سادهترین آنها،
Box<T> است که به ما اجازه میدهد دادهها را به جای stack، در heap تخصیص دهیم.
استفاده از Box<T> برای ذخیره داده در Heap
سادهترین کاربرد Box<T>، گرفتن یک مقدار و تخصیص آن روی heap است. وقتی شما یک مقدار را
در یک Box قرار میدهید، خود اشارهگر Box (که شامل آدرس داده در heap است) روی
stack باقی میماند، اما دادهی واقعی به heap منتقل میشود.
src/main.rs
fn main() {
let b = Box::new(5);
println!("b = {}", b);
}
در این مثال، عدد 5 که معمولاً روی stack قرار میگیرد، به داخل یک Box منتقل شده و در
نتیجه روی heap تخصیص داده میشود. وقتی متغیر b از حوزه خارج میشود، Box به صورت
خودکار حافظه تخصیص داده شده در heap را آزاد میکند. این یکی از قابلیتهای اشارهگرهای
هوشمند است: پیادهسازی Trait مربوط به Drop برای مدیریت خودکار منابع.
کاربرد اصلی Box<T>: نوعهای بازگشتی
شاید بپرسید چرا باید یک مقدار ساده مانند i32 را که سایز مشخصی دارد، روی heap قرار دهیم؟
کاربرد اصلی Box<T> زمانی مشخص میشود که با نوعهایی سروکار داریم که کامپایلر نمیتواند سایز
آنها را در زمان کامپایل تعیین کند. یکی از مهمترین این موارد، «نوعهای بازگشتی» (recursive
types) است.
یک نوع بازگشتی، نوعی است که در تعریف خود، به خودش ارجاع میدهد. برای مثال، enum زیر را که یک
لیست پیوندی (cons list) را مدل میکند، در نظر بگیرید.
src/main.rs
این کد کامپایل نمیشود، زیرا کامپایلر Rust نمیتواند سایز نوع List را محاسبه کند. یک List
شامل یک Cons است که خود شامل یک List دیگر است و این زنجیره تا بینهایت ادامه دارد. برای حل
این مشکل، ما باید از یک نوع اشارهگر استفاده کنیم تا این بازگشت را بشکنیم. با قرار دادن List
بازگشتی در داخل یک Box، ما به کامپایلر میگوییم که Cons به جای نگهداری مستقیم یک List
دیگر، یک اشارهگر به آن را نگهداری میکند. سایز یک اشارهگر همیشه در زمان
کامپایل مشخص است.
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 (*) را برای اشارهگرهای هوشمند خود سفارشی کنیم.