مقدمه
به فصل «کالکشنها» خوش آمدید. کتابخانه استاندارد Rust مجموعهای از ساختارهای داده بسیار مفید به
نام «کالکشن» (collection) را فراهم میکند. برخلاف نوعهای ترکیبی مانند آرایه و تاپل که دادهها
را در stack ذخیره میکنند، دادههای کالکشنها در heap ذخیره میشوند. این به این
معنی است که اندازه آنها نیازی نیست در زمان کامپایل مشخص باشد و میتوانند در حین اجرای برنامه،
رشد کرده یا کوچک شوند. در این درس به بررسی اولین و رایجترین نوع کالکشن، یعنی وکتور یا Vec<T>
میپردازیم.
ایجاد و بهروزرسانی یک Vec
یک وکتور، که با Vec<T> نمایش داده میشود، به شما اجازه میدهد
تا لیستی از مقادیر از یک نوع
یکسان (T) را ذخیره کنید.
برای ساخت یک وکتور خالی، از تابع Vec::new() استفاده میکنیم. از
آنجایی که وکتور خالی است، Rust نمیتواند نوع دادههای آن را استنتاج کند، بنابراین باید نوع آن را
به صراحت مشخص کنیم. یک روش رایجتر و سادهتر، استفاده از ماکروی vec! است که به ما اجازه میدهد
یک وکتور را همراه با مقادیر اولیه ایجاد کنیم.
src/main.rs
fn main() {
let mut v: Vec<i32> = Vec::new();
v.push(5);
v.push(6);
v.push(7);
let v2 = vec![1, 2, 3];
}
همانطور که میبینید، برای افزودن عناصر جدید به وکتور با متد push()،
باید وکتور را با کلمه کلیدی mut به صورت تغییرپذیر (mutable) تعریف کنیم.
خواندن عناصر یک Vec
دو روش اصلی برای دسترسی به عناصر یک وکتور وجود دارد که هر کدام مزایا و معایب خود را دارند.
ایندکسگذاری در مقابل متد get
روش اول استفاده از سینتکس استاندارد ایندکسگذاری است. روش دوم، استفاده از متد get() است که یک Option<&T>
برمیگرداند.
src/main.rs
fn main() {
let v = vec![10, 20, 30, 40, 50];
let third: &i32 = &v[2];
println!("The third element is {}", third);
match v.get(2) {
Some(third) => println!("The third element is {}", third),
None => println!("There is no third element."),
}
let does_not_exist = v.get(100);
}
تفاوت اصلی در مدیریت خطا است. اگر با ایندکسگذاری به یک عنصر خارج از محدوده دسترسی پیدا کنید،
برنامه شما panic کرده و متوقف میشود. اما متد get() در این
حالت
مقدار None را برمیگرداند و به شما اجازه میدهد این وضعیت را به صورت کنترلشده مدیریت
کنید.
بنابراین، استفاده از get() و match روش امنتری محسوب میشود.
قوانین مالکیت و وامگیری در Vec
یک وکتور دادههای خود را به صورت متوالی در heap ذخیره میکند. قوانین مالکیت و وامگیری
Rust برای تضمین ایمنی حافظه، به شدت در مورد وکتورها نیز اعمال میشوند.
یک قانون مهم این است: شما نمیتوانید در حالی که یک رفرنس تغییرناپذیر (immutable borrow) به یکی
از عناصر وکتور دارید، وکتور را تغییر دهید (مثلاً با push()). کد زیر
کامپایل نخواهد شد:
src/main.rs
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
}
شاید این محدودیت در نگاه اول عجیب به نظر برسد. چرا تغییر انتهای وکتور باید روی رفرنسی به ابتدای
آن تأثیر بگذارد؟ پاسخ در نحوه کار وکتورها نهفته است. اگر با افزودن یک عنصر جدید، ظرفیت فعلی
وکتور در heap کافی نباشد، وکتور ممکن است مجبور شود یک بلوک حافظه جدید و بزرگتر را تخصیص
داده و تمام عناصر قدیمی را به مکان جدید کپی کند. در این صورت، حافظه قدیمی آزاد شده و رفرنس
first به یک مکان نامعتبر و آزاد شده از حافظه اشاره خواهد کرد (یک dangling pointer).
borrow
checker در Rust با جلوگیری از کامپایل این کد، ما را از این خطای خطرناک حافظه در زمان کامپایل
محافظت میکند.
پیمایش روی مقادیر یک Vec
برای پیمایش (iterate) روی عناصر یک وکتور، میتوانیم از حلقه for استفاده کنیم. ما
میتوانیم به صورت تغییرناپذیر (فقط برای خواندن) یا تغییرپذیر (برای ویرایش) روی عناصر پیمایش
کنیم.
src/main.rs
fn main() {
let v = vec![100, 32, 57];
for i in &v {
println!("{}", i);
}
let mut v_mut = vec![100, 32, 57];
for i in &mut v_mut {
*i += 50;
}
println!("{:?}", v_mut);
}
در حلقه اول، با استفاده از &v یک رفرنس تغییرناپذیر به هر عنصر
میگیریم. در حلقه دوم، با
استفاده از &mut v_mut یک رفرنس تغییرپذیر به هر عنصر میگیریم. برای
تغییر مقدار واقعی که رفرنس
به آن اشاره میکند، باید از عملگر dereference یعنی `*` استفاده کنیم.
در این درس با وکتورها به عنوان یکی از پرکاربردترین و بنیادیترین کالکشنها در Rust آشنا شدیم.
دیدیم که چگونه میتوان آنها را ایجاد، بهروزرسانی و پیمایش کرد و چگونه قوانین مالکیت Rust ایمنی
حافظه را حتی در ساختارهای داده دینامیک تضمین میکنند. در درس بعدی، به سراغ یکی دیگر از
کالکشنهای بنیادی، یعنی String، خواهیم رفت و با جزئیات بیشتری نحوه کار با متن و رشتهها
در
Rust را بررسی خواهیم کرد.