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

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

ساختار یا Struct چیست؟

ساختار یا struct که کوتاه‌شده‌ی کلمه‌ی structure است، در بسیاری از زبان‌های سطح پایین وجود دارد. اگر قبلاً‌ کدی به زبان C نوشته باشید حتماً زیاد از struct ها استفاده کرده اید.
ما با کمک struct ها می‌توانیم نوع‌داده‌های جدیدی تعریف کنیم. یعنی مقادیری که از لحاظ منطقی به هم مرتبط اند را در یک بسته با نام مشخص قرار بدهیم.
مهم‌ترین چیز در تشخیص اینکه الان باید از struct استفاده کنیم یا نه همین موضوع ارتباط است. شما در برنامه‌تان یک موجودیت منطقی دارید، پس بهتر است داده‌ها و عملکردهای مربوط به آن را یک‌جوری به هم متّصل کنید.
اگر با زبان‌های شئ‌گرا (Object Oriented) یا مبتنی بر شئ (مثل جاوااسکریپت) کار کرده‌اید، می‌توانید یک struct را معادل یک شئ (Object) درنظر بگیرید.

شیوه‌ی تعریف Struct در زبان Rust

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

  1. در struct هر تکّه دارای نام مشخصی است. با این نام ما به آن بخش مقدار می‌دهیم یا مقدارش‌را می‌گیریم.
  2. ما می‌توانیم به struct عملکردها را با استفاده از قابلیّت Method اضافه‌کنیم.

به خاطر این دو ویژگی استفاده از struct ها از tuple ها انعطاف‌پذیرتر است. اینجا دیگر لازم نیست ترتیب داده‌ها را به‌خاطر بسپاریم یا با استفاده از patternها داده‌ها را بگیریم.
خب حالا وقت آن است که ببینیم در Rust یک struct چطوری تعریف می‌شود. شیوه‌ی کلّی و البته کم‌تر قابل فهم تعریف یک struct این شکلی است:

struct TheNameOfYourStruct {
    key1: type1,
    key2: type2,
}

تعریف یک struct با خود کلمه‌ی کلیدی struct شروع می‌شود. بعد از کلمه‌ی struct اسم ساختار ما می‌آید. به‌خاطر داشته باشید که اسم یک struct باید به شکل PascalCase باشد.پس از آوردن اسم ساختار، حالا باید مقادیری که این struct خواهد داشت را مشخص کنیم.
مقادیر به‌صورت جفت‌های key:value مشخص می‌شوند. از آنجایی که مقادیر را هنگام تعریف struct مشخص نمی‌کنیم، در اینجا پس از آوردن اسم مقداری که قرار است ذخیره شود، نوع آن را مشخص می‌کنیم.
اینطوری تنها داده‌هایی که از نوع type1 هستند می‌توانند به key1 اختصاص داده شوند.
نکته: ما می‌توانیم نوع یک key را در struct از نوع رفرنس بگذاریم. امّا برای این کار باید lifetime آن را هم مشخص کنیم. در قسمت‌های بعدی آموزش به صورت کامل مفهوم lifetime را با هم یاد خواهیم گرفت.
نحوه‌ی کلّی ساخت نمونه از یک struct هم مانند زیر است:

let my_struct = TheNameOfYourStruct {
    key1: value1,
    key2: value2
}

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

مثالی از تعریف و استفاده از یک Struct در زبان Rust

اگر کمی گیج‌شده‌اید به شما حق می‌دهیم. بیایید با هم روی یک مثال ببینیم که کارکردن با struct ها چطوری است.
فرض کنید که ما می‌خواهیم یک برنامه‌ی مدیریت دروس برای یک دانشگاه بنویسیم.
ما در این سامانه سه موجودیت اساسی داریم: دانشجو، استاد و درس. می‌خواهیم برای هرکدام از این موجودیّت‌ها یک struct بسازیم. (چرا؟ چون داده‌های هرکدام به شکلی منطقی به هم مرتبط اند. مثلاً اسم و شماره‌ی دانشجویی یک دانشجو باید کنار هم باشد.)
بیایید اوّل از دانشجو شروع کنیم. هر دانشجو این ویژگی‌ها را دارد: نام که یک String است، شماره‌ی دانشجویی که یک عدد است و وضعیت تحصیل (فارغ‌التحصیل یا در حال تحصیل) که یک مقدار Boolean است.

