یک برنامه نمونه با استفاده از Structها

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

بیایید یک پروژه باینری جدید با Cargo به نام rectangles ایجاد کنیم که عرض و ارتفاع یک مستطیل را بر حسب پیکسل مشخص کرده و مساحت آن را محاسبه کند. لیست ۵-۸ یک برنامه کوتاه نشان می‌دهد که دقیقاً همین کار را در فایل src/main.rs پروژه ما انجام می‌دهد.

Filename: 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
}
Listing 5-8: محاسبه مساحت یک مستطیل که با متغیرهای عرض و ارتفاع جداگانه مشخص شده است

اکنون، این برنامه را با استفاده از دستور 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 با هر یک از ابعاد موفق به محاسبه مساحت مستطیل می‌شود، اما می‌توانیم این کد را خواناتر و قابل درک‌تر کنیم.

مشکل این کد در امضای تابع area مشخص است:

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 قرار است مساحت یک مستطیل را محاسبه کند، اما تابعی که نوشتیم دو پارامتر دارد و هیچ‌کجا در برنامه مشخص نیست که این پارامترها به هم مرتبط هستند. بهتر است عرض و ارتفاع را به صورت گروهی تعریف کنیم تا خوانایی و مدیریت کد بهتر شود. یکی از روش‌هایی که قبلاً در بخش «نوع Tuple» فصل ۳ بحث کردیم این است که از تاپل‌ها استفاده کنیم.

بازنویسی با استفاده از Tupleها

لیست ۵-۹ نسخه دیگری از برنامه ما را نشان می‌دهد که از تاپل‌ها استفاده می‌کند.

Filename: 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
}
Listing 5-9: مشخص کردن عرض و ارتفاع مستطیل با یک Tuple

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

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

بازنویسی با استفاده از Structها: افزودن معنای بیشتر

ما از ساختارها استفاده می‌کنیم تا با نام‌گذاری داده‌ها، معنای بیشتری به آن‌ها بدهیم. می‌توانیم تاپلی که استفاده می‌کنیم را به یک ساختار تبدیل کنیم که برای کل داده‌ها یک نام و همچنین برای بخش‌های مختلف آن نام‌هایی مشخص کنیم، همان‌طور که در لیست ۵-۱۰ نشان داده شده است.

Filename: 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
}
Listing 5-10: تعریف یک ساختار Rectangle

در اینجا یک ساختار تعریف کرده‌ایم و نام آن را Rectangle گذاشته‌ایم. داخل آکولادها، فیلدهایی به نام‌های width و height تعریف کرده‌ایم که هر دو از نوع u32 هستند. سپس، در main، یک نمونه خاص از Rectangle ایجاد کرده‌ایم که عرض آن 30 و ارتفاع آن 50 است.

تابع area ما اکنون با یک پارامتر تعریف شده است که آن را rectangle نامیده‌ایم و نوع آن یک ارجاع غیرقابل تغییر به یک نمونه از ساختار Rectangle است. همان‌طور که در فصل ۴ اشاره شد، ما می‌خواهیم ساختار را قرض بگیریم نه اینکه مالکیت آن را بگیریم. به این ترتیب، main مالکیت خود را حفظ می‌کند و می‌تواند همچنان از rect1 استفاده کند. به همین دلیل است که از & در امضای تابع و در جایی که تابع را فراخوانی می‌کنیم استفاده می‌کنیم.

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

افزودن قابلیت‌های مفید با Traits مشتق‌شده

زمانی که در حال اشکال‌زدایی برنامه خود هستیم، مفید است که بتوانیم نمونه‌ای از Rectangle را چاپ کرده و مقادیر تمام فیلدهای آن را ببینیم. لیست ۵-۱۱ تلاش می‌کند با استفاده از ماکروی println! که در فصل‌های قبلی استفاده کرده‌ایم، این کار را انجام دهد. با این حال، این کار موفق نخواهد بود.

Filename: src/main.rs
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {}", rect1);
}
Listing 5-11: تلاش برای چاپ یک نمونه از Rectangle

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

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

ماکروی println! می‌تواند بسیاری از انواع فرمت‌بندی را انجام دهد، و به صورت پیش‌فرض، آکولادها به println! می‌گویند که از فرمت‌بندی‌ای که به نام Display شناخته می‌شود استفاده کند: خروجی‌ای که برای مصرف مستقیم کاربر نهایی در نظر گرفته شده است. انواع ابتدایی که تاکنون دیده‌ایم به صورت پیش‌فرض ویژگی Display را پیاده‌سازی می‌کنند زیرا تنها یک روش برای نمایش یک مقدار مانند 1 یا هر نوع ابتدایی دیگری به کاربر وجود دارد. اما با ساختارها، روش فرمت‌بندی خروجی کمتر واضح است زیرا امکانات بیشتری برای نمایش وجود دارد: آیا می‌خواهید از ویرگول استفاده شود یا خیر؟ آیا می‌خواهید آکولادها چاپ شوند؟ آیا تمام فیلدها باید نشان داده شوند؟ به دلیل این ابهام، Rust سعی نمی‌کند حدس بزند که ما چه می‌خواهیم، و ساختارها پیاده‌سازی‌ای برای Display ندارند که بتوان با println! و جایگزین {} استفاده کرد.

