انواع دادهها
هر مقدار در زبان راست نوع خاصی از داده را دارد که به راست میگوید چه نوع دادهای مشخص شده است تا بداند چگونه با آن داده کار کند. ما به دو زیرمجموعه از انواع داده نگاه خواهیم کرد: انواع ساده و ترکیبی.
به خاطر داشته باشید که راست یک زبان ایستا-تایپ است، به این معنا که باید نوع تمام متغیرها در زمان کامپایل مشخص باشد. کامپایلر معمولاً میتواند بر اساس مقدار و نحوه استفاده از آن، نوع مورد نظر ما را حدس بزند. در مواردی که انواع متعددی ممکن است، مانند زمانی که یک 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: انواع اعداد صحیح در راست
طول | علامتدار | بدونعلامت |
---|---|---|
۸-بیتی | i8 | u8 |
۱۶-بیتی | i16 | u16 |
۳۲-بیتی | i32 | u32 |
۶۴-بیتی | i64 | u64 |
۱۲۸-بیتی | i128 | u128 |
وابسته به معماری | isize | usize |
هر حالت میتواند یا با علامت یا بدون علامت باشد و اندازه صریحی دارد. با علامت و بدون علامت به این اشاره دارند که آیا ممکن است عدد منفی باشد یا خیر؛ به عبارت دیگر، آیا عدد نیاز به علامت دارد (با علامت) یا اینکه فقط مثبت خواهد بود و بنابراین میتوان آن را بدون علامت نشان داد (بدون علامت). این شبیه به نوشتن اعداد روی کاغذ است: وقتی علامت مهم باشد، عدد با علامت مثبت یا منفی نشان داده میشود؛ اما وقتی فرض مثبت بودن عدد ایمن باشد، بدون علامت نشان داده میشود. اعداد با علامت با استفاده از نمایش دو مکمل ذخیره میشوند.
هر نوع عدد صحیح علامتدار میتواند مقادیری از
−(2n − 1) تا 2n − 1 − 1 را
در بر بگیرد، که در آن n تعداد بیتهای استفادهشده
توسط آن نوع است. بنابراین، یک i8
میتواند
مقادیر بین −(27) تا 27 − 1
را نگه دارد، یعنی از −۱۲۸ تا ۱۲۷.
انواع بدونعلامت (unsigned) میتوانند مقادیر
بین ۰ تا 2n − 1 را نگهداری کنند؛
مثلاً یک u8
میتواند مقادیری از ۰ تا
28 − 1، یعنی از ۰ تا ۲۵۵ را ذخیره کند.
علاوه بر این، نوعهای isize
و usize
به
معماری سیستمی بستگی دارند که برنامه روی آن
اجرا میشود: اگر معماری ۶۴ بیتی باشد، این نوعها
۶۴ بیتی هستند، و اگر ۳۲ بیتی باشد، ۳۲ بیتی خواهند بود.
شما میتوانید اعداد صحیح را به هر یک از اشکال نشان داده شده در جدول 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
، مقدار را wrap میکند. - در صورت بروز overflow، مقدار
None
را با متدهایchecked_*
بازمیگرداند. - مقدار و یک مقدار Boolean که نشان میدهد overflow رخ داده یا نه، با متدهای
overflowing_*
بازمیگردد. - در مقدار حداقل یا حداکثر نوع متوقف میشود (saturate) با استفاده از متدهای
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 = '😻'; }
توجه داشته باشید که literals نوع char
با
نقلقولهای تکی مشخص میشوند، در حالی که literals
رشتهای (string) از نقلقولهای دوتایی استفاده میکنند.
نوع char
در Rust اندازهای برابر با چهار بایت
دارد و نمایانگر یک مقدار اسکالر یونیکد است، به این
معنا که میتواند بسیار بیشتر از فقط کاراکترهای
ASCII را نمایش دهد. حروف دارای اعراب، کاراکترهای
چینی، ژاپنی و کرهای، ایموجیها و فضاهای بدون عرض
همگی مقادیر معتبر char
در Rust هستند. مقدارهای
اسکالر یونیکد در بازهی U+0000
تا U+D7FF
و
U+E000
تا U+10FFFF
شامل میشوند. با این حال،
مفهوم “کاراکتر” در یونیکد واقعاً وجود ندارد،
بنابراین تصور انسانی شما از “کاراکتر” ممکن است با
آنچه char
در Rust است تفاوت داشته باشد. این موضوع
را در بخش «ذخیره متن کدگذاریشده UTF-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]; }
آرایهها زمانی کاربردی هستند که بخواهید دادههایتان روی stack تخصیص یابند، مشابه سایر نوعهایی که تاکنون دیدیم، نه روی heap (که در فصل ۴ بیشتر دربارهی stack و heap صحبت خواهیم کرد) یا زمانی که میخواهید همیشه تعداد ثابتی از عناصر داشته باشید. با این حال، آرایه به اندازهی نوع vector انعطافپذیر نیست. وکتور نوعی مجموعه مشابه است که توسط کتابخانه استاندارد ارائه شده و اجازه دارد اندازهاش تغییر کند، چون محتوای آن روی heap ذخیره میشود. اگر مطمئن نیستید که از آرایه استفاده کنید یا وکتور، احتمالاً بهتر است وکتور را انتخاب کنید. فصل ۸ بهطور دقیقتر دربارهی وکتورها بحث میکند.
با این حال، آرایهها زمانی مفیدتر هستند که بدانید تعداد عناصر نیاز به تغییر ندارد. برای مثال، اگر از نامهای ماه در یک برنامه استفاده میکردید، احتمالاً از یک آرایه به جای یک وکتور استفاده میکردید زیرا میدانید همیشه ۱۲ عنصر خواهد داشت:
#![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!
را اجرا نکرد. زمانی که شما تلاش میکنید به یک عنصر با استفاده از ایندکس دسترسی پیدا کنید، راست بررسی میکند که ایندکسی که مشخص کردهاید کمتر از طول آرایه باشد. اگر ایندکس بزرگتر یا برابر با طول باشد، راست متوقف میشود (پانیک میکند). این بررسی باید در زمان اجرا انجام شود، بهویژه در این مورد، زیرا کامپایلر نمیتواند بداند که کاربر چه مقداری را هنگام اجرای کد وارد خواهد کرد.
این یک مثال از اصول ایمنی حافظه راست در عمل است. در بسیاری از زبانهای سطح پایین، این نوع بررسی انجام نمیشود و زمانی که شما یک ایندکس اشتباه ارائه میکنید، میتوان به حافظه نامعتبر دسترسی پیدا کرد. راست شما را از این نوع خطا با متوقف کردن فوری برنامه به جای اجازه دسترسی به حافظه و ادامه برنامه محافظت میکند. فصل ۹ خطایابی در راست و نحوه نوشتن کد خوانا و ایمن که نه دچار پانیک شود و نه اجازه دسترسی نامعتبر به حافظه را بدهد، بیشتر بررسی میکند.