مقدمه

به فصل «کالکشن‌ها» خوش آمدید. کتابخانه استاندارد Rust مجموعه‌ای از ساختارهای داده بسیار مفید به نام «کالکشن» (collection) را فراهم می‌کند. برخلاف نوع‌های ترکیبی مانند آرایه و تاپل که داده‌ها را در stack ذخیره می‌کنند، داده‌های کالکشن‌ها در heap ذخیره می‌شوند. این به این معنی است که اندازه آنها نیازی نیست در زمان کامپایل مشخص باشد و می‌توانند در حین اجرای برنامه، رشد کرده یا کوچک شوند. در این درس به بررسی اولین و رایج‌ترین نوع کالکشن، یعنی وکتور یا Vec<T> می‌پردازیم.

ایجاد و به‌روزرسانی یک Vec

یک وکتور، که با Vec<T> نمایش داده می‌شود، به شما اجازه می‌دهد تا لیستی از مقادیر از یک نوع یکسان (T) را ذخیره کنید.

برای ساخت یک وکتور خالی، از تابع Vec::new() استفاده می‌کنیم. از آنجایی که وکتور خالی است، Rust نمی‌تواند نوع داده‌های آن را استنتاج کند، بنابراین باید نوع آن را به صراحت مشخص کنیم. یک روش رایج‌تر و ساده‌تر، استفاده از ماکروی vec! است که به ما اجازه می‌دهد یک وکتور را همراه با مقادیر اولیه ایجاد کنیم.

Copy Icon src/main.rs
fn main() {
    // Create a new, empty vector. Type annotation is required.
    let mut v: Vec<i32> = Vec::new();

    // Add elements to the vector using the push method
    v.push(5);
    v.push(6);
    v.push(7);

    // A more common way: create a vector with initial values using the vec! macro
    let v2 = vec![1, 2, 3];
}

همانطور که می‌بینید، برای افزودن عناصر جدید به وکتور با متد push()، باید وکتور را با کلمه کلیدی mut به صورت تغییرپذیر (mutable) تعریف کنیم.

خواندن عناصر یک Vec

دو روش اصلی برای دسترسی به عناصر یک وکتور وجود دارد که هر کدام مزایا و معایب خود را دارند.

ایندکس‌گذاری در مقابل متد get

روش اول استفاده از سینتکس استاندارد ایندکس‌گذاری است. روش دوم، استفاده از متد get() است که یک Option<&T> برمی‌گرداند.

Copy Icon src/main.rs
fn main() {
    let v = vec![10, 20, 30, 40, 50];

    // Access via indexing. This will panic if the index is out of bounds.
    let third: &i32 = &v[2];
    println!("The third element is {}", third);

    // Safer access via the get method, which returns an Option.
    match v.get(2) {
        Some(third) => println!("The third element is {}", third),
        None => println!("There is no third element."),
    }

    // let does_not_exist = &v[100]; // This line would cause the program to panic.
    let does_not_exist = v.get(100); // This returns None and does not panic.
}

تفاوت اصلی در مدیریت خطا است. اگر با ایندکس‌گذاری به یک عنصر خارج از محدوده دسترسی پیدا کنید، برنامه شما panic کرده و متوقف می‌شود. اما متد get() در این حالت مقدار None را برمی‌گرداند و به شما اجازه می‌دهد این وضعیت را به صورت کنترل‌شده مدیریت کنید. بنابراین، استفاده از get() و match روش امن‌تری محسوب می‌شود.

قوانین مالکیت و وام‌گیری در Vec

یک وکتور داده‌های خود را به صورت متوالی در heap ذخیره می‌کند. قوانین مالکیت و وام‌گیری Rust برای تضمین ایمنی حافظه، به شدت در مورد وکتورها نیز اعمال می‌شوند.

یک قانون مهم این است: شما نمی‌توانید در حالی که یک رفرنس تغییرناپذیر (immutable borrow) به یکی از عناصر وکتور دارید، وکتور را تغییر دهید (مثلاً با push()). کد زیر کامپایل نخواهد شد:

Copy Icon src/main.rs
fn main() {
    let mut v = vec![1, 2, 3, 4, 5];

    let first = &v[0]; // Immutable borrow to the first element

    v.push(6); // Mutable borrow to the whole vector -> ERROR!

    // println!("The first element is: {}", first);
}

شاید این محدودیت در نگاه اول عجیب به نظر برسد. چرا تغییر انتهای وکتور باید روی رفرنسی به ابتدای آن تأثیر بگذارد؟ پاسخ در نحوه کار وکتورها نهفته است. اگر با افزودن یک عنصر جدید، ظرفیت فعلی وکتور در heap کافی نباشد، وکتور ممکن است مجبور شود یک بلوک حافظه جدید و بزرگتر را تخصیص داده و تمام عناصر قدیمی را به مکان جدید کپی کند. در این صورت، حافظه قدیمی آزاد شده و رفرنس first به یک مکان نامعتبر و آزاد شده از حافظه اشاره خواهد کرد (یک dangling pointer). borrow checker در Rust با جلوگیری از کامپایل این کد، ما را از این خطای خطرناک حافظه در زمان کامپایل محافظت می‌کند.

پیمایش روی مقادیر یک Vec

برای پیمایش (iterate) روی عناصر یک وکتور، می‌توانیم از حلقه for استفاده کنیم. ما می‌توانیم به صورت تغییرناپذیر (فقط برای خواندن) یا تغییرپذیر (برای ویرایش) روی عناصر پیمایش کنیم.

Copy Icon 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 {
        // To change the value, we have to use the dereference operator (*)
        *i += 50;
    }
    println!("{:?}", v_mut); // [150, 82, 107]
}

در حلقه اول، با استفاده از &v یک رفرنس تغییرناپذیر به هر عنصر می‌گیریم. در حلقه دوم، با استفاده از &mut v_mut یک رفرنس تغییرپذیر به هر عنصر می‌گیریم. برای تغییر مقدار واقعی که رفرنس به آن اشاره می‌کند، باید از عملگر dereference یعنی `*` استفاده کنیم.

در این درس با وکتورها به عنوان یکی از پرکاربردترین و بنیادی‌ترین کالکشن‌ها در Rust آشنا شدیم. دیدیم که چگونه می‌توان آنها را ایجاد، به‌روزرسانی و پیمایش کرد و چگونه قوانین مالکیت Rust ایمنی حافظه را حتی در ساختارهای داده دینامیک تضمین می‌کنند. در درس بعدی، به سراغ یکی دیگر از کالکشن‌های بنیادی، یعنی String، خواهیم رفت و با جزئیات بیشتری نحوه کار با متن و رشته‌ها در Rust را بررسی خواهیم کرد.