ذخیره لیستهایی از مقادیر با بردارها
اولین نوع مجموعهای که به آن خواهیم پرداخت، Vec<T>
یا همان بردار است.
بردارها به شما اجازه میدهند که بیش از یک مقدار را در یک ساختار داده ذخیره کنید
که تمامی مقادیر را در کنار یکدیگر در حافظه قرار میدهد. بردارها فقط میتوانند
مقادیر از یک نوع را ذخیره کنند. این ابزار زمانی مفید است که لیستی از آیتمها
مانند خطوط متنی در یک فایل یا قیمت آیتمها در یک سبد خرید داشته باشید.
ایجاد یک بردار جدید
برای ایجاد یک بردار خالی جدید، از تابع Vec::new
استفاده میکنیم، همانطور که
در لیست ۸-۱ نشان داده شده است.
fn main() { let v: Vec<i32> = Vec::new(); }
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]; }
چون مقادیر اولیه 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); }
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."), } }
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); }
وقتی این کد را اجرا میکنیم، روش اول []
باعث میشود برنامه متوقف شود زیرا به یک
عنصر غیرموجود اشاره میکند. این روش زمانی بهترین استفاده را دارد که بخواهید برنامهتان
در صورت تلاش برای دسترسی به عنصری خارج از انتهای بردار، متوقف شود.
وقتی متد 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}");
}
کامپایل کردن این کد به این خطا منجر میشود:
$ 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}"); } }
for
همچنین میتوانیم بر روی مرجعهای قابل تغییر به هر عنصر در یک بردار قابل تغییر پیمایش کنیم
تا تغییراتی روی تمام عناصر اعمال کنیم. حلقه for
در لیست ۸-۸ مقدار 50
را به هر عنصر اضافه میکند.
fn main() { let mut v = vec![100, 32, 57]; for i in &mut v { *i += 50; } }
برای تغییر مقدار مرجع قابل تغییر، باید از عملگر * (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), ]; }
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 }
وقتی بردار حذف میشود، تمام محتوای آن نیز حذف میشوند، به این معنی که اعداد صحیحی که نگهداری میکند تمیزکاری میشوند. بررسیکننده قرض اطمینان حاصل میکند که هر مرجع به محتوای یک بردار فقط تا زمانی که خود بردار معتبر است استفاده شود.
حال به نوع مجموعه بعدی میپردازیم: String
!