struct Student {
    name: String,
    id: u32,
    graduated: bool
}

خب بیایید این تکّه کد را با هم مرور کنیم.
اوّل از همه ما یک struct ساخته‌ایم و اسمش‌را گذاشته‌ایم Student. همانطوری که بالاتر گفتم اسم struct باید به شکل PascalCase باشد، به همین خاطر با حرف بزرگ اسم این struct را شروع کردیم.
حالا باید مشخّص کنیم که در نوع‌داده‌ی Student قرار است چه مقادیری نگهداری شوند.
ما یک مقدار با نام name تعریف کرده‌ایم که نوع آن String است. برای نگهداری شماره‌ی دانشجویی یک مقدار دیگر به نام id تعریف کرده‌ایم و گفته‌ایم که داده‌ای که قرار است درونش قرار بدهیم از نوع u32 خواهد بود.
آخر سر هم یک مقدار دیگر به نام graduated تعریف کرده‌ایم که از نوع Boolean است. اگر دانشجو فارغ‌التّحصیل شده باشد مقدار true درونش قرار می‌گیرد. در غیر این صورت هم مقدار false.
حالا بیایید یک نمونه از این struct بسازیم و اطّلاعات یک دانشجو را درونش ذخیره کنیم:

let asghar = Student {
    id: 963258741,
    name: String::from("اصغر اکبرآبادی اصل"),
    graduated: false
};

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

چگونه یک struct را پرینت کنیم؟

حالا فرض کنید که می‌خواهیم یک struct را پرینت کنیم. بیایید اوّل همان روش عادی استفاده از !println را امتحان کنیم.

struct Student {
    name: String,
    id: u32,
    graduated: bool
}

fn main() {
    let asghar = Student {
        id: 963258741,
        name: String::from("اصغر اکبرآبادی اصل"),
        graduated: false
    };
    println!("{}", asghar);
}

اگر این‌را اجرا کنیم چه خروجی‌ای می‌گیریم؟

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

این ارور دارد به ما چه می‌گوید؟
ماکرو !println به صورت پیش‌فرض وقتی که به {} می‌رسد به دنبال استفاده از formatter مخصوص Display می‌رود. Display برای typeهای پیش‌فرض زبان تعریف شده است، امّا برای struct هایی که خودمان می‌سازیم از قبل تعریف نشده است.
بعداً که به توضیح درمورد trait ها و formatterها رسیدیم بیشتر در این مورد صحبت می‌کنیم. فعلاً همانطوری که درون پیغام خطا نوشته شده است، به جای {} از {?:} استفاده می‌کنیم تا از فرمت دیباگ استفاده کند.
برای این کار باید خط قبل از تعریف struct بگوییم که می‌خواهیم از فرمت debug برای نمایش این struct استفاده کنیم.

#[derive(Debug)]
struct Student {
    name: String,
    id: u32,
    graduated: bool
}

fn main() {
    let asghar = Student {
        id: 963258741,
        name: String::from("اصغر اکبرآبادی اصل"),
        graduated: false
    };
    println!("{:?}", asghar);
}

برای استفاده از فرمت debug قبل از تعریف ساختار، [derive(Debug)]# را قرار داده‌ایم.

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

Student { name: "اصغر اکبرآبادی اصل", id: 963258741, graduated: false }

تغییردادن یا گرفتن یک مقدار مشخّص از یک struct

برای دریافت یک مقدار از struct کافی است بعد از نام متغیّر یک نقطه بگذاریم و بعد key مربوط به آن مقدار را بنویسیم.
مثلاً فرض کنید می‌خواهیم نام دانشجو را دریافت کنیم و نمایشش بدهیم:

fn main() {
    let asghar = Student {
        id: 963258741,
        name: String::from("اصغر اکبرآبادی اصل"),
        graduated: false
    };
    println!("Student name: {}", asghar.name);
}

با نوشتن asghar.name مقدار name مربوط به ساختار asghar که از نوع Student است را می‌گیریم.
حالا فرض کنید ما می‌خواهیم یک ویژگی دانشجو را تغییر بدهیم. مثلاً اسم دانشجو اشتباه وارد شده است و ما می‌خواهیم این اشتباه را تصحیح کنیم.

