آموزش زبان برنامه‌نویسی Rust – قسمت۱۳- شروع کار با Enumeration ها

در قسمت‌های قبلی فهمیدیم که struct چیست و چه کارهایی می‌توان با آن کرد. حالا می‌خواهیم با هم یک روش دیگر را برای تعریف type های متفاوت در زبان Rust یادبگیریم.

چرا به یک روش دیگر نیاز است و چطوری باید با آن کار کرد؟ برای فهمیدن پاسخ بیایید با هم enum ها را یادبگیریم.

Enum چیست؟

ما با enum، یک type جدید را تعریف می‌کنیم، امّا با شمردن تمامی حالات ممکن آن. درست همانطوری که از اسم آن، enumeration، به نظر می‌رسد.

مقادیر یک enum یک‌سری مقدار ثابت (constant) دارای نام مشخّص اند. برای یک متغیّر از نوع یک enum، در یک زمان مشخّص، تنها یکی از فیلدهای آن معنی دارد. یعنی چی؟ یکم که جلوتر برویم متوجّه می‌شوید.

شیوه‌ی تعریف‌کردن enum

برای بیشتر آشنا شدن با جناب enum بهتر است که مثل همیشه سراغ یک مثال برویم.

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

یک راه دیگر نمایش اعداد در فرمت rgb است. در این فرمت هر عدد را با ۳ عدد طبیعی که بین ۰ تا ۲۵۵ مقدار می‌گیرند، نمایش می‌دهیم. مثلاً همان رنگ قرمز این شکلی نمایش داده می‌شود: (۲۵۵,۰,۰) .

فرض‌کنید که ما در کدمان فقط همین دو حالت را داریم. پس اگر بخواهیم حالات نوع داده‌ی Colour را بشماریم، به دو حالت می‌رسیم: rgb و hex.

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

enum Colour {
    RGB,
    Hex
}

بیایید با بررسی کلمه‌به‌کلمه‌ی این کد بفهمیم که چطوری می‌توان یک enum را تعریف کرد.

تعریف یک enumeration با کلمه‌ی کلیدی enum شروع می‌شود. اینطوری به کامپایلر می‌فهمانیم که قرار است یک نوع‌داده‌ی جدید به شکل enumeration تعریف کنیم.

پس از کلمه‌ی کلیدی enum، اسم type جدیدمان را می‌نویسم. حالا ما می‌خواستیم یک type برای رنگ داشته باشیم، به همین خاطر اسمش را گذاشته‌ایم colour. حواستان باشد که مثل struct ها، اینجا هم اسامی باید PascalCase باشند.

بعد از اسم enum، داخل آکولادها حالت‌های مختلف این data type را مشخّص می‌کنیم. گفتیم که ما فقط ۲ فرمت رنگ را در کدمان داریم، پس اینجا هم فقط اسم همان ۲ نوع را می‌نویسم و آن‌ها را با کاما (,) از هم جدا می‌کنیم.

باز هم حواستان باشد که اسم حالاتی که برای یک enum تعریف می‌کنیم هم باید به شکل PascalCase باشد.

حالا می‌خواهیم یک متغیّر تعریف کنیم که حالت RGB را نمایش بدهد:

let a = Colour::RGB;

برای این کار ابتدا اسم enum را می‌نویسیم. اینطوری مشخّص می‌کنیم که قرار است از چه typeی استفاده کنیم.

بعد از آوردن اسم enum، با عملگر scope resolution (::) مشخّص می‌کنیم که کدام یک از حالات این نوع‌داده را می‌خواهیم. مثلاً اینجا بعد از :: می‌نویسم: RGB.

مقادیر enum در حافظه چه شکلی ذخیره می‌شوند؟

در حافظه، مقادیر enum ساده‌ای که ما تعریف کردیم به شکل اعداد صحیح ذخیره می‌شوند. کامپایلر به ترتیب به هر حالت یک عدد اختصاص می‌دهد. شروع شمارشش هم از عدد صفر است.

می‌توانیم با cast کردن مقدار یک enum بفهمید که عدد پیش‌فرضی که کامپایلر به آن مقدار اختصاص‌داده است چقدر است. فقط حواستان باشد که این اعداد قابل تغییر نیستند.

fn main() {
    let a = Colour::RGB;
    let b = Colour::Hex;
    println!("RGB value in memory: {}", a as u8);
    println!("Hex value in memory: {}", b as u8);
}

ما در این کد با نوشتن a as u8 مقدار متغیّر a را به نوع u8 تبدیل کرده‌ایم. اینطوری می‌توانیم به عددی که کامپایلر برای آن مقدار در نظر گرفته است دسترسی داشته باشیم. خروجی این برنامه این خواهد بود:

RGB value in memory: 0
Hex value in memory: 1

