ذخیره لیست‌هایی از مقادیر با بردارها

اولین نوع مجموعه‌ای که به آن خواهیم پرداخت، Vec<T> یا همان بردار است. بردارها به شما اجازه می‌دهند که بیش از یک مقدار را در یک ساختار داده ذخیره کنید که تمامی مقادیر را در کنار یکدیگر در حافظه قرار می‌دهد. بردارها فقط می‌توانند مقادیر از یک نوع را ذخیره کنند. این ابزار زمانی مفید است که لیستی از آیتم‌ها مانند خطوط متنی در یک فایل یا قیمت آیتم‌ها در یک سبد خرید داشته باشید.

ایجاد یک بردار جدید

برای ایجاد یک بردار خالی جدید، از تابع Vec::new استفاده می‌کنیم، همانطور که در لیست ۸-۱ نشان داده شده است.

fn main() {
    let v: Vec<i32> = Vec::new();
}
Listing 8-1: ایجاد یک بردار جدید و خالی برای نگهداری مقادیر نوع i32

توجه داشته باشید که ما یک توضیح نوع اضافه کرده‌ایم. چون ما هیچ مقداری به این بردار اضافه نکرده‌ایم، Rust نمی‌داند چه نوع عناصری را قصد داریم ذخیره کنیم. این نکته مهمی است. بردارها با استفاده از جنریک‌ها پیاده‌سازی شده‌اند؛ در فصل ۱۰ خواهیم دید که چگونه می‌توان جنریک‌ها را در انواع خودتان استفاده کرد. در حال حاضر بدانید که نوع Vec<T> ارائه شده توسط کتابخانه استاندارد می‌تواند هر نوعی را نگهداری کند. وقتی یک بردار برای نگهداری نوع خاصی ایجاد می‌کنیم، می‌توانیم نوع موردنظر را داخل براکت‌های زاویه‌ای مشخص کنیم. در لیست ۸-۱، ما به Rust اعلام کرده‌ایم که بردار Vec<T> در v عناصر نوع i32 را نگهداری خواهد کرد.

بیشتر اوقات، شما یک Vec<T> با مقادیر اولیه ایجاد خواهید کرد و Rust نوع مقادیر را از روی آنها استنتاج خواهد کرد، بنابراین به ندرت نیاز به توضیح نوع خواهید داشت. Rust به راحتی ماکروی vec! را فراهم می‌کند که یک بردار جدید ایجاد کرده و مقادیر مورد نظر شما را در آن قرار می‌دهد. لیست ۸-۲ یک بردار جدید Vec<i32> را ایجاد می‌کند که مقادیر 1، 2 و 3 را نگهداری می‌کند. نوع عدد صحیح i32 است چون این نوع پیش‌فرض برای اعداد صحیح است، همانطور که در بخش “انواع داده‌ها” فصل ۳ بحث کردیم.

fn main() {
    let v = vec![1, 2, 3];
}
Listing 8-2: ایجاد یک بردار جدید شامل مقادیر

چون مقادیر اولیه i32 داده‌ایم، Rust می‌تواند استنتاج کند که نوع v Vec<i32> است و نیازی به توضیح نوع نیست. حالا به نحوه به‌روزرسانی یک بردار خواهیم پرداخت.

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

برای ایجاد یک بردار و سپس اضافه کردن عناصر به آن، می‌توانیم از متد push استفاده کنیم، همانطور که در لیست ۸-۳ نشان داده شده است.

fn main() {
    let mut v = Vec::new();

    v.push(5);
    v.push(6);
    v.push(7);
    v.push(8);
}
Listing 8-3: استفاده از متد push برای اضافه کردن مقادیر به یک بردار

همانطور که با هر متغیری دیگر انجام می‌دهیم، اگر بخواهیم بتوانیم مقدار آن را تغییر دهیم، باید آن را با استفاده از کلیدواژه mut قابل تغییر کنیم، همانطور که در فصل ۳ بحث شد. اعدادی که در داخل بردار قرار می‌دهیم همه از نوع i32 هستند و Rust این نوع را از داده‌ها استنتاج می‌کند، بنابراین نیازی به توضیح نوع Vec<i32> نیست.

خواندن عناصر بردار

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

