انواع داده‌ها

هر مقدار در زبان راست نوع خاصی از داده را دارد که به راست می‌گوید چه نوع داده‌ای مشخص شده است تا بداند چگونه با آن داده کار کند. ما به دو زیرمجموعه از انواع داده نگاه خواهیم کرد: انواع ساده و ترکیبی.

به خاطر داشته باشید که راست یک زبان ایستا-تایپ است، به این معنا که باید نوع تمام متغیرها در زمان کامپایل مشخص باشد. کامپایلر معمولاً می‌تواند بر اساس مقدار و نحوه استفاده از آن، نوع مورد نظر ما را حدس بزند. در مواردی که انواع متعددی ممکن است، مانند زمانی که یک String را به نوع عددی تبدیل کردیم در بخش “مقایسه حدس با عدد مخفی” در فصل 2، باید یک تعریف نوع اضافه کنیم، مانند این:

#![allow(unused)]
fn main() {
let guess: u32 = "42".parse().expect("Not a number!");
}

اگر تعریف نوع : u32 را که در کد بالا آمده است اضافه نکنیم، راست خطای زیر را نمایش می‌دهد، که به معنای این است که کامپایلر به اطلاعات بیشتری از ما نیاز دارد تا بداند کدام نوع را می‌خواهیم استفاده کنیم:

$ cargo build
   Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0284]: type annotations needed
 --> src/main.rs:2:9
  |
2 |     let guess = "42".parse().expect("Not a number!");
  |         ^^^^^        ----- type must be known at this point
  |
  = note: cannot satisfy `<_ as FromStr>::Err == _`
help: consider giving `guess` an explicit type
  |
2 |     let guess: /* Type */ = "42".parse().expect("Not a number!");
  |              ++++++++++++

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

شما تعریف‌های نوع مختلفی برای انواع داده‌های دیگر خواهید دید.

انواع ساده

یک نوع ساده نمایانگر یک مقدار واحد است. راست چهار نوع ساده اصلی دارد: اعداد صحیح، اعداد اعشاری، بولین‌ها و کاراکترها. ممکن است این‌ها را از زبان‌های برنامه‌نویسی دیگر بشناسید. بیایید ببینیم چگونه در راست کار می‌کنند.

انواع اعداد صحیح

یک عدد صحیح عددی بدون جزء اعشاری است. ما در فصل 2 از یک نوع عدد صحیح به نام u32 استفاده کردیم. این تعریف نوع نشان می‌دهد که مقدار مرتبط باید یک عدد صحیح بدون علامت (انواع اعداد صحیح با علامت با i به جای u شروع می‌شوند) باشد که 32 بیت فضا اشغال می‌کند. جدول 3-1 انواع اعداد صحیح ساخته شده در راست را نشان می‌دهد. ما می‌توانیم از هر یک از این حالت‌ها برای تعریف نوع یک مقدار عدد صحیح استفاده کنیم.

جدول 3-1: انواع اعداد صحیح در راست

طولبا علامتبدون علامت
8 بیتi8u8
16 بیتi16u16
32 بیتi32u32
64 بیتi64u64
128 بیتi128u128
معماریisizeusize

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

هر حالت با علامت می‌تواند اعداد را از -(2n - 1) تا 2n - 1 - 1 شامل شود، جایی که n تعداد بیت‌هایی است که آن حالت استفاده می‌کند. بنابراین یک i8 می‌تواند اعداد را از -(27) تا 27 - 1 ذخیره کند، که برابر است با -128 تا 127. حالت‌های بدون علامت می‌توانند اعداد را از 0 تا 2n - 1 ذخیره کنند، بنابراین یک u8 می‌تواند اعداد را از 0 تا 28 - 1 ذخیره کند، که برابر است با 0 تا 255.

علاوه بر این، نوع‌های isize و usize به معماری رایانه‌ای که برنامه شما روی آن اجرا می‌شود بستگی دارند، که در جدول به عنوان “معماری” مشخص شده است: 64 بیت اگر روی معماری 64 بیتی باشید و 32 بیت اگر روی معماری 32 بیتی باشید.

شما می‌توانید اعداد صحیح را به هر یک از اشکال نشان داده شده در جدول 3-2 بنویسید. توجه داشته باشید که عددهایی که می‌توانند به چندین نوع عددی تبدیل شوند، یک پسوند نوع دارند، مانند 57u8، برای تعیین نوع. اعداد همچنین می‌توانند از _ به عنوان جداکننده بصری برای خواناتر کردن استفاده کنند، مانند 1_000، که همان مقدار 1000 را دارد.

جدول 3-2: نمایش اعداد صحیح در راست

نوع اعدادمثال
دهدهی98_222
هگزادسیمال0xff
اکتال0o77
باینری0b1111_0000
بایت (فقط u8)b'A'

