آموزش زبان برنامه‌نویسی Rust-قسمت3:معرفی آرایه, تاپل, کاراکتر و مقادیر بولی

این قسمت در تاریخ ۱۷ دی‌ماه ۱۳۹۷ به‌روزرسانی شده است و اطّلاعات بیشتری درمورد کار با آرایه‌ها در آن قرار گرفته است. اگر قبلاً این نوشته‌را خوانده‌اید، پیشنهاد می‌کنم دوباره هم این کار را بکنید.

در قسمت قبلی با انواع داده‌های عددی آشنا شدیم. حالا برای اینکه بتوانیم یک برنامه‌ی واقعی با زبان Rust بنویسیم, باید با 4 نوع داده دیگر در زبان Rust آشنا شویم.

چند جلسه که جلوتر برویم و بیشتر با زبان Rust آشنا شویم, با هم یک پکیج(crate) واقعی برای این زبان می‌نویسیم. البته در این جلسه و جلسات بعدی باید کدهای زیادی بزنیم تا هم با ویژگی‌های Rust آشنا شویم و هم از خواندن صرف مطالب حوصله‌مان سر نرود.

خب برویم سراغ اصل مطلب. هنوز ۲ نوع از انواع اسکالر باقی مانده اند. قبل از رفتن سراغ انواع ترکیبی, باید آن‌هارا یاد بگیریم.

نوع داده Boolean

مثل اکثر زبان‌های دنیا, و برخلاق زبان c, زبان Rust یک نوع داده‌ای خاص برای ذخیره‌سازی مقادیر بولی دارد. این نوع خاص تنها می‌تواند 2 مقدار داشته باشد: true برای حالت درست و false برای حالت غلط.

مثلاً در قطعه کد زیر, مقدار متغیّر a درست و مقدار متغیّر b غلط است:

let a: bool;    // اینجا متغیّر فقط تعریف شده است. ولی مقدار دهی نشده است.
let b = false;  // اینجا متغیّر هم تعریف شده است و هم مقدار دهی.
a = true;   // اینجا متغیّر مقدار دهی شده است

مقادیر بولی در عبارات شرطی کاربرد دارند. با عبارات شرطی در جلسات بعدی آشنا خواهیم شد.

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

نوع داده کاراکتر

کاراکتر پایه‌ای‌ترین نوع داده الفبایی در زبان Rust است.

در این زبان کاراکترها داخل علامت نقل قول تنها(single quotation) یا همان ' قرار می‌گیرند. برخلاف رشته‌ها که بین علامت‌های نقل قول دوتایی(double quotation) یا همان " قرار می‌گیرند.

به‌علاوه همانطور که در جلسات ابتدایی دیدیم, زبان Rust به صورت پیش‌فرض از utf-8 پشتیبانی می‌کند. بنابراین کاراکترها می‌توانند حروف انگلیسی, فارسی, چینی, ایموجوی و تقریباً هرچیزی که فکرش‌را بکنید باشند.

برنامه‌ی زیر کاراکترهای مختلف‌را درون متغیّرها ذخیره می‌کند و در پایان آن‌هارا نمایش می‌دهد:

let a = 'e';
let b = '1';
let c = '‌';    //  نیم‌فاصله
let d = 'پ';
let e = '?';
println!("{} {} {}{}{} {} ", a, b, d, c, d, e);

وقتی بعد از کامپایل این برنامه‌را اجرا کنید, با خروجی زیر روبه‌رو می‌شوید:

e 1 پ‌پ ? 

اگر قبلاً با c یا cpp کار کرده باشید و یاد رنج‌هایی که برای استفاده از این کاراکترها باید می‌کشیدید افتاده‌اید, بهتر است یک لیوان آب بنوشید و بغضتان‌را قورت بدهید. دیگر آن دوران سخت به‌سر آمده است.

داده‌های ترکیبی

خب داده‌های اسکالر تمام شدند. حالا می‌رسیم به داده‌های ترکیبی. فرق اصلی داده‌های ترکیبی با داده‌های اسکالر این است که داده‌های اسکالر تنها یک مقدار را ذخیره می‌کردند, امّا داده‌های ترکیبی چندین مقدار را کنار هم ذخیره می‌کنند.

ما در زبان Rust دو نوع داده ترکیبی پایه داریم: آرایه و تاپل. برویم و به هرکدام از این ۲ نوع نگاهی بیاندازیم.

