وبلاگ شخصی
محمّدرضا
علی حسینی

جایی که تجربیات, علایق
و چیزهایی که یادگرفته‌ام را
با هم مرور می‌کنیم.

آموزش زبان برنامه‌نویسی Rust – قسمت 2: انواع داده‌های عددی و عملگرهای آن‌ها

آموزش زبان برنامه‌نویسی Rust – قسمت 2: انواع داده‌های عددی و عملگرهای آن‌ها

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

می‌توانید نسخه‌ی ویدیویی این آموزش را اینجا ببینید:

YouTube video

انواع داده در زبان Rust

در زبان Rust ما دو نوع داده مختلف داریم: عددی(Scalar) و ترکیبی(compound). از آنجایی که زبان Rust یک زبان statically typed است, یعنی نوع هر متغیّر یا ثابت باید در زمان کامپایل کاملاً مشخّص باشد, دانستن نوع داده های مختلف ضروری است.

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

در این قسمت فقط به نوع داده های عددی می‌پردازیم. در قسمت بعد با یکدیگر انواع دیگر را بررسی خواهیم کرد.

قبل از اینکه به نوع عددی در زبان Rust بپردازیم, یک مفهوم مهم در دنیای کامپیوتر را باید مرور کنیم.

اعداد علامت‌دار و بدون علامت

ما دو جور عدد داریم: اعدادی که علامتشان مهم است, یعنی مثبت و منفی دارند. و اعدادی که علامتشان مهم نیست و می‌توان همیشه آن‌هارا مثبت درنظر گرفت.

شاید بپرسید که نوع بدون علامت به چه دردی می‌خورد؟ تفاوت این دو عدد در نحوه‌ی ذخیره‌سازی آن‌ها است.

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

بگذارید با یک مثال جلو برویم. نحوه‌ی نمایش بدون علامت عدد 2 به صورت دودویی اینگونه است: 10. حالا اگر بخواهیم عدد 2+ را نمایش بدهیم, باید آن‌را به شکل 010 نمایش دهیم. آن صفری که قبل از 1 آمده است یعنی اینکه این عدد مثبت است.

اگر هم بخواهیم عدد 2- را نمایش دهیم حاصل 110 خواهد شد. معنای چپ‌ترین 1 این است که این عدد منفی است.

حالا در کامپیوتر ما مجبوریم که برای نمایش اعداد از بخش‌های هم‌اندازه‌ای از حافظه استفاده کنیم. مثلاً اعداد را در بخش‌های 8 بیتی نمایش دهیم.

وقتی عددی بدون علامت است, از همه‌ی این 8 بیت برای نمایش آن عدد می‌توان استفاده کرد. یعنی می‌توان اعداد 0 تا 255 را نشان داد.

امّا وقتی عدد علامت‌دار است, چپ‌ترین بیت به نمایش علامت عدد اختصاص داده می‌شود. پس برای خود عدد تنها 7 بیت باقی می‌ماند. با این حساب می‌توان اعداد 128- تا 127 را نشان داد. همانطور که می‌بینید اندازه‌ی اعدادی که می‌توان نمایش داد کاهش می‌یابد.

به‌طور کلّی استفاده از اعداد بدون علامت وقتی که داده‌ی منفی نداریم راه بهتری است. چون می‌توانیم با فضای یکسان اعداد بیشتری‌را ذخیره کنیم.

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

اعداد صحیح(Integer) در زبان Rust

اعداد صحیح اعدادی هستند که قسمت اعشاری ندارند و مقدارشان مشخّص است. در زبان Rust دو نوع کلّی از اعداد صحیح وجود دارند: اعداد صحیح علامت‌دار(Signed)  و اعداد صحیح بدون علامت(Unsigned).

فرمت کلّی برای یک نوع داده integer در زبان Rust این طوری است:

Type size

در جدول زیر 6 نوع مختلف برای اعداد علامت‌دار و بدون علامت نمایش‌داده شده اند:

طول علامت‌دار بدون علامت
۸ بیت i8 u8
۱۶ بیت i16 u16
۳۲ بیت i32 u32
۶۴ بیت i64 u64
۱۲۸ بیت i128 u128
معماری سیستم isize usize

همانطوری که می‌بینید علاوه بر حالاتی که خودمان دقیقاً اندازه‌ی عدد را انتخاب می‌کنیم, مقادیر isize و usize هم وجود دارند که از معماری سیستم پیروی می‌کنند. اگر معماری ماشین شما 64 بیتی باشد, اندازه عدد 64 بیت خواهد بود. اگر معماری 32 بیتی باشد, اندازه‌ی عدد هم 32 بیت خواهد بود.

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

