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