زبان Rust در چند سال اخیر محبوبیت زیادی بین برنامهنویسان سراسر دنیا کسب کرده که دلیل آن، رویکرد متفاوت آن در حل مسائل مختلف سیستمی است. این محبوبیت و رشد به حدی است که در کنار Mozilla، شرکتهایی نظیر گوگل، مایکروسافت و آمازون نیز به Rust Foundation پیوستهاند تا سهمی در توسعه و کاربردیتر کردن این زبان داشته باشند.
همانطور که اشاره کردم، زبان Rust در دسته زبانهای سیستمی قرار میگیرد و به طبع تفاوتهای عمدهای با زبانهایی نظیر سیشارپ و جاوا دارد. با این حال نمیتوان آن را به طور کامل مشابه زبانهای سیستمی دیگر نظیر زبان C در نظر گرفت. اصلیترین دلیل تفاوت این زبان با سایر زبانها، نحوه مدیریت حافظه توسط آن است. این تغییر رویکرد در مدیریت حافظه، Rust را به امنترین زبان برنامهنویسی سیستمی تبدیل کرده است.
منظور از مدیریت حافظه، نحوه تخصیص و آزادسازی فضای مورد نیاز برای متغییرهایی است که در طول اجرای برنامه از آنها استفاده میشود، است.
از طرف دیگر همین مسئله مهم و ارزشمند، چالشهای زیادی را برای یادگیری آن توسط برنامهنویسان، حتی برنامهنویسان با تجربه ایجاد کرده است که در ادامه به توضیح آن خواهم پرداخت.
برای درک بهتر مسئله اجازه دهید مرور کوتاهی بر مدلهای مدیریت حافظه، در زبانهای دیگر داشته باشیم.
در زبانهایی نظیر C، برنامهنویس وظیفه دارد فضای مورد نیاز برای هر متغییر را توسط دستورات برنامه رزرو کند و پس از اتمام نیاز، آن بخش از حافظه را آزاد نماید. همین عملیات مشکلات زیادی را میتواند ایجاد کند، مانند اینکه یک بخش از حافظه دوبار آزاد شود، به آدرسی اشاره شود که null است و مسائل دیگری که برنامهنویسان این زبانها حداقل یکبار به آن برخورد کردهاند.
در زبانهای سطح بالاتر نظیر سیشارپ، جاوا و گولنگ، مدیریت حافظه توسط Garbage Collector انجام میشود. به این شکل دیگر برنامه نویس درگیر تخصیص و آزادسازی حافظه نمیشود و این عملیات توسط GC اتفاق میافتد. این موضوع در کنار راحتیای که برای برنامهنویس فراهم میکند، کنترل او را بر اتفاقاتی که در حافظه میافتد از بین میبرد. در برخی از موارد همین نحوه مدیریت حافظه توسط GC میتواند حفرههای امنیتیای در برنامه ایجاد کند و برنامههای مخرب بتوانند از این حفرهها استفاده کنند و مشکلاتی در سیستم ایجاد نمایند.
در زبان Rust، برنامهنویس نه درگیر تخصیص و آزادسازی حافظه میشود و نه مفهومی به اسم Garbage Collerctorوجود دارد که این کار را برای او انجام دهد. بلکه باید با پیروی از یک سری اصول و درنظر گرفتن یک سری از محدودیتها در تعریف و نحوه استفاده از متغییرها، عمل تخصیص و آزادسازی حافظه را انجام دهد. نکته قابل تأمل این است که عمل بررسی صحت این اصول و درنظر گرفتن محدودیتها در زمان کامپایل انجام میشود و اگر برنامه در به کارگیری صحیح هر کدام از این موارد مشکل داشته باشد، کامپایلر خطا خواهد داد و برنامه کامپایل نخواهد شد.
از مزایای این نوع مدیریت حافظه در زبان Rust میتوان به این مسئله اشاره کرد که اگر برنامه شما به صورت multi-thread نوشته شده باشد، این تضمین وجود دارد که به هیچ وجه data race اتفاق نمیافتد. به این معنی که هیچگاه دو thread از برنامه به طور همزمان به یک بخش از حافظه دسترسی نخواهند داشت. همچنین مفهومی به اسم null و یا nil که در سایر زبانها وجود دارد در Rust نیست، به این دلیل که هیچگاه شما اجازه ندارید برنامهای را کامپایل کنید که متغییری در آن به فضای خالی در حافظه اشاره کند.
زبان Rust این اصول و محدودیتها را با استفاده از دو مفهوم کلی Ownership و Borrowing انجام میدهد.
مهفوم Ownership به این معناست که در طول برنامه، فقط و فقط یک متغییر میتواند مالک یک دادهی مشخص درون حافظه باشد. با این فرض، اگر مقدار یک متغییر را به طور مستقیم به متغییر دیگر نسبت دهیم، در اصل مالکیت آن را منتقل(move) کردهایم.
مفهوم Borrowing به این معناست که اگر متغییری نیاز داشته باشد تا به محتوای یک متغییر دیگر دسترسی داشته باشد، باید آن را قرض بگیرد، در این صورت مادامی که محتوای درون حافظه در اختیار متغییر دوم است، متغییر اول هیچ گونه مالکیتی نسبت به آن ندارد، تا زمانی که متغییر دوم آن را پس دهد.
همین دو مفهوم به ظاهر ساده، اگر به درستی درک نشوند، چالشهای زیادی را برای برنامهنویس رقم خواهند زد که در زبانهای دیگر اصلا به آن بر نخواهند خورد. برای درک بهتر مسئله اجازه دهید این موضوع را در یک مثال ساده ببینیم:
fn main() { let list_one = vec![1, 2, 3]; let list_two = list_one; println!("{:?}", list_one); }
در این مثال، ابتدا یک لیست از اعداد ایجاد شده و در متغییر list_one قرار داده شده، و سپس همان متغییر به متغییر جدیدی به نام list_two نسبت داده شدهاست. در همین خط مالکیت لیست مذکور از متغییر اول به متغییر دوم تغییر میکند و در خط چهارم که قصد چاپ لیست اول را داریم کامپایلر خطایی میدهد با این مضمون که قصد قرض گرفتن مقداری برای چاپ را دارید که قبلا به متغییر دیگری move شده است.
error[E0382]: borrow of moved value: `list_one`
--> src/main.rs:4:22
برای برطرف کردن این مشکل میتوان یک Reference از متغییر اول را به متغییر دوم داد. یعنی خط سوم را به شکل زیر نوشت:
let list_two = &list_one;
پس اگر همین چند اصل ساده را به خوبی درک نکرده باشیم، به احتمال زیاد در شروع کار با Rust به چالشهای ساده ولی به ظاهر پیچیدهای بر خواهیم خورد.
توضیحاتی که در خصوص زبان Rust و چالشهای آن در این مقاله نوشتم، بسیار مختصر و در حد و حوصله همین پست بود. به طور حتم پس از شروع به کار با این زبان به چالشهای متنوعتری بر خواهید خورد و در خیلی از موارد هم از زیبایی طراحی و کمک آن در حل مسائل، لذت خواهید برد.
این زبان بر اساس نظرسنجیهایی که هرساله توسط Stackoverflow در بین توسعهدهندگان انجام میشود، رشد و محبوبیت زیادی را کسب کرده ولی با این حال در ایران مهجور واقع شده که شاید دلیل آن، پیچیدگیهای زبان و کمبود منابع آموزشی فارسی برای این زبان باشد. حدس من این است در سالهای آتی همانند بسیاری از تکنولوژیهای دیگر توسعهدهندگان ایرانی نیز به این زبان مهاجرت خواهند کرد و در پیادهسازی برنامههای که متناسب با هدف این زبان هست، از آن استفاده خواهند کرد.