در قطعه کد زیر چند مثال از متغیّرهایی با نوع‌های مختلف را می‌بینیم:

let  a: u64 = 1024;
let b: i8 = -7;
let c : usize = 800;
let d = -64;          // به صورت پیش‌فرض نوع این متغیّر برابر با i32 در نظر گرفته می‌شود

نمایش مقادیر عددی

در زبان Rust شما می‌توانید اعداد را در مبنای 10, 8, 16 و 2 نمایش دهید. همچنین شیوه‌ی به‌خصوص بایت هم وجود دارد که می‌تواند در متغیّرهایی از نوع u8 کاراکترها را ذخیره کند.

همچنین برای افزایش خوانایی می‌توانید بین ارقام علامت _ را قرار دهید.

به مثال‌های زیر توجّه کنید:

let a = 123_456;
let b = 0xf2; //  hexadecimal
let c = 0o71;	// octal
let d = 0b1110_0001;	// binary
let c = b'C';	// byte

محدوده‌ی هر نوع و سرریز(Overflow)

هر نوع عددی با توجّه به علامت‌دار بودن یا نبودن و اندازه‌ی آن می‌تواند محدوده‌ی خاصی از اعداد را نمایش دهد. اگر عددی کوچک‌تر یا بزرگتر از آن محدوده را به آن نسبت دهیم سرریز(overflow) رخ می‌دهد. یعنی چون نمی‌توانیم مقدار کامل‌را نمایش دهیم, مقداری که نمایش‌داده می‌شود اشتباه خواهد بود.

در جدول زیر حداقل و حداکثر مقداری که هر نوع داده نگهداری می‌کند را می‌بینید:

نوع داده‌ی عددی حداقل عدد قابل نمایش حداکثر عدد قابل نمایش
i8 ۱۲۸− ۱۲۷
u8 ۰ ۲۵۵
i16 ۳۲٫۷۶۸− ۳۲٫۷۶۷
u16 ۰ ۶۵٫۶۳۵
i32 ۲٫۱۴۷٫۴۸۳٫۶۴۸− ۲٫۱۴۷٫۴۸۳٫۶۴۷
u32 ۰ ۴٫۲۹۴٫۹۶۷٫۲۹۵
i64 ۹٫۲۲۳٫۳۷۲٫۰۳۶٫۸۵۴٫۷۷۵٫۸۰۸− ۹٫۲۲۳٫۳۷۲٫۰۳۶٫۸۵۴٫۷۷۵٫۸۰۷
u64 ۰ ۱۸٫۴۴۶٫۷۴۴٫۰۷۳٫۷۰۹٫۵۵۱٫۶۱۵
i128 ۱۷۰٫۱۴۱٫۱۸۳٫۴۶۰٫۴۶۹٫۲۳۱٫۷۳۱٫۶۸۷٫۳۰۳٫۷۱۵٫۸۸۴٫۱۰۵٫۷۲۸− ۱۷۰٫۱۴۱٫۱۸۳٫۴۶۰٫۴۶۹٫۲۳۱٫۷۳۱٫۶۸۷٫۳۰۳٫۷۱۵٫۸۸۴٫۱۰۵٫۷۲۷
u128 ۰ ۳۴۰٫۲۸۲٫۳۶۶٫۹۲۰٫۹۳۸٫۴۶۳٫۴۶۳٫۳۷۴٫۶۰۷٫۴۳۱٫۷۶۸٫۲۱۱٫۴۵۵

حالا اگر overflow رخ دهد چه اتّفاقی می‌افتد؟ بیایید امتحان کنیم.

یک برنامه می‌نویسم و در آن مقدار 255 را به متغیّری از نوع i8  نسبت می‌دهیم:

fn main() {
    let a:i8 = 0xf_f;
    println!("{}\n", a);
}

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

error: literal out of range for i8
 --> src/main.rs:3:16
  |
3 |     let a:i8 = 0xf_f;
  |                ^^^^^
  |
  = note: #[deny(overflowing_literals)] on by default
  = note: the literal `0xf_f` (decimal `255`) does not fit into an `i8` and will become `-1i8`
  = help: consider using `u8` instead

چرا این اتّفاق می‌افتد؟ چون بعد از overflow مقداری که در متغیّر قرار می‌گیرد چیزی نیست که ما می‌خواهیم و این یعنی منطق و عملکرد برنامه کاملاً خراب می‌شود.

