การประกาศและสร้าง Struct
Struct คล้ายกับ tuple ที่พูดถึงในส่วน “Tuple Type” ตรงที่ทั้งคู่เก็บค่าหลายค่าที่เกี่ยวข้องกัน เช่นเดียวกับ tuple ชิ้นของ struct เป็น type ต่างกันได้ ต่างจาก tuple ใน struct คุณตั้งชื่อแต่ละชิ้น ข้อมูล จึงชัดเจนว่าค่าหมายถึงอะไร การเพิ่มชื่อเหล่านี้หมายความว่า struct ยืดหยุ่นกว่า tuple — คุณไม่ต้องพึ่งลำดับของข้อมูลเพื่อระบุหรือเข้าถึงค่า ของ instance
ในการประกาศ struct เราป้อน keyword struct แล้วตั้งชื่อ struct ทั้งหมด
ชื่อของ struct ควรอธิบายความสำคัญของชิ้นข้อมูลที่จัดกลุ่มเข้าด้วยกัน
จากนั้น ภายใน curly bracket เราประกาศชื่อและ type ของชิ้นข้อมูล ซึ่งเรา
เรียกว่า field เช่น Listing 5-1 แสดง struct ที่เก็บข้อมูลเกี่ยวกับ
บัญชีผู้ใช้
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {}
Userในการใช้ struct หลังประกาศแล้ว เราสร้าง instance ของ struct นั้นโดย
ระบุค่าที่เป็นรูปธรรมให้แต่ละ field เราสร้าง instance โดยระบุชื่อ struct
แล้วเพิ่ม curly bracket ที่มีคู่ key: value โดย key คือชื่อ field และ
value คือข้อมูลที่อยากเก็บใน field เหล่านั้น เราไม่ต้องระบุ field ในลำดับ
เดียวกับที่ประกาศใน struct พูดอีกอย่าง การประกาศ struct เหมือน template
ทั่วไปสำหรับ type และ instance เติม template นั้นด้วยข้อมูลเฉพาะเพื่อสร้าง
ค่าของ type เช่น เราประกาศผู้ใช้คนหนึ่งได้ตามที่แสดงใน Listing 5-2
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ในการได้ค่าเฉพาะจาก struct เราใช้ dot notation เช่น ในการเข้าถึง email
ของผู้ใช้นี้ เราใช้ user1.email ถ้า instance เป็น mutable เราเปลี่ยน
ค่าได้โดยใช้ dot notation และ assign ลงใน field เฉพาะ Listing 5-3 แสดง
วิธีเปลี่ยนค่าใน field email ของ instance User ที่ mutable
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 ของ instance Userหมายเหตุว่าทั้ง instance ต้องเป็น mutable — Rust ไม่ให้เรา mark แค่บาง field เป็น mutable เช่นเดียวกับ expression ใด ๆ เราสร้าง instance ใหม่ ของ struct เป็น expression สุดท้ายใน body ของฟังก์ชันได้ เพื่อ implicitly return instance ใหม่นั้น
Listing 5-4 แสดงฟังก์ชัน build_user ที่ return instance User ด้วย
email และ username ที่ให้มา field 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 ที่รับ email และ username แล้ว return instance Userมันสมเหตุสมผลที่จะตั้งชื่อ parameter ของฟังก์ชันให้เหมือนกับชื่อ field
ของ struct แต่การต้องเขียนชื่อ field email และ username และตัวแปร
ซ้ำน่าเบื่อนิดหน่อย ถ้า struct มี field มากขึ้น การเขียนชื่อแต่ละตัวซ้ำ
จะยิ่งน่ารำคาญ โชคดีมี shorthand ที่สะดวก!
ใช้ Field Init Shorthand
เพราะชื่อ parameter และชื่อ field ของ struct เหมือนกันเป๊ะใน Listing 5-4
เราใช้ syntax field init shorthand เขียน build_user ใหม่ได้ ให้มัน
ทำงานเหมือนกันเป๊ะ แต่ไม่มีการเขียน username และ email ซ้ำ ดังที่
แสดงใน Listing 5-5
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 ที่ใช้ field init shorthand เพราะ parameter username และ email มีชื่อเหมือน field ของ structที่นี่ เรากำลังสร้าง instance ใหม่ของ struct User ซึ่งมี field ชื่อ
email เราอยาก set ค่าของ field email เป็นค่าใน parameter email ของ
ฟังก์ชัน build_user เพราะ field email และ parameter email มีชื่อ
เหมือนกัน เราเขียนแค่ email แทน email: email
สร้าง Instance ด้วย Struct Update Syntax
บ่อยครั้งที่มีประโยชน์ในการสร้าง instance ใหม่ของ struct ที่มีค่าส่วนใหญ่ จาก instance อื่นของ type เดียวกัน แต่เปลี่ยนบางค่า คุณทำได้โดยใช้ struct update syntax
ขั้นแรก ใน Listing 5-6 เราแสดงวิธีสร้าง instance User ใหม่ใน user2
ในแบบปกติ โดยไม่มี update syntax เรา set ค่าใหม่สำหรับ email แต่ใช้ค่า
เดิมจาก user1 ที่เราสร้างใน Listing 5-2 สำหรับที่เหลือ
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 update syntax เราได้ผลแบบเดียวกันด้วยโค้ดน้อยกว่า ดังที่แสดง
ใน Listing 5-7 syntax .. ระบุว่า field ที่เหลือที่ไม่ set แบบ explicit
ควรมีค่าเหมือน field ใน instance ที่ให้
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 ใหม่สำหรับ instance User แต่ใช้ค่าที่เหลือจาก user1โค้ดใน Listing 5-7 ก็สร้าง instance ใน user2 ที่มีค่าต่างสำหรับ email
แต่มีค่าเหมือนสำหรับ field username, active และ sign_in_count จาก
user1 ..user1 ต้องมาท้ายสุด เพื่อระบุว่า field ที่เหลือใด ๆ ควรได้ค่า
จาก field ที่สอดคล้องใน user1 แต่เราเลือกระบุค่าสำหรับ field กี่ตัวก็ได้
ในลำดับใดก็ได้ ไม่ว่าลำดับของ field ใน struct จะเป็นอย่างไร
หมายเหตุว่า struct update syntax ใช้ = เหมือน assignment — นี่เพราะมัน
move ข้อมูล เหมือนที่เราเห็นในส่วน
“ตัวแปรและข้อมูลโต้ตอบกันด้วย Move” ในตัวอย่างนี้
เราใช้ user1 ไม่ได้แล้วหลังสร้าง user2 เพราะ String ใน field
username ของ user1 ถูก move เข้า user2 ถ้าเราให้ user2 ค่า
String ใหม่ทั้ง email และ username และจึงใช้แค่ค่า active และ
sign_in_count จาก user1 user1 จะยัง valid หลังสร้าง user2 ทั้ง
active และ sign_in_count เป็น type ที่ implement trait Copy ดังนั้น
พฤติกรรมที่เราพูดถึงในส่วน
“ข้อมูลที่อยู่บน Stack เท่านั้น: Copy” ใช้ได้ เรา
ยังใช้ user1.email ในตัวอย่างนี้ได้ เพราะค่าของมันไม่ถูก move ออกจาก
user1
สร้าง Type ต่างกันด้วย Tuple Struct
Rust ยังรองรับ struct ที่ดูคล้าย tuple เรียกว่า tuple struct Tuple struct มีความหมายเพิ่มที่ชื่อ struct ให้ แต่ไม่มีชื่อผูกกับ field — มี แค่ type ของ field Tuple struct มีประโยชน์เมื่อคุณอยากให้ทั้ง tuple มี ชื่อ และทำให้ tuple เป็น type ต่างจาก tuple อื่น และเมื่อการตั้งชื่อแต่ละ field เหมือน struct ปกติจะยาวหรือซ้ำ
ในการประกาศ tuple struct เริ่มด้วย keyword struct และชื่อ struct ตาม
ด้วย type ใน tuple เช่น ที่นี่เราประกาศและใช้ tuple struct สองตัวชื่อ
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 เป็น type ต่างกัน เพราะเป็น instance
ของ tuple struct ต่างกัน แต่ละ struct ที่คุณประกาศเป็น type ของตัวมันเอง
แม้ field ภายใน struct อาจมี type เดียวกัน เช่น ฟังก์ชันที่รับ parameter
type Color รับ Point เป็น argument ไม่ได้ แม้ทั้งสอง type ประกอบจาก
ค่า i32 สามตัว ไม่อย่างนั้น instance ของ tuple struct คล้าย tuple ตรงที่
คุณ destructure เป็นชิ้น ๆ ได้ และใช้ . ตามด้วย index เพื่อเข้าถึงค่า
แต่ละค่าได้ ต่างจาก tuple ตรงที่ tuple struct บังคับให้คุณตั้งชื่อ type
ของ struct เมื่อ destructure เช่น เราเขียน let Point(x, y, z) = origin;
เพื่อ destructure ค่าใน origin point เข้าตัวแปรชื่อ x, y และ z
ประกาศ Unit-Like Struct
คุณยังประกาศ struct ที่ไม่มี field ใด ๆ ได้! เหล่านี้เรียกว่า unit-like
struct เพราะพฤติกรรมคล้าย () ที่เป็น unit type ที่เราเอ่ยใน
“Tuple Type” Unit-like struct มีประโยชน์เมื่อ
คุณต้อง implement trait บน type บางตัว แต่ไม่มีข้อมูลที่อยากเก็บใน type
เอง เราจะพูดถึง trait ในบทที่ 10 นี่คือตัวอย่างการประกาศและสร้าง
instance ของ unit struct ชื่อ AlwaysEqual:
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
ในการประกาศ AlwaysEqual เราใช้ keyword struct ชื่อที่อยากได้ แล้ว
semicolon ไม่ต้องใช้ curly bracket หรือวงเล็บ! จากนั้นเราได้ instance
ของ AlwaysEqual ในตัวแปร subject แบบเดียวกัน — ใช้ชื่อที่เราประกาศ
ไว้ โดยไม่มี curly bracket หรือวงเล็บ ลองนึกว่าทีหลังเราจะ implement
พฤติกรรมสำหรับ type นี้ ว่าทุก instance ของ AlwaysEqual เท่ากับทุก
instance ของ type อื่นใดเสมอ บางทีเพื่อมีผลที่รู้สำหรับจุดประสงค์การ
ทดสอบ เราไม่ต้องการข้อมูลใด ๆ เพื่อ implement พฤติกรรมนั้น! คุณจะเห็น
ในบทที่ 10 วิธีประกาศ trait และ implement บน type ใด ๆ รวมถึง unit-like
struct
Ownership ของข้อมูล Struct
ในการประกาศ struct User ใน Listing 5-1 เราใช้ type String ที่ owned
แทน type &str ที่เป็น string slice นี่เป็นตัวเลือกที่ตั้งใจ เพราะเรา
อยากให้แต่ละ instance ของ struct นี้ own ข้อมูลทั้งหมดของมัน และให้ข้อมูล
นั้น valid ตราบที่ทั้ง struct valid
เป็นไปได้ที่ struct จะเก็บ reference ของข้อมูลที่ owned โดยอย่างอื่น แต่การทำเช่นนั้นต้องใช้ lifetime ฟีเจอร์ของ Rust ที่เราจะพูดถึงในบทที่ 10 Lifetime รับประกันว่าข้อมูลที่ struct อ้างถึง valid ตราบที่ struct valid ลองว่าคุณพยายามเก็บ reference ใน struct โดยไม่ระบุ lifetime ดัง ต่อไปนี้ใน 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,
};
}
Compiler จะบ่นว่าต้อง lifetime specifier:
$ 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
ในบทที่ 10 เราจะพูดถึงวิธีแก้ error เหล่านี้ เพื่อให้คุณเก็บ reference
ใน struct ได้ แต่ตอนนี้เราจะแก้ error แบบนี้โดยใช้ type ที่ owned อย่าง
String แทน reference อย่าง &str