مقدمه

برای درک فایده‌ی استفاده از ساختارها، در اینجا برنامه‌ای می‌نویسیم که مساحت یک مستطیل را محاسبه کند. ابتدا این برنامه را با استفاده از متغیرهای اسکالر می‌نویسیم و بعد با استفاده از تاپل و در نهایت با استفاده از ساختار، این برنامه را بهبود می‌دهیم.

محاسبه مساحت مستطیل

یک پروژه‌ی باینری با نام rectangles ایجاد می‌کنیم و کد زیر را در فایل src/main.rs وارد می‌کنیم.

Copy Icon src/main.rs
fn main() {
  let width1 = 30;
  let height1 = 50;
          
  println!(
    "The area of the rectangle is {} square pixels.",
    area(width1, height1)
  );
}
          
fn area(width: u32, height: u32) -> u32 {
  width * height
}

تابع area() دو پارامتر از نوع u32 دریافت می‌کند و حاصلضرب آنها را برمی‌گرداند. اگر این برنامه را با استفاده از کامند cargo run اجرا کنیم، نتیجه‌ی زیر را مشاهده خواهیم کرد.

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
     Running `target/debug/rectangles`
The area of the rectangle is 1500 square pixels.
          

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

بازنویسی برنامه با تاپل‌ها

در کد زیر یک ورژن دیگر از برنامه را می‌بینیم که به جای اینکه از متغیرهای اسکالر width و height به عنوان پارامترهای تابع استفاده کند، از یک تاپل با نام dimensions استفاده کرده است.

Copy Icon src/main.rs
fn main() {
  let rect1 = (30, 50);
          
  println!(
    "The area of the rectangle is {} square pixels.",
    area(rect1)
  );
}
          
fn area(dimensions: (u32, u32)) -> u32 {
  dimensions.0 * dimensions.1
}

الان مرتبط بودن داده‌هایی که بناست ابعاد مستطیل در نظر گرفته شوند، مشخص است و از این نظر، برنامه نسبت به ورژن قبلی‌اش بهتر است. اما چون تاپل نامی به عناصرش نمی‌دهد، برای ارجاع به عرض و ارتفاع مستطیل باید از اندیس‌ها استفاده کنیم و از این نظر، خوانایی برنامه نسبت به ورژن قبلی، حتی بدتر شده است. به علاوه، ما باید همیشه یادمان باشد که اندیس صفر به width و اندیس 1 به height تعلق دارد. البته در مورد محاسبه‌ی مساحت یک مستطیل، به خاطر سپردن این موضوع ضرورتی ندارد اما اگر مثلاً بخواهیم مستطیل را ترسیم کنیم، این موضوع اهمیت پیدا می‌کند.

همه‌ی این مشکلات از این موضوع ناشی می‌شود که در کد ما معنای داده‌ها مشخص نیست. با استفاده از ساختارها (structs) می‌توانیم به داده‌ها معنا بدهیم و از این طریق، خوانایی برنامه را افزایش دهیم.

بازنویسی برنامه با ساختارها

حلقه‌ی مفقوده در برنامه‌ی ما، ساختارها هستند. با استفاده از ساختارها ما به داده‌ها نام و نتیجتاً معنا می‌دهیم. در کد زیر، یک ساختار با نام Rectangle تعریف شده و از آن به عنوان نوع پارامتر تابع area() استفاده شده است. به این ترتیب، به یک ورژن خوانا و با وضوح بالا از برنامه می‌رسیم.

Copy Icon src/main.rs
struct Rectangle {
  width: u32,
  height: u32,
}
          
fn main() {
  let rect1 = Rectangle {
    width: 30,
    height: 50,
  };
          
  println!(
    "The area of the rectangle is {} square pixels.",
    area(&rect1)
  );
}
          
fn area(rectangle: &Rectangle) -> u32 {
  rectangle.width * rectangle.height
}

در این ورژن از برنامه، امضای تابع area() کاملاً عملکرد این تابع را روایت می‌کند. این تابع، یک مستطیل را دریافت می‌کند و مساحتش را محاسبه می‌کند. به لطف امکان نامگذاری ساختار کلی و داده‌ها یا همان فیلدهای ساختار، هدف ما یعنی خوانایی و وضوح بالای برنامه، محقق می‌شود.

