تعریف و نمونه‌سازی Structها

ساختارها مشابه تاپل‌ها هستند که در بخش «نوع Tuple» مورد بحث قرار گرفتند، به این معنا که هر دو شامل مقادیر مرتبط متعددی هستند. مانند تاپل‌ها، اجزای یک ساختار می‌توانند از انواع مختلفی باشند. اما برخلاف تاپل‌ها، در یک ساختار شما برای هر جزء داده نام تعیین می‌کنید تا معنای مقادیر روشن‌تر شود. افزودن این نام‌ها باعث می‌شود که ساختارها از تاپل‌ها انعطاف‌پذیرتر باشند: شما مجبور نیستید برای مشخص کردن یا دسترسی به مقادیر یک نمونه به ترتیب داده‌ها تکیه کنید.

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

Filename: src/main.rs
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {}
Listing 5-1: تعریف یک ساختار User

برای استفاده از یک ساختار پس از تعریف آن، ما یک نمونه از آن ساختار ایجاد می‌کنیم که مقادیر مشخصی را برای هر یک از فیلدها مشخص می‌کند. ما نمونه‌ای را با تعیین نام ساختار و سپس اضافه کردن آکولادهایی که شامل زوج‌های کلید: مقدار هستند، ایجاد می‌کنیم، جایی که کلیدها نام فیلدها و مقادیر داده‌ای هستند که می‌خواهیم در آن فیلدها ذخیره کنیم. نیازی نیست که فیلدها را به همان ترتیبی که در ساختار تعریف شده‌اند، مشخص کنیم. به عبارت دیگر، تعریف ساختار مانند یک قالب کلی برای نوع است و نمونه‌ها این قالب را با داده‌های خاص پر می‌کنند تا مقادیر آن نوع را ایجاد کنند. برای مثال، می‌توانیم کاربر خاصی را همان‌طور که در لیست ۵-۲ نشان داده شده است، تعریف کنیم.

Filename: src/main.rs
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,
    };
}
Listing 5-2: ایجاد یک نمونه از ساختار User

برای به‌دست‌آوردن مقدار خاصی از یک ساختار، از نشانه‌گذاری نقطه استفاده می‌کنیم. به عنوان مثال، برای دسترسی به آدرس ایمیل این کاربر، از user1.email استفاده می‌کنیم. اگر نمونه قابل تغییر باشد، می‌توانیم مقدار را با استفاده از نشانه‌گذاری نقطه تغییر داده و در یک فیلد خاص مقداردهی کنیم. لیست ۵-۳ نشان می‌دهد که چگونه مقدار در فیلد email یک نمونه قابل تغییر User را تغییر دهیم.

Filename: src/main.rs
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]");
}
Listing 5-3: تغییر مقدار در فیلد email یک نمونه User

توجه داشته باشید که کل نمونه باید قابل تغییر باشد؛ Rust به ما اجازه نمی‌دهد که فقط برخی از فیلدها را به صورت قابل تغییر علامت‌گذاری کنیم. مانند هر عبارت دیگری، می‌توانیم یک نمونه جدید از ساختار را به عنوان آخرین عبارت در بدنه یک تابع بسازیم تا به طور ضمنی آن نمونه جدید را بازگردانیم.

لیست ۵-۴ یک تابع build_user را نشان می‌دهد که یک نمونه از User را با ایمیل و نام کاربری مشخص برمی‌گرداند. فیلد active مقدار true می‌گیرد و sign_in_count مقدار 1 دریافت می‌کند.

Filename: src/main.rs
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"),
    );
}
Listing 5-4: یک تابع build_user که یک ایمیل و نام کاربری می‌گیرد و یک نمونه User را بازمی‌گرداند

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

استفاده از میانبر مقداردهی فیلد

از آنجا که نام پارامترها و نام فیلدهای ساختار دقیقاً یکسان هستند، می‌توانیم از نحو میانبر مقداردهی فیلد برای بازنویسی build_user استفاده کنیم تا همان رفتار را داشته باشد اما تکرار username و email را نداشته باشد، همان‌طور که در لیست ۵-۵ نشان داده شده است.

Filename: src/main.rs
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"),
    );
}
Listing 5-5: یک تابع build_user که از میانبر مقداردهی فیلد استفاده می‌کند زیرا پارامترهای username و email همان نام فیلدهای ساختار را دارند