همانطوری که دیدید مقادیر enum به ترتیب شماره‌گذاری شده اند.

نکته:‌ کامپایلر همیشه سعی می‌کند که کوچکترین نوع داده‌ی عددی را برای ذخیره‌ی این اعداد استفاده کند تا حداقل فضا را در حافظه اشغال کند.

حالا ممکن است که شما نیازداشته‌باشید که این اعداد را خودتان تعیین کنید. چرا؟ چون اینطوری با معنی می‌شوند. مثلاً یک enum که قرار است status های مختلف یک درخواست http را مشخص کند.

برای این کار ما هنگام تعریف enum، مقدار هر حالت را مشخّص می‌کنیم:

enum HttpStatus {
    Ok = 200,
    NotFound = 404,
    InternalServerError = 500
}

خب حالا ببینیم که کامپایلر چه مقادیری را برای این حالات درنظر گرفته است:

fn main() {
    let a = HttpStatus::Ok;
    let b = HttpStatus::NotFound;
    let c = HttpStatus::InternalServerError;
    println!("Ok value in the memory: {}", a as u8);
    println!("NotFound value in the memory: {}", b as u8);
    println!("InternalServerError value in the memory: {}", c as u8);
}

با اجرای این برنامه انتظار داریم که اعداد ۲۰۰، ۴۰۴ و ۵۰۰ را ببینیم. درست است؟

Ok value in the memory: 200
NotFound value in the memory: 148
InternalServerError value in the memory: 244

اِ پس چرا اینطوری شد؟ دلیلش همان نکته‌ای است که بالاتر خواندیم. کامپایلر همیشه کوچکترین data type را برای ذخیره‌ی این مقادیر درنظر می‌گیرد. خب کوچکترین نوع‌داده‌ی عددی ای که می‌تواند عدد ۴۰۴ یا ۵۰۰ را ذخیره کند چی است؟ آفرین. u16. امّا ما مقدار را به u8 تغییر داده‌ایم. اینطوری یک مقدار از داده از دست رفته است و عددی که نمایش داده می‌شود اشتباه است.

حالا بیایید این بار دو مقدار بعدی را به جای u8 به u16 تبدیل کنیم:

fn main() {
    let a = HttpStatus::Ok;
    let b = HttpStatus::NotFound;
    let c = HttpStatus::InternalServerError;
    println!("Ok value in the memory: {}", a as u8);
    println!("NotFound value in the memory: {}", b as u16);
    println!("InternalServerError value in the memory: {}", c as u16);
}

حالا یک نفس عمیق می‌کشیم و با استعانت از سیاه‌چاله‌ی مرکز کهکشان آندرومدا، برنامه را اجرا می‌کنیم:

Ok value in the memory: 200
NotFound value in the memory: 404
InternalServerError value in the memory: 500

خب حالا همه‌چیز درست شد و می‌توانیم ببینم که کامپایلر آن مقادیری که ما خواسته بودیم را برای هر حالت درنظر گرفته است.

فقط حواستان باشد که در حالت قبلی هم کامپایلر همین مقادیر را برای هر حالت در نظر گرفته بود. ما با کد اشتباهی و تبدیل نادرست مقادیر، عدد اشتباهی را دریافت می‌کردیم.

استفاده از enum به عنوان مقداری در یک struct

خب حالا ذخیره‌کردن نوع رنگ وقتی که خود مقدارش را ذخیره‌نکرده‌ایم به چه دردی می‌خورد؟ حالا چطوری در کنار نوع، مقدار داده را هم ذخیره کنیم؟

برای من و شمایی که تازه struct ها را یادگرفته‌ایم، پاسخ مشخّص است. کافی است یک struct تعریف کنیم که هم نوع رنگ را ذخیره کند و هم مقدارش را:

struct ColourStruct {
    colour_type: Colour,
    value: String
}

حالا در دو تا متغیّر، یک بار رنگ قرمز را به فرمت hex و یک بار به فرمت rgb ذخیره می‌کنیم:

let rgb_colour = ColourStruct {
    colour_type: Colour::RGB,
    value: String::from("(255, 0, 0)")
};

let hex_colour = ColourStruct {
    colour_type: Colour::Hex,
    value: String::from("ff0000")
};

همه چیز خوب به نظر می‌رسد نه؟ ولی خب مسئله این است که rgb سه تا عدد integer است و hex یک String. الان هر دو را مجبوریم به شکل String ذخیره کنیم. ای‌کاش می‌شد داده را هم در enum ذخیره‌کنیم، نه؟

خب آرزویتان از قبل برآورده شده است. شما در Rust می‌توانید داده را هم در enum ذخیره‌کنید.

ذخیره‌ی مقادیر در خود enum