حال چگونه می‌دانید که از کدام نوع عدد صحیح استفاده کنید؟ اگر مطمئن نیستید، مقادیر پیش‌فرض راست معمولاً مکان خوبی برای شروع هستند: نوع‌های عدد صحیح پیش‌فرض به i32 تبدیل می‌شوند. وضعیت اصلی که در آن ممکن است از isize یا usize استفاده کنید زمانی است که می‌خواهید به یک نوع مجموعه اشاره کنید.

سرریز عدد صحیح

فرض کنید یک متغیر از نوع u8 دارید که می‌تواند مقادیر بین 0 و 255 را نگه دارد. اگر تلاش کنید مقدار متغیر را به عددی خارج از این بازه، مانند 256، تغییر دهید، سرریز عدد صحیح رخ خواهد داد که می‌تواند منجر به یکی از دو رفتار شود. وقتی برنامه خود را در حالت دیباگ کامپایل می‌کنید، راست شامل بررسی‌هایی برای سرریز عدد صحیح است که باعث می‌شود برنامه شما در زمان اجرا پانیک کند اگر این رفتار رخ دهد. راست از اصطلاح پانیک کردن زمانی استفاده می‌کند که برنامه با یک خطا خارج شود؛ ما در بخش “خطاهای غیرقابل بازیابی با panic! در فصل 9 به طور عمیق‌تر درباره پانیک‌ها بحث خواهیم کرد.

وقتی برنامه خود را در حالت انتشار با پرچم --release کامپایل می‌کنید، راست این بررسی‌ها را برای سرریز عدد صحیح شامل نمی‌شود. در عوض، اگر سرریز رخ دهد، راست از دو مکمل بسته‌بندی استفاده می‌کند. به طور خلاصه، مقادیر بزرگتر از حداکثر مقداری که نوع می‌تواند نگه دارد به “حداقل مقادیر” بازه نوع بسته‌بندی می‌شوند. در مورد یک u8، مقدار 256 به 0 تبدیل می‌شود، مقدار 257 به 1 و غیره. برنامه پانیک نخواهد کرد، اما متغیر مقدار متفاوتی نسبت به آنچه انتظار می‌رفت خواهد داشت. اعتماد به رفتار بسته‌بندی سرریز عدد صحیح یک خطا محسوب می‌شود.

برای مدیریت صریح امکان سرریز، می‌توانید از این خانواده‌های روش‌ها استفاده کنید که توسط کتابخانه استاندارد برای نوع‌های عددی اولیه ارائه شده‌اند:

  • بسته‌بندی در همه حالت‌ها با روش‌های wrapping_*، مانند wrapping_add.
  • بازگرداندن مقدار None اگر سرریز رخ دهد با روش‌های checked_*.
  • بازگرداندن مقدار و یک بولین که نشان‌دهنده سرریز است با روش‌های overflowing_*.
  • اشباع در مقادیر حداقل یا حداکثر مقدار نوع با روش‌های saturating_*.

انواع اعداد اعشاری

راست همچنین دو نوع اولیه برای اعداد اعشاری دارد، که اعدادی با نقطه اعشار هستند. نوع‌های اعشاری راست f32 و f64 هستند که به ترتیب 32 بیت و 64 بیت اندازه دارند. نوع پیش‌فرض f64 است زیرا روی CPUهای مدرن، سرعت آن تقریباً مشابه f32 است اما دقت بیشتری دارد. همه نوع‌های اعشاری علامت‌دار هستند.

در اینجا مثالی که اعداد اعشاری را در عمل نشان می‌دهد آورده شده است:

Filename: src/main.rs

fn main() {
    let x = 2.0; // f64

    let y: f32 = 3.0; // f32
}

اعداد اعشاری طبق استاندارد IEEE-754 نمایش داده می‌شوند.

عملیات عددی

راست از عملیات ریاضی پایه‌ای که برای تمام انواع عددی انتظار دارید پشتیبانی می‌کند: جمع، تفریق، ضرب، تقسیم و باقی‌مانده. تقسیم اعداد صحیح به نزدیک‌ترین عدد صحیح به سمت صفر گرد می‌شود. کد زیر نشان می‌دهد چگونه می‌توانید از هر عملیات عددی در یک عبارت let استفاده کنید:

Filename: src/main.rs

fn main() {
    // addition
    let sum = 5 + 10;

    // subtraction
    let difference = 95.5 - 4.3;

    // multiplication
    let product = 4 * 30;

    // division
    let quotient = 56.7 / 32.2;
    let truncated = -5 / 3; // Results in -1

    // remainder
    let remainder = 43 % 5;
}