لیست ۸-۴ هر دو روش دسترسی به یک مقدار در بردار، با استفاده از سینتکس اندیس (index)و متد get را نشان می‌دهد.

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    let third: &i32 = &v[2];
    println!("The third element is {third}");

    let third: Option<&i32> = v.get(2);
    match third {
        Some(third) => println!("The third element is {third}"),
        None => println!("There is no third element."),
    }
}
Listing 8-4: استفاده از سینتکس اندیس (index)و متد get برای دسترسی به یک آیتم در بردار

به چند جزئیات اینجا توجه کنید. ما از مقدار اندیس (index)2 برای دسترسی به عنصر سوم استفاده می‌کنیم زیرا بردارها با شماره از صفر اندیس‌گذاری می‌شوند. استفاده از & و [] یک مرجع به عنصر در مقدار اندیس (index)را به ما می‌دهد. وقتی از متد get با اندیسی که به عنوان آرگومان داده می‌شود استفاده می‌کنیم، یک Option<&T> دریافت می‌کنیم که می‌توانیم با match از آن استفاده کنیم.

Rust این دو روش ارجاع به یک عنصر را ارائه می‌دهد تا بتوانید انتخاب کنید که برنامه شما چگونه رفتار کند وقتی تلاش می‌کنید از یک مقدار اندیس (index)خارج از محدوده عناصر موجود استفاده کنید. به عنوان یک مثال، بیایید ببینیم چه اتفاقی می‌افتد وقتی یک بردار با پنج عنصر داشته باشیم و سپس تلاش کنیم به یک عنصر در اندیس (index)۱۰۰ با هر دو تکنیک دسترسی پیدا کنیم، همانطور که در لیست ۸-۵ نشان داده شده است.

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    let does_not_exist = &v[100];
    let does_not_exist = v.get(100);
}
Listing 8-5: تلاش برای دسترسی به عنصر در اندیس (index)۱۰۰ در یک بردار شامل پنج عنصر

وقتی این کد را اجرا می‌کنیم، روش اول [] باعث می‌شود برنامه متوقف شود زیرا به یک عنصر غیرموجود اشاره می‌کند. این روش زمانی بهترین استفاده را دارد که بخواهید برنامه‌تان در صورت تلاش برای دسترسی به عنصری خارج از انتهای بردار، متوقف شود.

وقتی متد get یک اندیس (index)خارج از بردار دریافت می‌کند، مقدار None را بدون متوقف کردن برنامه بازمی‌گرداند. شما از این روش استفاده می‌کنید اگر دسترسی به عنصری خارج از محدوده بردار ممکن است گاه‌به‌گاه در شرایط عادی رخ دهد. کد شما سپس منطق لازم برای مدیریت داشتن Some(&element) یا None را خواهد داشت، همانطور که در فصل ۶ بحث شد. برای مثال، اندیس (index)ممکن است از یک عدد ورودی توسط کاربر بیاید. اگر کاربر تصادفاً عددی وارد کند که بیش از حد بزرگ باشد و برنامه مقدار None دریافت کند، شما می‌توانید به کاربر اطلاع دهید که چند آیتم در بردار موجود است و به او فرصت دیگری برای وارد کردن یک مقدار معتبر بدهید. این راهکار برای کاربر پسندتر است تا این که برنامه به دلیل یک اشتباه تایپی متوقف شود!

وقتی برنامه یک مرجع معتبر دارد، بررسی‌کننده قرض قوانین مالکیت و قرض‌گیری (که در فصل ۴ پوشش داده شد) را اعمال می‌کند تا اطمینان حاصل کند که این مرجع و هر مرجع دیگری به محتوای بردار معتبر باقی می‌مانند. به یاد بیاورید که قانون بیان می‌کند نمی‌توانید مرجع‌های قابل تغییر و غیرقابل تغییر را در یک حوزه داشته باشید. این قانون در لیست ۸-۶ اعمال می‌شود، جایی که یک مرجع غیرقابل تغییر به اولین عنصر در یک بردار نگه داشته شده است و سعی داریم یک عنصر به انتها اضافه کنیم. این برنامه زمانی کار نخواهد کرد اگر همچنین بخواهیم بعداً در تابع به آن عنصر ارجاع دهیم.