آرایه

یکی از راه‌های ذخیره‌سازی چندین داده, استفاده از آرایه‌ها است. در زبان Rust اندازه‌ی هر آرایه مشخّص و غیرقابل تغییر است. یعنی وقتی شما آرایه‌ای با طول ۵ تعریف کرده‌اید, دیگر نمی‌توانید اندازه‌ی آن‌را عوض کنید و مثلاً ۶ داده را در آن ذخیره کنید.

اگر قرار است اندازه‌ی مجموعه‌ی داده‌هایتان در طول برنامه تغییر کند, بهتر است از انواعی مثل وکتور(که بعداً به سراغش خواهیم رفت) استفاده کنید.

همچنین تمامی عناصر ذخیره شده در آرایه باید از یک نوع باشند. یعنی شما نمی‌توانید در یک آرایه هم داده‌ای از نوع i16 داشته باشید و هم داده‌ای از نوع boolean.

الگوی کلّی تعریف یک آرایه به شکل(گیج کننده‌ی) زیر است:

let array_name: [Type; size] = [element0, element1, ...];

بعد از کلمه‌ی کلیدی let, اسم آرایه نوشته می‌شود(مثل هر متغیّر دیگری). بعد از علامت : داخل براکت باز و بسته باید نوع و اندازه آن را مشخص کنیم.

ابتدا نوع داده‌های موجود در این آرایه نوشته می‌شود. بعد از نوع, یک ; قرارداده می‌شود و پس از آن اندازه‌ی آرایه می‌آید.

مقادیر آرایه هم به ترتیب پس از علامت مساوی درون براکت‌های باز و بسته قرار می‌گیرند. باید بین هر عنصر و عنصر بعدی آن یک ویرگول هم قرار داد.

احتمالاً کمی گیج شده اید. پس بگذارید با یک مثال از این احساس گیجی خارج شویم.

فرض کنید می‌خواهیم یک آرایه به طول 3 تعریف کنیم که در آن عناصری از نوع i8 ذخیره خواهند شد. حاصل چیزی این شکلی خواهد شد:

let a: [i8; 3] = [0, 0, 0];

از آنجایی که زندگی با Rust خیلی آسان‌تر است, خیلی اوقات نیازی به تعریف نوع و اندازه‌ی آرایه هم ندارید. کافی است متغیّر را مقداردهی کنید تا هنگام کامپایل خود کامپایلر اندازه و نوع آرایه‌را تشخیص دهد.

مثلاً تکه کد زیر را ببینید:

let months = ["فروردین", "اردیبهشت", "خرداد",
                      "تیر", "مرداد", "شهریور",
                      "مهر", "آبان", "آذر",
                      "دی", "بهمن", "اسفند"];

در این جا خود کامپایلر متوجّه می‌شود که ما یک آرایه به طول 12 از داده‌های رشته‌ای داریم.

دسترسی به عنصرهای آرایه

هر عنصر آرایه یک شماره‌ی منحصر به فرد یا index دارد. ما می‌توانیم با index هر عنصر به آن دسترسی پیدا کنیم. یعنی مقدارش را دریافت کنیم یا آن‌را عوض کنیم.

ایندکس از ۰ شروع می‌شود. یعنی اوّلین عضو آرایه, ایندکس ۰ را می‌گیرد. دومی ایندکس ۱ و همینطور تا عضو آخر.

برای اینکه به یک عنصر آرایه دسترسی داشته باشیم کافی است بعد از نام آن, داخل براکت‌های باز و بسته شماره ایندکس عنصر مورد نظر را بنویسیم.

مثلاً فرض کنید می‌خواهیم ماه‌های اوّل, سوم و آخر را از آرایه‌ی قبلی پرینت کنیم:

fn main(){
    let months = ["فروردین", "اردیبهشت", "خرداد",
                          "تیر", "مرداد", "شهریور",
                          "مهر", "آبان", "آذر",
                          "دی", "بهمن", "اسفند"];

    let first_month = months[0];
    let last_month = months[11];
    let third_month = months[2];

    println!("{}", first_month);
    println!("{}", last_month);
    println!("{}", third_month);
}

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

فروردین
اسفند
خرداد

خب حالا فرض کنید می‌خواهیم اسم ماه هفتم‌را عوض کنیم. برای این کار باید برنامه‌ی زیر را اجرا کنیم:

fn main(){
    let months = ["فروردین", "اردیبهشت", "خرداد",
                          "تیر", "مرداد", "شهریور",
                          "مهر", "آبان", "آذر",
                          "دی", "بهمن", "اسفند"];

    println!("ماه هفتم قبل از عمل: {}", months[6]);
    months[6] = "محمّدرضا علی حسینی";
    println!("ماه هفتم بعد از عمل: {}", months[6]);

}

حالا برنامه‌را اجرا می‌کنیم:

cannot assign to indexed content `months[..]` of immutable binding
 --> src/main.rs:8:5
   |
   2 |     let months = ["فروردین", "اردیبهشت", "خرداد",
     |         ------ consider changing this to `mut months`
     ...
     8 |     months[6] = "محمّدرضا علی حسینی";
       |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot mutably borrow field of immutable binding

خب کار نکرد. :) دلیلش‌را قبلاً در قسمت اوّل دیده بودیم. متغیّرها در زبان Rust به‌صورت پیش‌فرض immutable هستند. پس ما نمی‌توانیم مقدار آن‌ها را عوض کنیم(راستش‌را بخواهید موقعی که این برنامه‌را می‌نوشتم خودم هم این موضوع‌را فراموش کرده بودم و از دیدن این ارور شوکه شدم).

حالا در برنامه یک تغییر کوچک می‌دهیم و آرایه‌را mutable می‌کنیم:

fn main(){
    let mut months = ["فروردین", "اردیبهشت", "خرداد",
                          "تیر", "مرداد", "شهریور",
                          "مهر", "آبان", "آذر",
                          "دی", "بهمن", "اسفند"];

    println!("ماه هفتم قبل از عمل: {}", months[6]);
    months[6] = "محمّدرضا علی حسینی";
    println!("ماه هفتم بعد از عمل: {}", months[6]);

}

این بار اگر برنامه‌را اجرا کنیم دیگر به ارور نمی‌خوریم و به خوبی و خوشی خروجی زیر را دریافت می‌کنیم:

ماه هفتم قبل از عمل: مهر
ماه هفتم بعد از عمل: محمّدرضا علی حسینی

حالا اگر مثل من به مسیرهای اشتباهی که یک برنامه می‌تواند طی کند خیلی علاقه‌مندید, شاید از خودتان می‌پرسید که اگر ایندکسی که به آرایه داده شده بزرگتر از مقدار مجاز باشد چه اتّفاقی می‌افتد.

قبل از اینکه جلوتر برویم بگذارید این اتّفاق‌را در زبان c ببینیم. برنامه‌ی زیر را در زبان c درنظر بگیرید:

#include <cstdio>

int main() {

   int myArray[3] = {0, 1, 2};
   printf("%d", myArray[3]);
    return 0;
}

مقادیر ایندکس مجاز برای آرایه‌ی myArray که طولش 3 است, ۰ تا 2 است. حالا ما هنگام پرینت خواسته‌ایم که مقدار موجود در ایندکس(۳) که اصلاً جزو این آرایه نیست خروجی داده شود.

خب چه اتّفاقی می‌افتد؟ اگر با زبان c کار کرده باشید می‌دانید که خروجی‌ای شبیه به خروجی پایین را دریافت می‌کنیم:

1245487872

با هربار اجرای برنامه مقداری که نمایش داده می‌شود متفاوت است و اصلاً ربطی به مقادیر آرایه‌ی ما ندارد. در یک کلام یعنی اینکه بدون هیچ هشداری, برنامه‌ی ما دارد اشتباه کار می‌کند.

حالا بیایید برنامه‌ای دقیقاً مشابه همین برنامه در زبان Rust بنویسیم:

fn main(){
    let my_array = [0, 1, 2];
    println!("{}", my_array[3]);
}

حالا اگر برنامه‌را کامپایل و سپس اجرا کنیم با این صحنه روبه‌رو خواهیم شد:

thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 3', src/main.rs:3:20
note: Run with `RUST_BACKTRACE=1` for a backtrace.

حالا اگر برنامه‌را با Backtrace که در خروجی قبلی پیشنهاد شده است اجرا کنیم, با متنی طولانی از شیوه‌ی panic مذکور روبه‌رو می‌شویم:

thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 3', src/main.rs:3:20 stack backtrace:
   0: std::sys::unix::backtrace::tracing::imp::unwind_backtrace
                at libstd/sys/unix/backtrace/tracing/gcc_s.rs:49
                   1: std::sys_common::backtrace::print
                                at libstd/sys_common/backtrace.rs:71
                                             at libstd/sys_common/backtrace.rs:59
                                                2: std::panicking::default_hook::{{closure}}
                                                             at libstd/panicking.rs:211
                                                                3: std::panicking::default_hook
                                                                             at libstd/panicking.rs:227
                                                                                4: std::panicking::rust_panic_with_hook
                                                                                             at libstd/panicking.rs:463
                                                                                                5: std::panicking::begin_panic_fmt
                                                                                                             at libstd/panicking.rs:350
                                                                                                                6: rust_begin_unwind
                                                                                                                             at 
                                                                                                                             ibstd/panicking.rs:328
                                                                                                                                7: 
                                                                                                                                بقیه‌ی متن‌را چون طولانی بود حذف کردم. خودتان با اجرای برنامه امتحان کنید

چیزی که دیدید جلوه‌ای از زیبایی‌های Rust بود! این زبان قول داده که ایمنی حافظه‌ی شمارا تأمین کند. به همین دلیل برخلاف c نمی‌گذارد که به بخش‌های دیگر حافظه دسترسی داشته باشید.

روشی کوتاه برای ساخت آرایه‌ای با عناصر یکسان

حالا خیلی وقت‌ها، مثلاً موقع initialize کردن یک آرایه، ما می‌خواهیم که تمامی عناصر یک آرایه مقداری برابر داشته باشند.

در Rust می‌توانیم به جای اینکه یک مقدار را به اندازه‌ی طول آرایه تکرارکنیم، از سینتک زیر استفاده‌کنیم که مثل یک shortcut برایمان همان کار را می‌کند:

[valueType;arrayLength]

مثلاً می‌خواهیم یک آرایه از نوع i32 بسازیم که ۷تا عنصر به مقدار ۰ دارد، برای این کار کد زیر را می‌نویسیم:

[0i32;7]

پرینت‌کردن یک آرایه

برای پرینت‌کردن یک آرایه، ما نمی‌توانیم از ("{}")!println استفاده‌کنیم. اگر این کاررا بکنیم با ارور مواجه می‌شویم. مثلاً کد زیر را ببینید:

fn main() {
    println!("my array is: {}", [10i8;10]);
}

اگر این برنامه‌را بخواهیم کامپایل‌کنیم، با ارور زیر مواجه می‌شویم:

error[E0277]: `[i8; 10]` doesn't implement `std::fmt::Display`
  --> src/main.rs:26:33
   |
26 |     println!("my array is: {}", [10i8;10]);
   |                                 ^^^^^^^^^ `[i8; 10]` cannot be formatted with the default formatter
   |
   = help: the trait `std::fmt::Display` is not implemented for `[i8; 10]`
   = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
   = note: required by `std::fmt::Display::fmt`

error: aborting due to previous error