اگر به خواندن خطاها ادامه دهیم، به این یادداشت مفید خواهیم رسید:

   = 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! به صورت println!("rect1 is {rect1:?}"); خواهد بود. قرار دادن مشخص‌کننده :? داخل آکولادها به println! می‌گوید که می‌خواهیم از یک فرمت خروجی به نام Debug استفاده کنیم. ویژگی Debug به ما اجازه می‌دهد تا ساختار خود را به روشی که برای توسعه‌دهندگان مفید است چاپ کنیم تا مقدار آن را هنگام اشکال‌زدایی کد خود ببینیم.

کد را با این تغییر کامپایل کنید. خب، باز هم یک خطا دریافت می‌کنیم:

error[E0277]: `Rectangle` doesn't implement `Debug`

اما باز هم کامپایلر یادداشتی مفید به ما می‌دهد:

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

Rust در واقع قابلیت چاپ اطلاعات اشکال‌زدایی را دارد، اما باید به صورت صریح این قابلیت را برای ساختار خود فعال کنیم. برای انجام این کار، ویژگی بیرونی #[derive(Debug)] را دقیقاً قبل از تعریف ساختار اضافه می‌کنیم، همان‌طور که در لیست ۵-۱۲ نشان داده شده است.

Filename: src/main.rs
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {rect1:?}");
}
Listing 5-12: افزودن ویژگی برای مشتق کردن Debug و چاپ نمونه Rectangle با استفاده از فرمت اشکال‌زدایی

اکنون وقتی برنامه را اجرا می‌کنیم، هیچ خطایی دریافت نخواهیم کرد و خروجی زیر را خواهیم دید:

$ 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 }

عالی! این خروجی ممکن است زیباترین نباشد، اما مقادیر تمام فیلدها را برای این نمونه نشان می‌دهد که قطعاً در هنگام اشکال‌زدایی کمک می‌کند. زمانی که ساختارهای بزرگ‌تری داریم، مفید است که خروجی کمی آسان‌تر خوانده شود؛ در چنین مواردی می‌توانیم به جای {:?} از {:#?} در رشته println! استفاده کنیم. در این مثال، استفاده از سبک {:#?} خروجی زیر را ایجاد خواهد کرد:

$ 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,
}

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

Here is the continuation of the translation for “ch05-02-example-structs.md” into Persian:

در اینجا مثالی آورده شده است که در آن ما به مقدار اختصاص داده شده به فیلد width و همچنین مقدار کل ساختار در rect1 علاقه‌مند هستیم:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let scale = 2;
    let rect1 = Rectangle {
        width: dbg!(30 * scale),
        height: 50,
    };

    dbg!(&rect1);
}

ما می‌توانیم dbg! را در اطراف عبارت 30 * scale قرار دهیم و چون dbg! مالکیت مقدار عبارت را بازمی‌گرداند، فیلد width همان مقداری را خواهد داشت که اگر فراخوانی dbg! در آنجا وجود نداشت. ما نمی‌خواهیم dbg! مالکیت rect1 را بگیرد، بنابراین از یک ارجاع به rect1 در فراخوانی بعدی استفاده می‌کنیم. در اینجا خروجی این مثال آورده شده است:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
     Running `target/debug/rectangles`
[src/main.rs:10:16] 30 * scale = 60
[src/main.rs:14:5] &rect1 = Rectangle {
    width: 60,
    height: 50,
}

می‌توانیم ببینیم که اولین بخش خروجی از خط ۱۰ در src/main.rs آمده است، جایی که ما در حال اشکال‌زدایی عبارت 30 * scale هستیم، و مقدار حاصل آن 60 است (فرمت‌بندی Debug که برای اعداد صحیح پیاده‌سازی شده است فقط مقدار آن‌ها را چاپ می‌کند). فراخوانی dbg! در خط ۱۴ از src/main.rs مقدار &rect1 را چاپ می‌کند که ساختار Rectangle است. این خروجی از فرمت‌بندی زیبا و مفید Debug برای نوع Rectangle استفاده می‌کند. ماکروی dbg! می‌تواند در هنگام تلاش برای درک رفتار کدتان بسیار مفید باشد!

علاوه بر ویژگی Debug، Rust تعدادی ویژگی برای ما فراهم کرده است که می‌توانیم با استفاده از ویژگی derive آن‌ها را به نوع‌های سفارشی خود اضافه کنیم و رفتار مفیدی ارائه دهند. این ویژگی‌ها و رفتار آن‌ها در ضمیمه ج فهرست شده‌اند. ما در فصل ۱۰ به نحوه پیاده‌سازی این ویژگی‌ها با رفتار سفارشی و همچنین نحوه ایجاد ویژگی‌های خود می‌پردازیم. همچنین بسیاری از ویژگی‌های دیگر به غیر از derive وجود دارند؛ برای اطلاعات بیشتر، به بخش «ویژگی‌ها» در مرجع Rust مراجعه کنید.

تابع area ما بسیار خاص است: فقط مساحت مستطیل‌ها را محاسبه می‌کند. مفید خواهد بود اگر این رفتار را به صورت نزدیک‌تر با ساختار Rectangle مرتبط کنیم، زیرا این تابع با هیچ نوع دیگری کار نخواهد کرد. بیایید ببینیم که چگونه می‌توانیم با تبدیل تابع area به یک متد که برای نوع Rectangle تعریف شده است، این کد را بازنویسی کنیم.