خب حالا می‌خواهیم که مقادیر را در خود enum ذخیره کنیم. برای حالتی که رنگ به فرمت rgb ذخیره‌شده است، می‌خواهیم ۳ عدد از نوع u16 را نگهداری کنیم تا نشان‌دهنده‌ی سه مقدار مختلف این فرمت باشد.

برای حالت hex هم می‌خواهیم که یک String را ذخیره کنیم.

بیایید اوّل کد را با هم ببینیم:

enum Colour {
    RGB(u16, u16, u16),
    Hex(String)
}

همانطوری که می‌بینید همه‌چیز مثل قبل است، با این تفاوت که مقابل نام حالت و داخل پرانتز، داده‌ای که قرار است درونش ذخیره بشود را مشخّص کرده ایم. مثلاً برای RGB گفته‌ایم که می‌خواهیم یک tuple like struct سه‌تایی را ذخیره‌کنیم که ۳ تا عدد از نوع u16 درونش ذخیره می‌کند.

یا برای Hex گفته‌ایم که قرار است یک String درونش ذخیره بشود.

حالا درون کد چطوری متغیّری از این نوع و با مقدار مشخّص تعریف کنیم؟

let rgb_colour = Colour::RGB(255, 0, 0);
let hex_colour = Colour::Hex(String::from("ff0000"));

انواع حالات در یک enum

ما در کل ۳ نوع حالت مختلف را می‌توانیم در enum ها تعریف کنیم. درست مثل ۳ حالت مختلف structی که داشتیم.

حالت ساده که در بخش ابتدایی دیدیم و همان unit like struct ها اند. حالت دوم که همین بالا دیدیم و مقدار را به صورت یک tuple like struct ذخیره می‌کنیم.

حالت سوم هم ذخیره‌ی مقادیر در یک enum به عنوان یک struct معمولی است.

مثلاً در RGB هر عدد نشان‌دهنده‌ی یک رنگ است. R برای قرمز، G برای سبز و B برای آبی. حالا ما می‌خواهیم به جای اینکه با یک tuple سه‌تایی که مقادیرش اسم ندارند، مقادیر را با یک struct مشخّص کنیم.

برای این کار کافی است کد را به شکل زیر تغییر بدهیم:

enum Colour {
    RGB {red: u16, green: u16, blue: u16},
    Hex(String)
}

حالا مقداری که در حالت RGB ذخیره می‌شود یک struct است. پس ما می‌توانیم مقادیر را بدون درنظر گرفتن ترتیبشان و با اسم آن‌ها مشخّص کنیم:

let rgb_colour = Colour::RGB {
    blue: 0,
    red: 255,
    green: 0
};

let hex_colour = Colour::Hex(String::from("ff0000"));

در Rust هرچیزی که برنامه‌نویسی را ساده‌تر و کد را خواناتر می‌کند درنظرگرفته‌شده است.

کی باید به جای struct از enum استفاده کرد؟

struct ها ترکیب‌هایی از نوع «و» منطقی هستند. یعنی تمامی مقادیر باهم یک نوع داده را می‌سازند. امّا enum ها ترکیب‌هایی از نوع «یا» منطقی هستند. یعنی یک enum تنها یکی از حالات مختلف است. پس وقتی که یک رنگ از نوع RGB است، اصلاً hex و رفتارهای متناسب با آن برایش تعریف نمی‌شود.

اگر بخواهیم همین کار را با struct انجام بدهیم، یعنی یک نمونه از struct در یک زمان یا rgb باشد یا hex، باید به سختی و به صورت نصفه‌نیمه رفتارش را کنترل کنیم تا نتواند در یک زمان هر دو رفتار  را داشته باشد.

به علاوه چون کامپایلر می‌داند که یک نمونه از یک enum نمی‌تواند هم‌زمان دو حالت را داشته باشد، می‌تواند فضا را برای ما بهینه کند. مثلاً برای چند حالت تنها یک فضای مشخّص را اختصاص بدهد.

به علاوه از enum ها می‌توان در pattern matching هم استفاده کرد. بعداً درموردش خیلی یادمی‌گیریم.

خب جلسه‌ی آشنایی با enum تمام شد. ما هنوز نمی‌دانیم که چطوری می‌توان به مقادیر ذخیره‌شده در یک enum دسترسی داشت. به علاوه enum ها هم مانند struct ها متد و associated function دارند. باید ببینیم که آن‌ها را هم چطوری می‌توان تعریف کرد.

برای فهمیدن تمام این‌ها باید اوّل یک چیز دیگر را یادبگیریم: pattern matching. در جلسه‌ی بعدی به شکل کامل یادمی‌گیریم که pattern ها چی اند، match چه کار می‌کند و همه‌ی این‌ها چطوری زندگی برنامه‌نویس‌ها را زیبا و بدون باگ می‌کنند.

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


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


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

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