درمورد traitها بعداً صحبت خواهیم‌کرد، امّا همانطوری که در متن ارور توضیح داده شده است، د به جای {} از {?:} یا {?#:} استفاده کنیم. بنابراین برای اینکه بتوانیم یک آرایه‌را پرینت‌کنیم، باید از کدی شبیه به کد زیر استفاده‌کنیم:

fn main() {
    println!("my array is: {:?}", [10i8;10]);
}

حالا اگر این برنامه‌را کامپایل و اجراکنیم، خروجی همان‌چیزی خواهد شد که انتظارش‌را داشتیم:

my array is: [10, 10, 10, 10, 10, 10, 10, 10, 10, 10]

تاپل

تاپل هم یک نوع داده ترکیبی دیگر است که از آرایه عمومی‌تر است, چون برخلاف آرایه شما می‌توانید عناصری از نوع‌های مختلف‌را در آن ذخیره کنید. امّا حواستان باشد که باز هم مثل آرایه, اندازه‌ی تاپل ثابت و غیر قابل تغییر است.

الگوی کلّی(و باز هم گیج‌کننده) تعریف یک تاپل به شکل زیر است:

let tuple_name: (Type0, Type1, ...) = (Value0, Value1, ...);

ما بعد از نوشتن نام تاپل و پس از علامت : داخل پرانتز به ترتیب نوع هر عنصر را می‌نویسیم. برای مقداردهی هم دوباره عناصر را به‌ترتیب درون پرانتز می‌نویسیم.

مثال‌های زیر را ببینید:

let tup0: (i32, char, bool, f64);   // تاپل و نوع عناصر را مشخص کردیم. ولی هنوز مقدار نداده‌ایم
let tup1 = (1, true, "سلام", 9.99); // نوع‌را مشخص نکردیم. چون با دادن مقادیر خود کامپایلر آن‌را می‌فهمد.
tup0 = (33, 'G', false, 9.87);  // اینجا مقادیر مربوط به تاپل اول را به آن نسبت می‌دهیم

دسترسی به عناصر تاپل

برای دسترسی به عنصرهای یک تاپل ۲ راه داریم.

اوّلین راه شبیه به همان کاری است که در آرایه انجام می‌دادیم. عنصرهای هر تاپل هم درست مثل عناصر آرایه ایندکس‌دهی می‌شوند. فقط اینجا به جای اینکه برای دسترسی به مقدار یک ایندکس از براکت استفاده کنیم, عدد ایندکس‌را بعد از علامت . می‌نویسیم.

مثلاً در برنامه‌ی زیر عنصر سوم تاپل‌را چاپ می‌کنیم:

fn main(){
    let tup = (1, true, "سلام", 9.99);
    println!("{}", tup.2);
}

بعد از کامپایل و اجرا خروجی زیر را می‌بینیم:

سلام

اگر به اندازه‌ی کافی تحت تأثیر این مثال قرار گرفتید, برویم سراغ روش دوم.

فرض کنید می‌خواهیم عنصرهای یک تاپل‌را در متغیّرهای جداگانه ذخیره کنیم. برای این کار از شیوه‌ی زیر استفاده می‌کنیم:

fn main(){
    let tup = (1, true, "سلام", 9.99);
    let (x, y, v, z) = tup;
    println!("x: {}, y: {}, v: {}, z: {}", x, y, v, z);
}

خب حالا بعد از اجرا این خروجی‌را می‌بینیم:

x: 1, y: true, v: سلام, z: 9.99

می‌توانید ایندکس اشتباه دادن به تاپل‌هارا هم امتحان کنید.

در جلسه‌ی بعدی با شرط‌ها آشنا می‌شویم تا به نوشتن برنامه‌های واقعی نزدیک‌تر شویم.

یادتان نرود که هرجایی که برایتان گنگ بود را می‌توانید در بخش نظرات بپرسید. به‌علاوه می‌توانید به من ایمیل هم بزنید.

پس تا جلسه‌ی بعدی بدرود.


این نوشته اولین بار در بلاگ شخصی‌ام منتشر شده بود. آنجا می‌توانید مطالب بیشتری را ببینید.

جلسه‌ی قبلی:

https://virgool.io/Software/%D8%A2%D9%85%D9%88%D8%B2%D8%B4-%D8%B2%D8%A8%D8%A7%D9%86-%D8%A8%D8%B1%D9%86%D8%A7%D9%85%D9%87%D9%86%D9%88%DB%8C%D8%B3%DB%8C-rust-%D9%82%D8%B3%D9%85%D8%AA-2-%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D8%AF%D8%A7%D8%AF%D9%87%D9%87%D8%A7%DB%8C-%D8%B9%D8%AF%D8%AF%DB%8C-%D9%88-%D8%B9%D9%85%D9%84%DA%AF%D8%B1%D9%87%D8%A7%DB%8C-%D8%A2%D9%86%D9%87%D8%A7-yhnwoffqcvgs


جلسه‌ی اول این سری آموزش‌ها:

https://virgool.io/Software/%D8%A2%D9%85%D9%88%D8%B2%D8%B4-%D8%B2%D8%A8%D8%A7%D9%86-%D8%A8%D8%B1%D9%86%D8%A7%D9%85%D9%87%D9%86%D9%88%DB%8C%D8%B3%DB%8C-rust-%D9%82%D8%B3%D9%85%D8%AA-0-%D9%85%D8%B9%D8%B1%D9%81%DB%8C-%D9%88-%D8%B4%D8%B1%D9%88%D8%B9-%D8%A8%D9%87-%DA%A9%D8%A7%D8%B1-nwqjyzch0lde