فهرست مطالب
وقتی برای نخستین بار کدی به زبان Rust مینویسید، خطاهای مکرر کامپایلر درباره انتقال متغیرها یا پایان عمر دادهها کلافهکننده به نظر میرسد. در زبانهایی مثل پایتون یا جاوااسکریپت، زبالهروب به شکل خودکار حافظه مصرفشده را پاکسازی میکند. در زبانهایی مانند C نیز وظیفه تخصیص و آزادسازی حافظه کاملاً بر دوش برنامهنویس است؛ رویکردی که بارها به خطاهای مهلکی مثل نشت حافظه یا اشارهگرهای معلق ختم میشود. زبان Rust راه سوم را برگزیده است: مدیریت حافظه بر پایه سیستم مالکیت (Ownership) که در زمان کامپایل بررسی میشود و هیچ بار پردازشی اضافی در زمان اجرا تحمیل نمیکند.
پشته و هیپ چگونه رفتار متغیرها را تعیین میکنند
برای درک عملکرد سیستم مالکیت، باید تفاوت دو بخش حافظه یعنی پشته (Stack) و هیپ (Heap) را بشناسید. پشته دادهها را به ترتیب ورود ذخیره میکند و با همان ترتیب معکوس خارج میسازد. دسترسی به پشته فوقالعاده سریع است، اما اندازه دادههایی که در آن قرار میگیرند باید در زمان کامپایل قطعی و ثابت باشد.
در مقابل، دادههایی که طول متغیر دارند یا اندازه آنها هنگام اجرا مشخص میشود، در هیپ ذخیره میگردند. زمانی که درخواستی برای ذخیره داده روی هیپ ثبت میکنید، تخصیصدهنده حافظه فضایی با اندازه مناسب پیدا میکند، آن را علامت میزند و یک آدرس یا اشارهگر بازمیگرداند. نگهداری این اشارهگر با اندازه معین روی پشته انجام میشود، اما دسترسی به داده اصلی نیازمند دنبال کردن آدرس خواهد بود. هدف اصلی سیستم مالکیت در Rust، مدیریت دادههای موجود در هیپ است تا حافظه بیهوده هدر نرود و آزادسازی منابع در لحظه مناسب رخ دهد.
سه قانون طلایی سیستم مالکیت
کامپایلر Rust صحت اجرای برنامه را بر اساس سه قانون بنیادین میسنجد:
۱. هر مقدار در Rust یک مالک مشخص دارد.
۲. در هر لحظه، تنها یک مالک برای هر مقدار مجاز است.
۳. وقتی مالک از حوزه دسترسی (Scope) خارج شود، مقدار به شکل خودکار حذف و حافظه آزاد میگردد.
این قوانین تضمین میکنند که دو بخش مختلف از برنامه، همزمان ادعای آزادسازی یک بلوک حافظه را نداشته باشند و خطای آزادسازی مضاعف رخ ندهد.
انتقال مالکیت در عمل و خطای متداول کامپایلر
نوع داده String نمونهای ایدئال برای نمایش رفتار هیپ است. این نوع داده برخلاف رشتههای ثابت درون کد، اندازه متغیر دارد و روی هیپ ذخیره میشود. بیایید قطعه کد زیر را بررسی کنیم:
rustfn main() {
// تخصیص حافظه روی هیپ برای رشته اول
let s1 = String::from("salam");
// انتقال مالکیت داده از s1 به s2
let s2 = s1;
// تلاش برای استفاده از متغیر اول که دیگر مالکیتی ندارد
println!("{}", s1);
}
اگر این کد را کامپایل کنید، برنامه با خطای زیر متوقف میشود و اجرا نخواهد شد:
texterror[E0382]: borrow of moved value: `s1`
--> src/main.rs:۸:۲۰
|
۴ | let s1 = String::from("salam");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
۵ | let s2 = s1;
| -- value moved here
...
۸ | println!("{}", s1);
| ^^
در بسیاری از زبانها با نسبت دادن s1 به s2، فقط یک اشارهگر جدید کپی میشود یا کل داده بازنویسی میگردد. در Rust، اشارهگر روی پشته منتقل شده و s1 نامعتبر اعلام میشود. این مفهوم Move نام دارد. چون s1 دیگر مالکیتی بر رشته ندارد، استفاده از آن پس از انتقال ممنوع است.
اشتباه رایج در کپی سطحی و راهکار درست
برنامهنویسان تازهکار غالباً گمان میکنند رفتار متغیرهایی مانند عدد صحیح یا کاراکتر باید روی ساختارهای پیچیده هم تکرار شود. متغیرهای با اندازه ثابت روی پشته به سرعت کپی میشوند و انتقال مالکیت رخ نمیدهد، اما برای دادههای روی هیپ، تخصیص دوباره منابع هزینه دارد. اگر واقعاً به یک نسخه کپی مستقل و عمیق نیاز دارید، باید متد clone را فراخوانی کنید:
rustfn main() {
let s1 = String::from("salam");
// ایجاد کپی کامل از داده روی هیپ
let s2 = s1.clone();
// هر دو متغیر معتبر باقی میمانند
println!("s1: {} و s2: {}", s1, s2);
}
با اجرای این کد، خروجی بدون هیچ مشکلی نمایش داده خواهد شد:
texts1: salam و s2: salam
سختگیری کامپایلر Rust مانع از ایجاد باگهای پنهان در سطح سیستم است. با تسلط بر این رفتارها متوجه میشوید که امنیت و پایداری کد، ارزش پایبندی به این قواعد را دارد. برای یادگیری گامبهگام و تسلط بر مباحث امانتگرفتن و طول عمرها، دوره راست از صفر تا برنامهنویس مسیر یادگیری را پیش روی شما قرار میدهد.





نظرها
اولین نفری باشید که نظر میدهدسؤالتان را بپرسید یا تجربهتان را بنویسید تا نویسندهی مطلب پاسخ بدهد.