fn main() {
    let asghar = Student {
        id: 963258741,
        name: String::from("اصغر اکبرآبادی اصل"),
        graduated: false
    };
    asghar.name = String::from("اکبر اصغر زاده");
    println!("Student name: {}", asghar.name);
}

همانطوری که از بخش توضیح mutability به خاطر دارید (اگر ندارید روی این نوشته کلیک کنید و خیلی زود به‌خاطر بیاورید)، انتظار ما این است که این کد ارور بدهد:

error[E0594]: cannot assign to field `asghar.name` of immutable binding
  --> src/main.rs:15:5
   |
10 |     let asghar = Student {
   |         ------ consider changing this to `mut asghar`
...
15 |     asghar.name = String::from("اکبر اصغر زاده");
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot mutably borrow field of immutable binding

خب حالا برای اینکه بتوانیم یک مقدار را تغییر بدهیم باید چه کار کنیم؟
در زبان Rust امکان این وجود ندارد که بتوانید تنها یک یا چند فیلد درون struct را mutable کنید. به همین خاطر موقع تعریف متغیّر، باید تمام آن را به عنوان یک متغیّر mutable تعریف کنیم.

fn main() {
    let mut asghar = Student {
        id: 963258741,
        name: String::from("اصغر اکبرآبادی اصل"),
        graduated: false
    };
    asghar.name = String::from("اکبر اصغر زاده");
    println!("Student name: {}", asghar.name);
}

حالا اگر این برنامه‌را اجرا کنیم می‌بینیم که نام دانشجو تغییر کرده است:

Student name: اکبر اصغر زاده

استفاده از Factory Function ها برای ساخت Struct ها

وقتی که لازم است به دفعات در کدمان نمونه‌هایی از یک struct را بسازیم، مخصوصاً وقتی که تعداد فیلدهای struct زیاد است، نوشتن تمامی key: value ها می‌تواند کار خسته‌کننده و سختی باشد.
برای این کار می‌توانیم از Factory Function ها استفاده کنیم.
عموماً به توابعی که یک Object جدید تولید می‌کنند Factory Function می‌گویند. ما در Rust چیزی به نام Object نداریم. امّا همانطوری که ابتدای نوشته گفتم تقریباً یک struct را می‌توان معادل یک Object دانست.
ما می‌توانیم برای ساخت نمونه‌های جدید از یک struct از یک تابع استفاده کنیم.
پارامترهای ورودی این تابع باید دقیقاً هم‌نام با key های فیلدهای درون struct باشند. مثلاً تابع زیر با هربار فراخوانی یک نمونه از ساختار Student را برمی‌گرداند:

fn create_student(name: String, id: u32, graduated: bool) -> Student {
    Student {
        name,
        id,
        graduated
    }
}

همانطوری که می‌بینید چون پارامترهای ورودی تابع با keyهای struct برابر اند، دیگر لازم نیست برای مشخص‌کردن مقادیر از ساختار key: value استفاده کنیم.
خود کامپایلر متوجّه می‌شود که باید به key متناظر به هر پارامتر مقدار آن پارامتر را اختصاص بدهد.
مثلاً کد زیر دقیقاً همان مقادیر متغیّر asghar را در کدهای قبلی تولید می‌کند و نمایش می‌دهد:

#[derive(Debug)]
struct Student {
    name: String,
    id: u32,
    graduated: bool
}
fn create_student(name: String, id: u32, graduated: bool) -> Student {
    Student {
        name,
        id,
        graduated
    }
}
fn main() {
    let asghar = create_student(String::from("اصغر اکبرآبادی اصل"), 963258741, true);
    println!("Student name: {:?}", asghar);
}

نتیجه دقیقاً مانند حالت عادی ساخت struct خواهد بود:

Student { name: "اصغر اکبرآبادی اصل", id: 963258741, graduated: false }

برای شروع کار با Struct ها همین‌قدر کافی است.
در قسمت بعدی با هم می‌بینیم که چطوری می‌توان برای Struct ها Method تعریف کرد. با این کار می‌توانیم علاوه‌بر کنار هم قرار دادن مقادیر مرتبط، عملکردهای مربوط به آن‌ها را هم به آن‌ها متّصل کنیم تا برنامه‌ی بهتری داشته باشیم.

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


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

خواندن قسمت قبلی

رفتن به اوّلین قسمت این مجموعه