یک برنامه نمونه با استفاده از Structها
برای درک بهتر زمانی که ممکن است بخواهیم از ساختارها استفاده کنیم، بیایید یک برنامه بنویسیم که مساحت یک مستطیل را محاسبه کند. ما ابتدا با استفاده از متغیرهای جداگانه شروع میکنیم و سپس برنامه را بازنویسی میکنیم تا از ساختارها استفاده کند.
بیایید یک پروژه باینری جدید با Cargo به نام rectangles ایجاد کنیم که عرض و ارتفاع یک مستطیل را بر حسب پیکسل مشخص کرده و مساحت آن را محاسبه کند. لیست ۵-۸ یک برنامه کوتاه نشان میدهد که دقیقاً همین کار را در فایل 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 }
اکنون، این برنامه را با استفاده از دستور 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ها
لیست ۵-۹ نسخه دیگری از برنامه ما را نشان میدهد که از تاپلها استفاده میکند.
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
ایندکس 0
تاپل و height
ایندکس 1
تاپل است. این کار حتی برای کسی که از کد ما استفاده میکند سختتر خواهد بود و به اشتباهات بیشتری منجر میشود. چون معنای دادههای ما در کد مشخص نشده است، احتمال خطا بیشتر میشود.
بازنویسی با استفاده از Structها: افزودن معنای بیشتر
ما از ساختارها استفاده میکنیم تا با نامگذاری دادهها، معنای بیشتری به آنها بدهیم. میتوانیم تاپلی که استفاده میکنیم را به یک ساختار تبدیل کنیم که برای کل دادهها یک نام و همچنین برای بخشهای مختلف آن نامهایی مشخص کنیم، همانطور که در لیست ۵-۱۰ نشان داده شده است.
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 }
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!
که در فصلهای قبلی استفاده کردهایم، این کار را انجام دهد. با این حال، این کار موفق نخواهد بود.
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {}", rect1);
}
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)]
را دقیقاً قبل از تعریف ساختار اضافه میکنیم، همانطور که در لیست ۵-۱۲ نشان داده شده است.
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!("rect1 is {rect1:?}"); }
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
تعریف شده است، این کد را بازنویسی کنیم.