تعریف و نمونهسازی Structها
ساختارها مشابه تاپلها هستند که در بخش «نوع Tuple» مورد بحث قرار گرفتند، به این معنا که هر دو شامل مقادیر مرتبط متعددی هستند. مانند تاپلها، اجزای یک ساختار میتوانند از انواع مختلفی باشند. اما برخلاف تاپلها، در یک ساختار شما برای هر جزء داده نام تعیین میکنید تا معنای مقادیر روشنتر شود. افزودن این نامها باعث میشود که ساختارها از تاپلها انعطافپذیرتر باشند: شما مجبور نیستید برای مشخص کردن یا دسترسی به مقادیر یک نمونه به ترتیب دادهها تکیه کنید.
برای تعریف یک ساختار، کلمه کلیدی struct را وارد کرده و نام کل ساختار را تعیین میکنیم. نام یک ساختار باید توصیفکننده اهمیت اجزای دادهای باشد که با هم گروهبندی میشوند. سپس، داخل آکولادها، نامها و انواع اجزای دادهای را که به آنها فیلد میگوییم، تعریف میکنیم. برای مثال، لیست ۵-۱ یک ساختار را نشان میدهد که اطلاعات مربوط به یک حساب کاربری را ذخیره میکند.
struct User { active: bool, username: String, email: String, sign_in_count: u64, } fn main() {}
Userبرای استفاده از یک struct پس از تعریف آن، باید یک instance از آن ایجاد کنیم
با مشخص کردن مقادیر مشخص برای هر یک از فیلدها.
برای ساختن یک instance، نام struct را مینویسیم
و سپس داخل کروشهها جفتهای کلید: مقدار قرار میدهیم؛
که در آنها، کلیدها نام فیلدها هستند و مقادیر، دادههایی هستند که میخواهیم در آن فیلدها ذخیره کنیم.
لازم نیست فیلدها را به همان ترتیبی بنویسیم که در تعریف struct آمدهاند.
به عبارت دیگر، تعریف struct مانند یک الگوی کلی برای نوع داده است
و instanceها آن الگو را با دادههای مشخص پر میکنند تا مقادیر آن نوع را بسازند.
برای نمونه، میتوانیم یک کاربر خاص را همانطور که در لیست ۵-۲ نشان داده شده تعریف کنیم.
struct User { active: bool, username: String, email: String, sign_in_count: u64, } fn main() { let user1 = User { active: true, username: String::from("someusername123"), email: String::from("[email protected]"), sign_in_count: 1, }; }
Userبرای بهدستآوردن مقدار خاصی از یک ساختار، از نشانهگذاری نقطه استفاده میکنیم. به عنوان مثال، برای دسترسی به آدرس ایمیل این کاربر، از user1.email استفاده میکنیم. اگر نمونه قابل تغییر باشد، میتوانیم مقدار را با استفاده از نشانهگذاری نقطه تغییر داده و در یک فیلد خاص مقداردهی کنیم. لیست ۵-۳ نشان میدهد که چگونه مقدار در فیلد email یک نمونه قابل تغییر User را تغییر دهیم.
struct User { active: bool, username: String, email: String, sign_in_count: u64, } fn main() { let mut user1 = User { active: true, username: String::from("someusername123"), email: String::from("[email protected]"), sign_in_count: 1, }; user1.email = String::from("[email protected]"); }
email یک نمونه Userتوجه داشته باشید که کل نمونه باید قابل تغییر باشد؛ Rust به ما اجازه نمیدهد که فقط برخی از فیلدها را به صورت قابل تغییر علامتگذاری کنیم. مانند هر عبارت دیگری، میتوانیم یک نمونه جدید از ساختار را به عنوان آخرین عبارت در بدنه یک تابع بسازیم تا به طور ضمنی آن نمونه جدید را بازگردانیم.
لیست ۵-۴ یک تابع build_user را نشان میدهد که یک نمونه از User را با ایمیل و نام کاربری مشخص برمیگرداند. فیلد active مقدار true میگیرد و sign_in_count مقدار 1 دریافت میکند.
struct User { active: bool, username: String, email: String, sign_in_count: u64, } fn build_user(email: String, username: String) -> User { User { active: true, username: username, email: email, sign_in_count: 1, } } fn main() { let user1 = build_user( String::from("[email protected]"), String::from("someusername123"), ); }
build_user که یک ایمیل و نام کاربری میگیرد و یک نمونه User را بازمیگرداندنوشتن نام پارامترهای تابع با همان نام فیلدهای ساختار منطقی است، اما تکرار نامهای email و username برای هر دو فیلد و متغیرها کمی خستهکننده است. اگر ساختار فیلدهای بیشتری داشت، تکرار هر نام حتی آزاردهندهتر میشد. خوشبختانه، یک راه میانبر راحت وجود دارد!
استفاده از میانبر مقداردهی فیلد
از آنجا که نام پارامترها و نام فیلدهای ساختار دقیقاً یکسان هستند، میتوانیم از نحو میانبر مقداردهی فیلد برای بازنویسی build_user استفاده کنیم تا همان رفتار را داشته باشد اما تکرار username و email را نداشته باشد، همانطور که در لیست ۵-۵ نشان داده شده است.
struct User { active: bool, username: String, email: String, sign_in_count: u64, } fn build_user(email: String, username: String) -> User { User { active: true, username, email, sign_in_count: 1, } } fn main() { let user1 = build_user( String::from("[email protected]"), String::from("someusername123"), ); }
build_user که از میانبر مقداردهی فیلد استفاده میکند زیرا پارامترهای username و email همان نام فیلدهای ساختار را دارنداینجا، ما یک نمونه جدید از ساختار User میسازیم که فیلدی به نام email دارد. ما میخواهیم مقدار فیلد email را به مقداری که در پارامتر email تابع build_user وجود دارد تنظیم کنیم. از آنجا که فیلد email و پارامتر email نام یکسانی دارند، فقط نیاز داریم email بنویسیم، نه email: email.
ایجاد نمونهها از نمونههای دیگر با استفاده از نحو بهروزرسانی Struct
اغلب مفید است که یک instance جدید از یک struct ایجاد کنیم
که بیشتر مقادیر آن از یک instance دیگر با همان نوع گرفته شده باشد،
اما برخی از مقادیر آن تغییر کرده باشند.
برای انجام این کار میتوانید از syntax بهروزرسانی struct استفاده کنید.
ابتدا، در لیست ۵-۶ نشان داده شده است که چگونه میتوان یک نمونه جدید User در user2 ایجاد کرد، بدون استفاده از نحو بهروزرسانی. ما یک مقدار جدید برای email تنظیم میکنیم اما در غیر این صورت از همان مقادیر در user1 که قبلاً در لیست ۵-۲ ایجاد شده است، استفاده میکنیم.
struct User { active: bool, username: String, email: String, sign_in_count: u64, } fn main() { // --snip-- let user1 = User { email: String::from("[email protected]"), username: String::from("someusername123"), active: true, sign_in_count: 1, }; let user2 = User { active: user1.active, username: user1.username, email: String::from("[email protected]"), sign_in_count: user1.sign_in_count, }; }
User با استفاده از تمام مقادیر به جز یکی از user1با استفاده از نحو بهروزرسانی Struct، میتوانیم همان نتیجه را با کد کمتری به دست آوریم، همانطور که در لیست ۵-۷ نشان داده شده است. نحو .. مشخص میکند که فیلدهای باقیماندهای که به صورت صریح تنظیم نشدهاند باید همان مقادیری را داشته باشند که در نمونه داده شده هستند.
struct User { active: bool, username: String, email: String, sign_in_count: u64, } fn main() { // --snip-- let user1 = User { email: String::from("[email protected]"), username: String::from("someusername123"), active: true, sign_in_count: 1, }; let user2 = User { email: String::from("[email protected]"), ..user1 }; }
email برای یک نمونه User اما استفاده از مقادیر باقیمانده از user1کد در لیست ۵-۷ همچنین نمونهای در user2 ایجاد میکند که مقدار متفاوتی برای email دارد اما دارای مقادیر مشابهی برای فیلدهای username، active و sign_in_count از user1 است. ..user1 باید در انتها بیاید تا مشخص کند که فیلدهای باقیمانده باید مقادیر خود را از فیلدهای مربوطه در user1 دریافت کنند، اما میتوانیم مقادیر را برای هر تعداد فیلدی که میخواهیم به هر ترتیبی مشخص کنیم، بدون توجه به ترتیب فیلدها در تعریف ساختار.
توجه داشته باشید که نحو بهروزرسانی struct از = مانند عمل انتساب استفاده میکند؛
زیرا داده را منتقل میکند، همانطور که در بخش «تعامل متغیرها و دادهها با Move» دیدیم.
در این مثال، پس از ایجاد user2 دیگر نمیتوانیم از user1 استفاده کنیم
چون String موجود در فیلد username از user1 به user2 منتقل شده است.
اگر برای user2 مقادیر جدیدی از نوع String برای هر دو فیلد email و username مشخص کرده بودیم
و تنها از مقادیر active و sign_in_count از user1 استفاده کرده بودیم،
آنگاه user1 پس از ساختن user2 همچنان معتبر باقی میماند.
زیرا active و sign_in_count از نوعهایی هستند که Copy trait را پیادهسازی میکنند،
و بنابراین رفتاری که در بخش «دادههای فقط-پشته: Copy» توضیح دادیم، اعمال میشود.
در این مثال، همچنان میتوانیم از user1.email استفاده کنیم،
چون مقدار آن از user1 خارج نشده است.
استفاده از ساختارهای Tuple بدون فیلدهای نامگذاریشده برای ایجاد انواع مختلف
Rust همچنین از ساختارهایی که شبیه تاپلها هستند پشتیبانی میکند که به آنها ساختارهای Tuple میگویند. ساختارهای Tuple به دلیل نام ساختار معنای بیشتری دارند اما نامهایی برای فیلدهای خود ندارند؛ بلکه فقط نوع فیلدها را دارند. ساختارهای Tuple زمانی مفید هستند که بخواهید به کل تاپل یک نام بدهید و آن را به عنوان نوعی متفاوت از تاپلهای دیگر مشخص کنید، و وقتی نامگذاری هر فیلد مانند یک ساختار معمولی طولانی یا زائد باشد.
برای تعریف یک ساختار Tuple، با کلمه کلیدی struct و نام ساختار شروع کنید و سپس نوعهای موجود در تاپل را مشخص کنید. به عنوان مثال، در اینجا ما دو ساختار Tuple به نامهای Color و Point تعریف و استفاده کردهایم:
struct Color(i32, i32, i32); struct Point(i32, i32, i32); fn main() { let black = Color(0, 0, 0); let origin = Point(0, 0, 0); }
توجه داشته باشید که مقادیر black و origin از انواع متفاوتی هستند
چون آنها instanceهای دو tuple struct مختلفاند.
هر structای که تعریف میکنید، نوع خاص خود را دارد،
حتی اگر فیلدهای داخل آن struct نوعهای یکسانی داشته باشند.
برای مثال، یک تابع که پارامتری از نوع Color میگیرد،
نمیتواند یک Point را به عنوان آرگومان دریافت کند،
حتی اگر هر دو نوع از سه مقدار i32 تشکیل شده باشند.
به جز این مورد، tuple structها شبیه به tupleها هستند
از این جهت که میتوانید آنها را به اجزای منفردشان destructure کنید،
و با استفاده از . و اندیس، به مقدار خاصی دسترسی پیدا کنید.
برخلاف tupleها، tuple structها نیاز دارند که هنگام destructure کردن،
نام نوع struct را مشخص کنید.
برای مثال، برای destructure کردن مقادیر موجود در origin به متغیرهای x، y و z،
باید بنویسیم: let Point(x, y, z) = origin;
ساختارهای شبیه به Unit بدون هیچ فیلدی
شما همچنین میتوانید ساختارهایی تعریف کنید که هیچ فیلدی ندارند! اینها به عنوان ساختارهای شبیه Unit شناخته میشوند زیرا شبیه به نوع ()، نوع Unit، رفتار میکنند که در بخش «نوع Tuple» مورد اشاره قرار گرفت. ساختارهای شبیه Unit زمانی مفید هستند که نیاز به پیادهسازی یک ویژگی بر روی یک نوع داشته باشید اما هیچ دادهای برای ذخیره در خود نوع نداشته باشید. ما ویژگیها را در فصل ۱۰ بحث خواهیم کرد. در اینجا مثالی از اعلام و نمونهسازی یک ساختار شبیه Unit به نام AlwaysEqual آورده شده است:
struct AlwaysEqual; fn main() { let subject = AlwaysEqual; }
برای تعریف AlwaysEqual، از کلمه کلیدی struct، نام دلخواه و سپس یک نقطه ویرگول استفاده میکنیم. نیازی به آکولاد یا پرانتز نیست! سپس میتوانیم یک نمونه از AlwaysEqual را در متغیر subject با استفاده از همان نامی که تعریف کردهایم، بدون هیچ آکولاد یا پرانتزی دریافت کنیم. تصور کنید که در آینده رفتاری را برای این نوع پیادهسازی خواهیم کرد که همه نمونههای AlwaysEqual همیشه با تمام نمونههای دیگر برابر باشند، شاید برای داشتن نتیجهای مشخص برای اهداف آزمایشی. برای پیادهسازی آن رفتار نیازی به هیچ دادهای نداریم! شما در فصل ۱۰ خواهید دید که چگونه میتوانید ویژگیها را تعریف و آنها را بر روی هر نوعی، از جمله ساختارهای شبیه به Unit، پیادهسازی کنید.
مالکیت دادههای Struct
در تعریف ساختار User در لیست ۵-۱، ما از نوع مالک String به جای نوع برش رشته &str استفاده کردیم. این یک انتخاب عمدی است زیرا ما میخواهیم هر نمونه از این ساختار همه دادههای خود را مالک باشد و این دادهها به مدت زمانی که کل ساختار معتبر است، معتبر باقی بمانند.
همچنین ممکن است ساختارهایی وجود داشته باشند که به دادههای متعلق به چیز دیگری ارجاع میدهند، اما برای انجام این کار نیاز به استفاده از طول عمرها داریم، یک ویژگی از Rust که ما در فصل ۱۰ مورد بحث قرار خواهیم داد. طول عمرها اطمینان حاصل میکنند که دادههایی که توسط یک ساختار ارجاع داده شدهاند تا زمانی که ساختار معتبر است، معتبر باقی میمانند. بیایید بگوییم شما سعی دارید یک ارجاع را در یک ساختار ذخیره کنید بدون اینکه طول عمرها را مشخص کنید، مانند مثال زیر؛ این کار نخواهد کرد:
struct User {
active: bool,
username: &str,
email: &str,
sign_in_count: u64,
}
fn main() {
let user1 = User {
active: true,
username: "someusername123",
email: "[email protected]",
sign_in_count: 1,
};
}
کامپایلر شکایت خواهد کرد که به مشخصکنندههای طول عمر نیاز دارد:
$ cargo run
Compiling structs v0.1.0 (file:///projects/structs)
error[E0106]: missing lifetime specifier
--> src/main.rs:3:15
|
3 | username: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
1 ~ struct User<'a> {
2 | active: bool,
3 ~ username: &'a str,
|
error[E0106]: missing lifetime specifier
--> src/main.rs:4:12
|
4 | email: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
1 ~ struct User<'a> {
2 | active: bool,
3 | username: &str,
4 ~ email: &'a str,
|
For more information about this error, try `rustc --explain E0106`.
error: could not compile `structs` (bin "structs") due to 2 previous errors
در فصل ۱۰، ما بحث خواهیم کرد که چگونه این خطاها را برطرف کنید تا بتوانید ارجاعها را در ساختارها ذخیره کنید، اما در حال حاضر، ما این خطاها را با استفاده از انواع مالک مانند String به جای ارجاعها مانند &str برطرف خواهیم کرد.