هر عبارت در این دستورات از یک عملگر ریاضی استفاده می‌کند و به یک مقدار واحد ارزیابی می‌شود، که سپس به یک متغیر متصل می‌شود. ضمیمه ب شامل لیستی از تمام عملگرهایی است که راست فراهم می‌کند.

نوع بولین

مانند اکثر زبان‌های برنامه‌نویسی دیگر، نوع بولین در راست دو مقدار ممکن دارد: true و false. نوع بولین در راست یک بایت اندازه دارد. نوع بولین در راست با استفاده از bool مشخص می‌شود. برای مثال:

Filename: src/main.rs

fn main() {
    let t = true;

    let f: bool = false; // with explicit type annotation
}

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

نوع کاراکتر

نوع char در راست ابتدایی‌ترین نوع الفبایی زبان است. در اینجا برخی از مثال‌های اعلام مقادیر char آورده شده است:

Filename: src/main.rs

fn main() {
    let c = 'z';
    let z: char = 'ℤ'; // with explicit type annotation
    let heart_eyed_cat = '😻';
}

توجه داشته باشید که مقادیر char با استفاده از علامت نقل قول تکی مشخص می‌شوند، در حالی که مقادیر رشته‌ای از علامت نقل قول دوتایی استفاده می‌کنند. نوع char در راست چهار بایت اندازه دارد و نمایانگر یک مقدار اسکالر یونیکد است، به این معنی که می‌تواند خیلی بیشتر از فقط ASCII را نمایان کند. حروف با لهجه؛ حروف چینی، ژاپنی و کره‌ای؛ ایموجی؛ و فاصله‌های بدون عرض همگی مقادیر char معتبر در راست هستند. مقادیر اسکالر یونیکد در بازه U+0000 تا U+D7FF و U+E000 تا U+10FFFF قرار دارند. با این حال، “کاراکتر” واقعاً یک مفهوم در یونیکد نیست، بنابراین درک انسانی شما از آنچه یک “کاراکتر” است ممکن است با آنچه یک char در راست است همخوانی نداشته باشد. ما این موضوع را به تفصیل در بخش “ذخیره متن رمزگذاری‌شده UTF-8 با رشته‌ها” در فصل 8 بحث خواهیم کرد.

انواع ترکیبی

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

نوع تاپل

تاپل یک روش کلی برای گروه‌بندی چند مقدار با انواع مختلف در یک نوع ترکیبی است. تاپل‌ها طول ثابتی دارند: پس از اعلام، نمی‌توانند بزرگ‌تر یا کوچک‌تر شوند.

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

Filename: src/main.rs

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
}

متغیر tup به کل تاپل متصل می‌شود زیرا یک تاپل به عنوان یک عنصر ترکیبی واحد در نظر گرفته می‌شود. برای استخراج مقادیر جداگانه از یک تاپل، می‌توانیم از تطابق الگو برای تجزیه مقدار تاپل استفاده کنیم، مانند این:

Filename: src/main.rs

fn main() {
    let tup = (500, 6.4, 1);

    let (x, y, z) = tup;

    println!("The value of y is: {y}");
}

این برنامه ابتدا یک تاپل ایجاد کرده و آن را به متغیر tup متصل می‌کند. سپس از یک الگو با let برای گرفتن tup و تبدیل آن به سه متغیر جداگانه، x، y، و z استفاده می‌کند. این فرآیند تجزیه نامیده می‌شود زیرا تاپل واحد را به سه قسمت تقسیم می‌کند. در نهایت، برنامه مقدار y را که 6.4 است، چاپ می‌کند.

ما همچنین می‌توانیم یک عنصر از تاپل را مستقیماً با استفاده از یک نقطه (.) به دنبال شماره شاخص مقدار مورد نظر دسترسی داشته باشیم. برای مثال:

Filename: src/main.rs

fn main() {
    let x: (i32, f64, u8) = (500, 6.4, 1);

    let five_hundred = x.0;

    let six_point_four = x.1;

    let one = x.2;
}

این برنامه تاپل x را ایجاد کرده و سپس به هر عنصر تاپل با استفاده از شاخص‌های مربوطه آنها دسترسی پیدا می‌کند. همانند اکثر زبان‌های برنامه‌نویسی، اولین شاخص در یک تاپل 0 است.

تاپل بدون هیچ مقداری یک نام خاص دارد، واحد. این مقدار و نوع مربوط به آن هر دو با () نوشته می‌شوند و یک مقدار خالی یا یک نوع بازگشت خالی را نشان می‌دهند. عبارات به طور ضمنی مقدار واحد را بازمی‌گردانند اگر هیچ مقدار دیگری بازنگردانند.

نوع آرایه

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

ما مقادیر یک آرایه را به صورت یک لیست جدا شده با کاما در داخل کروشه می‌نویسیم:

Filename: src/main.rs

