تعریف و نمونهسازی Structها
ساختارها مشابه تاپلها هستند که در بخش «نوع Tuple» مورد بحث قرار گرفتند، به این معنا که هر دو شامل مقادیر مرتبط متعددی هستند. مانند تاپلها، اجزای یک ساختار میتوانند از انواع مختلفی باشند. اما برخلاف تاپلها، در یک ساختار شما برای هر جزء داده نام تعیین میکنید تا معنای مقادیر روشنتر شود. افزودن این نامها باعث میشود که ساختارها از تاپلها انعطافپذیرتر باشند: شما مجبور نیستید برای مشخص کردن یا دسترسی به مقادیر یک نمونه به ترتیب دادهها تکیه کنید.
برای تعریف یک ساختار، کلمه کلیدی struct
را وارد کرده و نام کل ساختار را تعیین میکنیم. نام یک ساختار باید توصیفکننده اهمیت اجزای دادهای باشد که با هم گروهبندی میشوند. سپس، داخل آکولادها، نامها و انواع اجزای دادهای را که به آنها فیلد میگوییم، تعریف میکنیم. برای مثال، لیست ۵-۱ یک ساختار را نشان میدهد که اطلاعات مربوط به یک حساب کاربری را ذخیره میکند.
struct User { active: bool, username: String, email: String, sign_in_count: u64, } fn main() {}
User
برای استفاده از یک ساختار پس از تعریف آن، ما یک نمونه از آن ساختار ایجاد میکنیم که مقادیر مشخصی را برای هر یک از فیلدها مشخص میکند. ما نمونهای را با تعیین نام ساختار و سپس اضافه کردن آکولادهایی که شامل زوجهای کلید: مقدار هستند، ایجاد میکنیم، جایی که کلیدها نام فیلدها و مقادیر دادهای هستند که میخواهیم در آن فیلدها ذخیره کنیم. نیازی نیست که فیلدها را به همان ترتیبی که در ساختار تعریف شدهاند، مشخص کنیم. به عبارت دیگر، تعریف ساختار مانند یک قالب کلی برای نوع است و نمونهها این قالب را با دادههای خاص پر میکنند تا مقادیر آن نوع را ایجاد کنند. برای مثال، میتوانیم کاربر خاصی را همانطور که در لیست ۵-۲ نشان داده شده است، تعریف کنیم.
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
اغلب مفید است که یک نمونه جدید از یک ساختار ایجاد کنیم که شامل اکثر مقادیر از یک نمونه دیگر است، اما برخی از آنها تغییر کردهاند. شما میتوانید این کار را با استفاده از نحو بهروزرسانی 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 از =
مانند یک عملگر انتساب استفاده میکند؛ این به این دلیل است که دادهها را جابهجا میکند، همانطور که در بخش «تعامل متغیرها و دادهها با انتقال» مورد بحث قرار گرفت. در این مثال، دیگر نمیتوانیم از user1
به عنوان یک کل پس از ایجاد user2
استفاده کنیم، زیرا String
در فیلد username
از user1
به user2
منتقل شد. اگر ما به user2
مقادیر جدید String
برای هر دو email
و username
داده بودیم و بنابراین فقط از مقادیر active
و sign_in_count
از user1
استفاده کرده بودیم، user1
پس از ایجاد user2
همچنان معتبر باقی میماند. هم active
و هم sign_in_count
از انواعی هستند که ویژگی Copy
را پیادهسازی میکنند، بنابراین رفتار مورد بحث در بخش «دادههای فقط روی پشته: Copy» اعمال میشود. در این مثال، همچنان میتوانیم از user1.email
استفاده کنیم، زیرا مقدار آن منتقل نشده است.
استفاده از ساختارهای 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
انواع متفاوتی دارند زیرا آنها نمونههایی از ساختارهای Tuple متفاوت هستند. هر ساختاری که تعریف میکنید نوع خودش را دارد، حتی اگر فیلدهای درون ساختار نوع یکسانی داشته باشند. برای مثال، یک تابع که پارامتری از نوع Color
میگیرد نمیتواند یک Point
را به عنوان آرگومان بگیرد، حتی اگر هر دو نوع از سه مقدار i32
تشکیل شده باشند. در غیر این صورت، نمونههای ساختار Tuple مشابه تاپلها هستند به این معنا که میتوانید آنها را به اجزای فردی تجزیه کنید و میتوانید از یک .
به همراه ایندکس برای دسترسی به مقدار خاصی استفاده کنید. برخلاف تاپلها، ساختارهای Tuple نیاز دارند که هنگام تجزیه آنها نوع ساختار را مشخص کنید. برای مثال، میتوانیم بنویسیم let Point(x, y, z) = point
.
ساختارهای شبیه به 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
برطرف خواهیم کرد.