اینجا، ما یک نمونه جدید از ساختار User می‌سازیم که فیلدی به نام email دارد. ما می‌خواهیم مقدار فیلد email را به مقداری که در پارامتر email تابع build_user وجود دارد تنظیم کنیم. از آنجا که فیلد email و پارامتر email نام یکسانی دارند، فقط نیاز داریم email بنویسیم، نه email: email.

ایجاد نمونه‌ها از نمونه‌های دیگر با استفاده از نحو به‌روزرسانی Struct

اغلب مفید است که یک نمونه جدید از یک ساختار ایجاد کنیم که شامل اکثر مقادیر از یک نمونه دیگر است، اما برخی از آن‌ها تغییر کرده‌اند. شما می‌توانید این کار را با استفاده از نحو به‌روزرسانی Struct انجام دهید.

ابتدا، در لیست ۵-۶ نشان داده شده است که چگونه می‌توان یک نمونه جدید User در user2 ایجاد کرد، بدون استفاده از نحو به‌روزرسانی. ما یک مقدار جدید برای email تنظیم می‌کنیم اما در غیر این صورت از همان مقادیر در user1 که قبلاً در لیست ۵-۲ ایجاد شده است، استفاده می‌کنیم.

Filename: src/main.rs
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,
    };
}
Listing 5-6: ایجاد یک نمونه جدید User با استفاده از تمام مقادیر به جز یکی از user1

با استفاده از نحو به‌روزرسانی Struct، می‌توانیم همان نتیجه را با کد کمتری به دست آوریم، همان‌طور که در لیست ۵-۷ نشان داده شده است. نحو .. مشخص می‌کند که فیلدهای باقی‌مانده‌ای که به صورت صریح تنظیم نشده‌اند باید همان مقادیری را داشته باشند که در نمونه داده شده هستند.

Filename: src/main.rs
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
    };
}
Listing 5-7: استفاده از نحو به‌روزرسانی Struct برای تنظیم یک مقدار جدید 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 تعریف و استفاده کرده‌ایم:

Filename: src/main.rs
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 آورده شده است:

Filename: src/main.rs
struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

برای تعریف AlwaysEqual، از کلمه کلیدی struct، نام دلخواه و سپس یک نقطه ویرگول استفاده می‌کنیم. نیازی به آکولاد یا پرانتز نیست! سپس می‌توانیم یک نمونه از AlwaysEqual را در متغیر subject با استفاده از همان نامی که تعریف کرده‌ایم، بدون هیچ آکولاد یا پرانتزی دریافت کنیم. تصور کنید که در آینده رفتاری را برای این نوع پیاده‌سازی خواهیم کرد که همه نمونه‌های AlwaysEqual همیشه با تمام نمونه‌های دیگر برابر باشند، شاید برای داشتن نتیجه‌ای مشخص برای اهداف آزمایشی. برای پیاده‌سازی آن رفتار نیازی به هیچ داده‌ای نداریم! شما در فصل ۱۰ خواهید دید که چگونه می‌توانید ویژگی‌ها را تعریف و آن‌ها را بر روی هر نوعی، از جمله ساختارهای شبیه به Unit، پیاده‌سازی کنید.

مالکیت داده‌های Struct

در تعریف ساختار User در لیست ۵-۱، ما از نوع مالک String به جای نوع برش رشته &str استفاده کردیم. این یک انتخاب عمدی است زیرا ما می‌خواهیم هر نمونه از این ساختار همه داده‌های خود را مالک باشد و این داده‌ها به مدت زمانی که کل ساختار معتبر است، معتبر باقی بمانند.

همچنین ممکن است ساختارهایی وجود داشته باشند که به داده‌های متعلق به چیز دیگری ارجاع می‌دهند، اما برای انجام این کار نیاز به استفاده از طول عمر‌ها داریم، یک ویژگی از Rust که ما در فصل ۱۰ مورد بحث قرار خواهیم داد. طول عمرها اطمینان حاصل می‌کنند که داده‌هایی که توسط یک ساختار ارجاع داده شده‌اند تا زمانی که ساختار معتبر است، معتبر باقی می‌مانند. بیایید بگوییم شما سعی دارید یک ارجاع را در یک ساختار ذخیره کنید بدون اینکه طول عمرها را مشخص کنید، مانند مثال زیر؛ این کار نخواهد کرد:

Filename: src/main.rs
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 برطرف خواهیم کرد.