fn main() {
    let a = [1, 2, 3, 4, 5];
}

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

با این حال، آرایه‌ها زمانی مفیدتر هستند که بدانید تعداد عناصر نیاز به تغییر ندارد. برای مثال، اگر از نام‌های ماه در یک برنامه استفاده می‌کردید، احتمالاً از یک آرایه به جای یک وکتور استفاده می‌کردید زیرا می‌دانید همیشه ۱۲ عنصر خواهد داشت:

#![allow(unused)]
fn main() {
let months = ["January", "February", "March", "April", "May", "June", "July",
              "August", "September", "October", "November", "December"];
}

شما نوع یک آرایه را با استفاده از کروشه‌ها به همراه نوع هر عنصر، یک نقطه ویرگول، و سپس تعداد عناصر در آرایه می‌نویسید، مانند این:

#![allow(unused)]
fn main() {
let a: [i32; 5] = [1, 2, 3, 4, 5];
}

در اینجا، i32 نوع هر عنصر است. پس از نقطه ویرگول، عدد ۵ نشان می‌دهد که آرایه شامل پنج عنصر است.

شما همچنین می‌توانید یک آرایه را طوری مقداردهی اولیه کنید که هر عنصر مقدار یکسانی داشته باشد، با مشخص کردن مقدار اولیه، یک نقطه ویرگول، و سپس طول آرایه در کروشه‌ها، مانند این:

#![allow(unused)]
fn main() {
let a = [3; 5];
}

آرایه‌ای با نام a شامل ۵ عنصر خواهد بود که همه ابتدا مقدار ۳ دارند. این همان نوشتن let a = [3, 3, 3, 3, 3]; است، اما به شیوه‌ای مختصرتر.

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

یک آرایه یک بخش واحد از حافظه با اندازه‌ای مشخص و ثابت است که می‌تواند روی استک تخصیص داده شود. شما می‌توانید به عناصر یک آرایه با استفاده از ایندکس دسترسی پیدا کنید، مانند این:

Filename: src/main.rs

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

    let first = a[0];
    let second = a[1];
}

در این مثال، متغیری با نام first مقدار 1 را می‌گیرد زیرا این مقدار در ایندکس [0] در آرایه قرار دارد. متغیری با نام second مقدار 2 را از ایندکس [1] در آرایه می‌گیرد.

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

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

Filename: src/main.rs

use std::io;

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

    println!("Please enter an array index.");

    let mut index = String::new();

    io::stdin()
        .read_line(&mut index)
        .expect("Failed to read line");

    let index: usize = index
        .trim()
        .parse()
        .expect("Index entered was not a number");

    let element = a[index];

    println!("The value of the element at index {index} is: {element}");
}

این کد به درستی کامپایل می‌شود. اگر این کد را با استفاده از cargo run اجرا کنید و مقادیری مانند 0، 1، 2، 3 یا 4 را وارد کنید، برنامه مقدار متناظر در آن ایندکس از آرایه را چاپ می‌کند. اما اگر به جای آن عددی خارج از محدوده آرایه، مانند 10، وارد کنید، خروجی چیزی شبیه به این خواهد بود:

thread 'main' panicked at src/main.rs:19:19:
index out of bounds: the len is 5 but the index is 10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

این برنامه در نقطه استفاده از مقدار نامعتبر در عملیات ایندکس‌گذاری دچار خطای زمان اجرا شد. برنامه با یک پیام خطا خاتمه یافت و دستور نهایی println! را اجرا نکرد. زمانی که شما تلاش می‌کنید به یک عنصر با استفاده از ایندکس دسترسی پیدا کنید، راست بررسی می‌کند که ایندکسی که مشخص کرده‌اید کمتر از طول آرایه باشد. اگر ایندکس بزرگ‌تر یا برابر با طول باشد، راست متوقف می‌شود (پانیک می‌کند). این بررسی باید در زمان اجرا انجام شود، به‌ویژه در این مورد، زیرا کامپایلر نمی‌تواند بداند که کاربر چه مقداری را هنگام اجرای کد وارد خواهد کرد.

این یک مثال از اصول ایمنی حافظه راست در عمل است. در بسیاری از زبان‌های سطح پایین، این نوع بررسی انجام نمی‌شود و زمانی که شما یک ایندکس اشتباه ارائه می‌کنید، می‌توان به حافظه نامعتبر دسترسی پیدا کرد. راست شما را از این نوع خطا با متوقف کردن فوری برنامه به جای اجازه دسترسی به حافظه و ادامه برنامه محافظت می‌کند. فصل ۹ خطایابی در راست و نحوه نوشتن کد خوانا و ایمن که نه دچار پانیک شود و نه اجازه دسترسی نامعتبر به حافظه را بدهد، بیشتر بررسی می‌کند.