fn main() {
    let mut v = vec![1, 2, 3, 4, 5];

    let first = &v[0];

    v.push(6);

    println!("The first element is: {first}");
}
Listing 8-6: تلاش برای اضافه کردن یک عنصر به یک بردار در حالی که یک مرجع به یک آیتم نگه داشته شده است

کامپایل کردن این کد به این خطا منجر می‌شود:

$ cargo run
   Compiling collections v0.1.0 (file:///projects/collections)
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
 --> src/main.rs:6:5
  |
4 |     let first = &v[0];
  |                  - immutable borrow occurs here
5 |
6 |     v.push(6);
  |     ^^^^^^^^^ mutable borrow occurs here
7 |
8 |     println!("The first element is: {first}");
  |                                     ------- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `collections` (bin "collections") due to 1 previous error

کد در لیست ۸-۶ ممکن است به نظر بیاید که باید کار کند: چرا یک مرجع به اولین عنصر باید به تغییرات انتهای بردار اهمیت دهد؟ این خطا به نحوه کار بردارها مربوط است: چون بردارها مقادیر را در کنار یکدیگر در حافظه قرار می‌دهند، اضافه کردن یک عنصر جدید به انتهای بردار ممکن است نیازمند اختصاص حافظه جدید و کپی کردن عناصر قدیمی به مکان جدید باشد، اگر فضای کافی برای قرار دادن همه عناصر در کنار یکدیگر در محل کنونی بردار وجود نداشته باشد. در این حالت، مرجع به اولین عنصر به حافظه‌ای اشاره می‌کند که آزاد شده است. قوانین قرض‌گیری از به وجود آمدن این شرایط در برنامه‌ها جلوگیری می‌کنند.

نکته: برای اطلاعات بیشتر درباره جزئیات پیاده‌سازی نوع Vec<T>، به “The Rustonomicon” مراجعه کنید.

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

برای دسترسی به هر عنصر در یک بردار به ترتیب، می‌توانیم به جای استفاده از اندیس‌ها برای دسترسی به یک عنصر در هر بار، بر روی تمامی عناصر پیمایش کنیم. لیست ۸-۷ نشان می‌دهد چگونه می‌توان از یک حلقه for برای گرفتن مرجع‌های غیرقابل تغییر به هر عنصر در یک بردار از مقادیر i32 استفاده کرد و آنها را چاپ کرد.

fn main() {
    let v = vec![100, 32, 57];
    for i in &v {
        println!("{i}");
    }
}
Listing 8-7: چاپ هر عنصر در یک بردار با پیمایش بر روی عناصر با استفاده از یک حلقه for

همچنین می‌توانیم بر روی مرجع‌های قابل تغییر به هر عنصر در یک بردار قابل تغییر پیمایش کنیم تا تغییراتی روی تمام عناصر اعمال کنیم. حلقه for در لیست ۸-۸ مقدار 50 را به هر عنصر اضافه می‌کند.

fn main() {
    let mut v = vec![100, 32, 57];
    for i in &mut v {
        *i += 50;
    }
}
Listing 8-8: پیمایش بر روی مرجع‌های قابل تغییر به عناصر در یک بردار

برای تغییر مقدار مرجع قابل تغییر، باید از عملگر * (dereference) استفاده کنیم تا به مقدار موجود در i دسترسی پیدا کنیم، سپس می‌توانیم از عملگر += استفاده کنیم. درباره عملگر dereference در بخش “دنبال کردن اشاره‌گر (Pointer) به مقدار با عملگر dereference” در فصل ۱۵ بیشتر صحبت خواهیم کرد.

پیمایش بر روی یک بردار، چه به صورت غیرقابل تغییر و چه به صورت قابل تغییر، امن است زیرا از قوانین بررسی‌کننده قرض پیروی می‌کند. اگر بخواهیم در بدنه حلقه‌های for در لیست ۸-۷ و لیست ۸-۸ آیتم‌ها را درج یا حذف کنیم، با خطای کامپایل مشابهی با کدی که در لیست ۸-۶ دیدیم روبرو خواهیم شد. مرجع به برداری که حلقه for نگه می‌دارد از تغییر همزمان کل بردار جلوگیری می‌کند.

استفاده از Enum برای ذخیره انواع مختلف

بردارها فقط می‌توانند مقادیر از یک نوع را ذخیره کنند. این موضوع ممکن است گاهی ناخوشایند باشد؛ مطمئناً موارد استفاده‌ای وجود دارند که نیاز به ذخیره یک لیست از آیتم‌ها با انواع مختلف دارید. خوشبختانه، متغیرهای یک enum تحت یک نوع enum تعریف شده‌اند، بنابراین وقتی نیاز به یک نوع برای نمایش عناصر از انواع مختلف دارید، می‌توانید یک enum تعریف کرده و از آن استفاده کنید!

برای مثال، فرض کنید می‌خواهیم مقادیر یک ردیف از یک صفحه گسترده را که برخی از ستون‌های آن شامل اعداد صحیح، برخی شامل اعداد اعشاری و برخی شامل رشته‌ها می‌باشند، دریافت کنیم. می‌توانیم یک enum تعریف کنیم که متغیرهای آن انواع مختلف مقادیر را نگهداری کنند، و تمام متغیرهای enum به عنوان یک نوع مشابه (یعنی نوع enum) در نظر گرفته می‌شوند. سپس می‌توانیم یک بردار ایجاد کنیم که این enum را نگهداری کند و در نتیجه انواع مختلف را ذخیره کند. این موضوع در لیست ۸-۹ نمایش داده شده است.

fn main() {
    enum SpreadsheetCell {
        Int(i32),
        Float(f64),
        Text(String),
    }

    let row = vec![
        SpreadsheetCell::Int(3),
        SpreadsheetCell::Text(String::from("blue")),
        SpreadsheetCell::Float(10.12),
    ];
}
Listing 8-9: تعریف یک enum برای ذخیره انواع مختلف در یک بردار

Rust باید بداند چه انواعی در بردار خواهند بود تا بتواند در زمان کامپایل دقیقاً مشخص کند چه مقدار حافظه در heap برای ذخیره هر عنصر نیاز است. همچنین باید به طور صریح مشخص کنیم که چه انواعی در این بردار مجاز هستند. اگر Rust اجازه می‌داد که بردار هر نوعی را نگهداری کند، احتمال داشت که یک یا چند نوع باعث ایجاد خطا در عملیات انجام شده روی عناصر بردار شوند. استفاده از یک enum به علاوه یک عبارت match به این معنی است که Rust در زمان کامپایل اطمینان حاصل خواهد کرد که تمام حالت‌های ممکن مدیریت شده‌اند، همانطور که در فصل ۶ بحث شد.

اگر مجموعه جامعی از انواعی که برنامه در زمان اجرا دریافت می‌کند و باید در بردار ذخیره شود را نمی‌دانید، تکنیک enum کار نخواهد کرد. به جای آن، می‌توانید از یک شیء ویژگی (trait object) استفاده کنید که در فصل ۱۸ مورد بررسی قرار خواهد گرفت.

اکنون که برخی از رایج‌ترین روش‌های استفاده از بردارها را بحث کردیم، مطمئن شوید که مستندات API را برای تمام متدهای مفیدی که کتابخانه استاندارد روی Vec<T> تعریف کرده است مرور کنید. برای مثال، علاوه بر push، متد pop عنصر آخر را حذف کرده و بازمی‌گرداند.

حذف یک بردار، عناصر آن را نیز حذف می‌کند

مانند هر struct دیگری، یک بردار وقتی از محدوده خارج می‌شود آزاد می‌شود، همانطور که در لیست ۸-۱۰ نشان داده شده است.

fn main() {
    {
        let v = vec![1, 2, 3, 4];

        // do stuff with v
    } // <- v goes out of scope and is freed here
}
Listing 8-10: نمایش زمان حذف بردار و عناصر آن

وقتی بردار حذف می‌شود، تمام محتوای آن نیز حذف می‌شوند، به این معنی که اعداد صحیحی که نگهداری می‌کند تمیزکاری می‌شوند. بررسی‌کننده قرض اطمینان حاصل می‌کند که هر مرجع به محتوای یک بردار فقط تا زمانی که خود بردار معتبر است استفاده شود.

حال به نوع مجموعه بعدی می‌پردازیم: String!