رخ‌دادن overflow اتّفاق نسبتاً رایجی است و باید خیلی حواستان به آن باشد. اگر در حالتی متغیّری overflow کند, تمامی روند برنامه‌تان مختل می‌شود و نتیجه‌ای که کاربر می‌بیند اشتباه می‌شود.

بزرگترین مشکل overflow این است که عموماً در حالات خاصی از روند اجرای برنامه رخ می‌دهد, به همین دلیل شما هنگام نوشتن برنامه از درستی کارکرد آن مطمئن هستید, امّا هنگامی که برنامه به دست کاربران می‌افتد می‌بینید که خیلی‌ها به خاطر کارکرد اشتباه برنامه شاکی شده اند.

اعداد اعشاری(floating point) در زبان Rust

خب کارمان با اعداد صحیح تمام شد. پس نوبتی هم که باشد, نوبت اعداد اعشاری است. اعدادی که از دو بخش «جزء صحیح» و «اعشار» تشکیل می‌شوند.

در زبان Rust ما تنها 2 نوع عدد اعشاری داریم.  f32 و f64. همانطوری که حدس می‌زنید حرف f مخفف floating point است و عدد مقابل آن هم اندازه‌ی عدد را نمایش می‌دهد.

از آنجایی که در اکثر دستگاه‌های امروزی سرعت پردازی اعداد اعشاری 32 بیتی و 64 بیتی تفاوت چندانی ندارد, مقدار پیش‌فرض در زبان Rust نوع f64 است. چون با سرعت پردازش برابر دقّت بیشتری در نمایش اعشار دارد. ساده‌ترش را بخواهید یعنی تعداد ارقام اعشاری بیشتری را نمایش می‌دهد.

let b: f32 = 2.95;  
let a = 2.95;	// به صورت پیش‌فرض نوع متغیّر برابر با f64 درتظر گرفته می‌شود

کپی کردن علامت اعداد اعشاری

ما می‌توانیم علامت یک عدد اعشاری را روی دیگری کپی کنیم. برای این کار باید از متد copysign استفاده کنیم. (در جلسات بعدی می‌فهمیم که متدها چی هستند. فعلاً نگران آن نباشید)
مثلاً برنامه‌ی زیر را ببینید:

fn main() {
    let negative_float = -3.16;
    println!("{}", 5.5_f32.copysign(negative_float));
}

ما در این برنامه ابتدا یک عدد اعشاری منفی را تعریف کرده‌ایم. سپس علامت آن را روی عدد اعشاری ۵.۵ کپی کرده‌ایم.
برای این کار ابتدا عدد ۵.۵ را نوشته‌ایم. سپس بعد از علامت _ نوع عدد، در اینجا f32، را وارد کرده‌ایم. حالا برای فراخوانی متد copysign ابتدا یک نقطه گذاشته و سپس اسم متد را نوشته‌ایم. درون پرانتزهای مقابل نام متد هم عددی را که می‌خواهیم علامتش را روی عدد جدید کپی کنیم قرار داده‌ایم.
حالا اگر این برنامه را اجرا کنیم خروجی زیر را دریافت می‌کنیم:

-5.5

 

عملگرهای انواع عددی

روی مقادیر عددی می‌توان عملیات جمع, تفریق, ضرب, تقسیم و mod(باقی‌مانده)گیری را انجام داد. مثل اکثر زبان‌های دیگر برای جمع از علامت + , برای تفریق از علامت - , برای ضرب از علامت * , برای تقسیم از علامت / و برای باقی‌مانده گیری از علامت ٪ استفاده می‌شود.

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

مثلاً برنامه‌ی زیر را اجرا می‌کنیم:

fn main() {
    let a = 5;
    let c = 2.5;
    let d = a - c;
    println!("{}\n", d);
}

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

error[E0277]: cannot subtract `{float}` from `{integer}`
 --> src/main.rs:4:15
  |
4 |     let d = a - c;
  |               ^ no implementation for `{integer} - {float}`
  |
  = help: the trait `std::ops::Sub<{float}>` is not implemented for `{integer}`

همانطوری که در متن ارور می‌بینید در زبان Rust امکان تفریق عدد صحیح و اعشار از هم وجود ندارد. همان‌طور که امکان هیچ عملیاتی که شامل هر دو نوع شود وجود ندارد.

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

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

 

خواندن جلسه‌ی بعدی

 

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

با کلیک روی این‌یکی متن هم به جلسه‌ی قبلی خواهید رفت.

«نوشته‌های مرتبط»

پاسخی بگذارید

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

«نوشته‌های ویژه»

«نوشته‌های محبوب»

«دیدگاه کاربران»