توجه داشته باشید که پارامتر تابع area() یک رفرنس به Rectangle است که باعث می‌شود مالکیت نمونه‌ی Rectangle به تابع area() منتقل نشود و بنابراین، این نمونه بعد از فراخوانی تابع area() همچنان در تابع main() در دسترس باشد.

استفاده از قابلیت‌های Derived Traits

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

Copy Icon src/main.rs
struct Rectangle {
  width: u32,
  height: u32,
}
          
fn main() {
  let rect1 = Rectangle {
    width: 30,
    height: 50,
  };
          
  println!("rect1 is {}", rect1);
}

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

error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`

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

ماکروی println!() می‌تواند خروجی را به روش‌های مختلفی فرمت کند. در حالت پیش فرض، استفاده از آکلادها باعث می‌شود که از روشی به نام Display برای فرمت خروجی استفاده شود. تا قبل از این مثال، از ماکروی println!() فقط برای چاپ مقادیر نوع‌های primitive استفاده کرده بودیم. این نوع‌ها به طور پیش‌فرض Display را پیاده‌سازی کرده‌اند؛ چون یک مقدار primitive مثل 1 را فقط به یک روش می‌توان چاپ کرد. اما در مورد ساختارها، یک نمونه را می‌توان به روش‌های مختلفی فرمت کرد. مثلاً می‌توان از کاما استفاده کرد یا نکرد یا اینکه خود آکلادها را چاپ کرد یا نکرد. بنابراین، ساختارها Display را پیاده‌سازی نکرده‌اند و این علت بروز خطای بالاست. در واقع، در مثال بالا ما سغی کرده‌ایم برای یک ساختار از قابلیتی که ندارد، استفاده کنیم.

در ادامه‌ی پیغام خطای تولید شده، راه‌حلی به ما پیشنهاد شده است:

= help: the trait `std::fmt::Display` is not implemented for `Rectangle`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead         
          

پیشنهاد شده که از :? درون آکلادها استفاده کنیم. با این کار، ماکروی println!() از یک فرمت دیگر با نام Debug برای نمایش خروجی استفاده می‌کند. این فرمت همانطور که نامش نشان می‌دهد، برای مشاهده‌ی مقادیر توسط برنامه‌نویس در حین نوشتن و دیباگ کدها مناسب است. برای تست این روش، باید گزاره‌ی println!() در مثال بالا را به صورت زیر بنویسیم.

println!("rect1 is {rect1:?}");

اما اگر برنامه را بعد از این تغییر، اجرا کنیم، باز هم با خطا مواجه خواهیم شد.

= help: the trait `Debug` is not implemented for `Rectangle`
= note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle`         
          

پیغام خطا می گوید که Debug توسط نوع Rectangle پیاده‌سازی نشده است. اما این بار راه حل متفاوتی را پیشنهاد داده است. کافیست یک attribute با نام #[derive(Debug)] را قبل از تعریف ساختار قرار دهیم. این کار باعث می‌شود که پیاده‌سازی فراهم‌شده توسط Rust برای Debug، روی ساختار Rectangle اعمال شود. یک trait مانند Debug که دارای یک پیاده‌سازی پیش‌فرض باشد و بتوان آن را با استفاده از تابع derive() در قالب یک attribute روی یک نوع اعمال کرد، یک Derived Trait نامیده می‌شود.

Copy Icon src/main.rs
#[derive(Debug)]
struct Rectangle {
  width: u32,
  height: u32,
}
            
fn main() {
  let rect1 = Rectangle {
    width: 30,
    height: 50,
  };
            
  println!("rect1 is {rect1:?}");
}

حالا اگر برنامه را اجرا کنیم، خطایی در کار نخواهد بود و خروجی زیر را مشاهده خواهیم کرد.

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/rectangles`
rect1 is Rectangle { width: 30, height: 50 }
          

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

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/rectangles`
rect1 is Rectangle {
    width: 30,
    height: 50,
}