การกำหนดนิยามและการสร้างตัวแทนโครงสร้างข้อมูล Struct (Defining and Instantiating Structs)
โครงสร้างข้อมูลแบบ Struct มีความคล้ายคลึงกับทูเพิล (tuples) ที่เราอธิบายกันไปในหัวข้อ “ชนิดข้อมูลแบบทูเพิล” ในแง่ที่ทั้งคู่ทำหน้าที่จัดเก็บบันทึกมูลค่าข้อมูลที่มีความเกี่ยวข้องกันหลายค่า และเช่นเดียวกับทูเพิล ข้อมูลแต่ละส่วนย่อยภายใน struct สามารถระบุเป็นชนิดข้อมูลชนิดที่แตกต่างกันออกไปได้ แต่ทว่าสิ่งที่ต่างออกไปจากทูเพิลคือ ใน struct คุณจะได้เป็นผู้กำหนดชื่อกำกับให้แก่ข้อมูลแต่ละส่วนย่อย เพื่อความชัดแจ้งของความหมายของข้อมูลเหล่านั้น การใส่ชื่อเรียกเหล่านี้ช่วยเพิ่มความยืดหยุ่นในการเขียนใช้งาน structs ยิ่งกว่าทูเพิล: กล่าวคือคุณไม่จำเป็นต้องพึ่งพาหรือจดจำลำดับก่อนหลังของข้อมูลเพื่อนำมาใช้อ้างอิงอ่านเขียนค่าภายในอ็อบเจกต์ตัวแทนเลย
กระบวนการกำหนดนิยาม struct จะเริ่มต้นสั่งการโดยระบุคำสำคัญ struct ตามด้วยชื่อหลักประจำตัวโครงสร้าง struct นั้นๆ โดยชื่อของ struct ควรบ่งชี้ถึงใจความสำคัญของกลุ่มชุดข้อมูลสารสนเทศที่นำมารวมกัน จากนั้นภายใต้ขอบเขตวงเล็บปีกกา เราจะทำประกาศนิยามชื่อระบุตัวแปรและประเภทชนิดข้อมูลย่อยของข้อมูลแต่ละรายการ ซึ่งจะเรียกว่า ฟิลด์ (fields) ยกตัวอย่างเช่น รายการที่ 5-1 แสดงนิยาม struct สำหรับบันทึกเก็บข้อมูลทั่วไปเกี่ยวกับบัญชีผู้ใช้งาน
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {}
การเรียกหยิบ struct ไปใช้งานภายหลังจากที่เราเขียนนิยามมันเสร็จเรียบร้อย จะใช้วิธีสั่งจัดสร้าง ตัวแทนอ็อบเจกต์ (instance) ของ struct นั้นๆ ผ่านการระบุป้อนค่าข้อมูลจริงเฉพาะเจาะจงให้ครบถ้วนในทุกๆ ฟิลด์ โดยเราสร้างตัวแทน instance ได้โดยระบุชื่อโครงสร้าง struct ตามด้วยวงเล็บปีกกาซึ่งบรรจุคู่ข้อมูล คีย์: ค่าข้อมูล (key: value) โดยคีย์ (keys) คือชื่อของฟิลด์ข้อมูล และค่าข้อมูล (values) คือชิ้นงานข้อมูลที่เราต้องการไปบันทึกเก็บจองไว้ในฟิลด์ตำแหน่งนั้นๆ ทั้งนี้เราไม่จำเป็นต้องเขียนระบุชื่อฟิลด์เรียงตามลำดับเดียวกับตอนประกาศนิยาม struct เสมอไป กล่าวอีกนัยหนึ่งคือ การเขียนนิยาม struct เปรียบเสมือนแบบร่างแม่พิมพ์ทั่วไป (template) ประจำชนิดข้อมูล และตัวแทน instance จะทำหน้าที่หยิบแม่พิมพ์นั้นไปเติมเต็มด้วยข้อมูลเฉพาะตัวเพื่อสร้างสรรค์มูลค่าของชนิดข้อมูลนั้นๆ ออกมาจริง ตัวอย่างเช่น เราสามารถประกาศสร้างตัวแทนข้อมูลบัญชีผู้ใช้งานคนหนึ่งได้ดังแสดงในรายการที่ 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("someone@example.com"),
sign_in_count: 1,
};
}
หากนักพัฒนาต้องการดึงอ่านข้อมูลเฉพาะฟิลด์ออกจาก struct จะใช้วิธีระบุเครื่องหมายจุด (dot notation) ตัวอย่างเช่น การเข้าถึงข้อมูลที่อยู่อีเมลของผู้ใช้รายนี้ จะเขียนเรียกใช้ผ่านประโยคคำสั่ง user1.email และหากตัวแทน instance นั้นถูกประทับตราระบุให้แก้ไขค่าข้อมูลได้ (mutable) เราจะสามารถป้อนเขียนข้อมูลทับแทนที่ค่าข้อมูลเดิมได้ง่ายๆ โดยระบุเครื่องหมายจุดโยงหาฟิลด์เป้าหมายแล้วสั่งเขียนส่งค่าขวาเข้าไปบันทึกทับในฟิลด์นั้น รายการที่ 5-3 แสดงตัวอย่างกระบวนการเข้าแก้ไขค่าข้อมูลในฟิลด์ 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("someone@example.com"),
sign_in_count: 1,
};
user1.email = String::from("anotheremail@example.com");
}
พึงสังเกตสเปกข้อกำหนดระบุว่า การตั้งค่าความสามารถในการแก้ไขตัวแปรนั้นจำเป็นต้องประกาศครอบคลุมสิทธิ์ทั้งตัวแทน instance โดยที่ภาษา Rust จะไม่อนุญาตให้เราทำเครื่องหมายล็อกสิทธิ์ความสามารถแก้ไขเฉพาะเจาะจงรายฟิลด์ (partial mutability) และเช่นเดียวกับโครงสร้างนิพจน์คำสั่งทั่วไป เราสามารถประกอบสร้างตัวแทน instance ตัวใหม่ของ struct เป็นนิพจน์รายการสุดท้ายในเนื้อหาฟังก์ชันเพื่อทำส่งคืนค่า (return) อ็อบเจกต์ตัวแทนตัวใหม่นั้นออกไปภายนอกโดยปริยายได้
รายการที่ 5-4 แสดงตัวอย่างฟังก์ชัน 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("someone@example.com"),
String::from("someusername123"),
);
}
การตั้งชื่อตัวแปรพารามิเตอร์ของฟังก์ชันให้มีสะกดสอดคล้องตรงตามชื่อของฟิลด์ภายใน struct ถือเป็นแนวทางปฏิบัติที่ดีและอ่านเข้าใจได้ลื่นไหล แต่อย่างไรก็ตามการที่ต้องมาคอยพิมพ์เขียนตัวแปรซ้ำสองรอบพ่วงในรูป email: email และ username: username จะแอบน่าเบื่อและเพิ่มภาระเขียนโค้ดโดยไม่จำเป็น หากในโครงสร้าง struct ของคุณมีฟิลด์สะสมปริมาณมากๆ การพิมพ์ประกบคู่ซ้ำเดิมจะยิ่งทวีความน่าปวดหัว โชคดีมากที่ Rust เตรียมไวยากรณ์ลดรูปแสนสะดวกมาช่วยตอบโจทย์นี้!
การใช้คำสั่งลดรูปประหยัดฟิลด์ (Using the Field Init Shorthand)
เนื่องจากข้อมูลชื่อพารามิเตอร์และชื่อฟิลด์เป้าหมายสะกดตรงกันพอดีในเนื้อตัวอย่างรายการที่ 5-4 เราจึงสามารถเลือกสวมหลักไวยากรณ์ ลดรูประบุสร้างฟิลด์ (field init shorthand) มาช่วยเขียนปรับแต่งนิยามโครงสร้างฟังก์ชัน build_user ใหม่ได้สำเร็จ ซึ่งจะช่วยสลัดข้อจำกัดของบรรทัดโค้ดซ้ำซากของตัวแปร username และ email ออกไปอย่างสมบูรณ์แบบโดยรักษาทิศทางการทำงานประมวลผลเบื้องหลังสอดคล้องกันตรงตามเดิมทุกประการ ดังแสดงตัวอย่างในรายการที่ 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("someone@example.com"),
String::from("someusername123"),
);
}
จากจุดตัวอย่างนี้ เรากำลังจัดสร้างตัวแทน instance ตัวใหม่ของ struct User ซึ่งมีฟิลด์ระบุชื่อว่า email โดยจุดประสงค์เราต้องการกำหนดให้ฟิลด์ email ดังกล่าวรับค่าข้อมูลตัวแปรมาจากพารามิเตอร์ email ของฟังก์ชัน build_user และเมื่อทั้งสองฝั่งใช้ชื่อเรียกที่สะกดเหมือนกันเป๊ะ ระบบจึงยินยอมให้เราเขียนระบุเพียงตัวแปร email ลอยเดี่ยวตัวเดียวได้ทันทีโดยละเครื่องหมายโคลอนพ่วงซ้ำคู่อย่าง email: email ออกไปได้เลย
การสร้างตัวแทนตัวใหม่ด้วยคำสั่งปรับเปลี่ยน Struct (Creating Instances with Struct Update Syntax)
ในหลายๆ กรณีของการออกแบบโปรเจกต์ มักจะเกิดความประสงค์ต้องการจัดสร้างตัวแทน instance ตัวใหม่ของ struct ซึ่งสเปกข้อมูลส่วนใหญ่นั้นลอกเลียนแบบยกเอามาจากข้อมูลตัวแทนตัวเก่าของชนิดประเภทเดียวกัน และต้องการปรับเปลี่ยนแก้ไขค่าข้อมูลใหม่เพิ่มเติมในบางรายการเท่านั้น คุณสามารถสั่งการลักษณะนี้ได้อย่างประหยัดโค้ดด้วยการใช้ หลักไวยากรณ์อัปเดต struct (struct update syntax)
ก่อนอื่น ในรายการที่ 5-6 เราจะแสดงทิศทางกระบวนการสร้างตัวแทนข้อมูล User ตัวใหม่เก็บไว้ในตัวแปร user2 ตามแนวทางมาตรฐานปกติทั่วไปโดยไม่ดึงสเปกอัปเดตย่อมาช่วยประมวลผล โดยเรากำหนดค่าอีเมลใหม่ให้ฟิลด์ email นอกเหนือจากนั้นจะลอกเอาประวัติข้อมูลตัวเดิมมาจากตัวแปร user1 ที่ตั้งขึ้นในรายการที่ 5-2 ทั้งหมด:
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
// --snip--
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("another@example.com"),
sign_in_count: user1.sign_in_count,
};
}
หากหันมาสวมหลักไวยากรณ์อัปเดต struct มาช่วยเขียน จะประหยัดความยาวบรรทัดโค้ดลงไปได้เยอะมากและให้ผลลัพธ์แบบเดียวกันดังแสดงในรายการที่ 5-7 โดยเครื่องหมายสัญลักษณ์จุดสองจุดคู่ .. จะเป็นตัวบอกเจตนาให้ระบบนำเอาเนื้อหาข้อมูลในฟิลด์ที่ไม่ได้เขียนป้อนค่าไว้ไปสแกนคัดลอกจากฟิลด์อ็อบเจกต์ตัวอ้างอิงต้นทางมาสวมประดับให้อัตโนมัติ:
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
// --snip--
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
let user2 = User {
email: String::from("another@example.com"),
..user1
};
}
ซอร์สโค้ดในรายการที่ 5-7 จะดำเนินการสร้างอ็อบเจกต์ตัวแทนใน user2 พร้อมป้อนค่าฟิลด์อีเมลใหม่ลงใน email แต่สลัดหน้าที่ป้อนค่าฟิลด์ username, active และ sign_in_count โดยดึงประวัติสำเนาของ user1 มาปักลงให้อัตโนมัติ โดยระเบียบข้อบังคับคือไวยากรณ์คำสั่ง ..user1 จำเป็นต้องเขียนระบุไว้เป็นลำดับสุดท้ายสุดของวงเล็บปีกกาเพื่อกำกับว่าฟิลด์ส่วนที่หลงเหลือที่ไม่ได้เขียนระบุค่าจะดึงค่ามาจากอ็อบเจกต์ตัวแทน user1 และเราสามารถสลับตำแหน่งหรือกำหนดฟิลด์ใหม่เพิ่มเติมปริมาณเท่าใดก็ทำได้ตามใจชอบ ไม่ต้องกังวลว่าลำดับเขียนโค้ดจะตรงตามพิมพ์เขียวตอนนิยาม struct หรือไม่
พึงตระหนักให้ลึกซึ้งว่าการเรียกสวมหลักไวยากรณ์อัปเดต struct นั้นจะใช้กลไกเครื่องหมายเท่ากับ = เช่นเดียวกับการป้อนค่าตัวแปรทั่วไป ซึ่งพฤติกรรมนี้จะทำให้ระบบเกิดการย้ายสิทธิ์การครอบครองหน่วยความจำ (move) ของชิ้นข้อมูลเป้าหมายอิงตามประเด็นความปลอดภัยที่เราเรียนรู้กันในหัวข้อ “ปฏิสัมพันธ์ระหว่างตัวแปรกับชิ้นข้อมูลในรูปแบบการย้ายสิทธิ์ (Move)” ในตัวอย่างโค้ดนี้ส่งผลให้เราไม่สามารถเขียนเรียกใช้งานตัวแปร user1 ได้อีกต่อไปภายหลังบรรทัดจัดตั้งตัวแทน user2 ไปแล้ว เนื่องจากมูลค่าชนิดข้อความ String ในฟิลด์ username ของตัวแปร user1 ได้ถูกย้ายสิทธิ์ (moved) ย้ายขาดเข้าไปสวมสิทธิ์ใน user2 ไปแล้วเรียบร้อย แต่หากเราจงใจเขียนกำหนดชนิดข้อความ String ตัวใหม่มอบให้แก่ฟิลด์ email และ username ของ user2 ทั้งคู่ และส่งผลให้มีการดึงเฉพาะสิทธิ์ฟิลด์ active และ sign_in_count มาคัดลอกโคลนใช้งานตามลำดับ ตัวแปร user1 จะยังคงรักษาความสมบูรณ์และเปิดใช้งานต่อยอดบรรทัดล่างได้ปกติ เนื่องจากประเภทข้อมูลของทั้ง active และ sign_in_count เป็นชนิดข้อมูลประเภทรองรับ Copy เทรต ซึ่งกลไกตัวระบบจะทำคัดลอกออโต้คัดลอกค่าให้ตามทิศทางพฤติกรรมที่เราอธิบายในหัวข้อ “ชิ้นข้อมูลประเภทจำกัดตัวเฉพาะสแตก: การทำก๊อปปี้ (Copy)” และเรายังสามารถเขียนเรียกใช้งานฟิลด์เฉพาะจุดอย่าง user1.email ได้ปกติเช่นกันเนื่องจากค่าข้อมูลตรงอีเมลชุดนั้นไม่ได้ถูกย้ายสิทธิ์หลุดพ้นไปจาก user1 ในระหว่างกระบวนการ
การกำหนดตั้งตัวแปรประเภทชนิดเฉพาะผ่านทูเพิลสตรักต์ (Creating Different Types with Tuple Structs)
Rust ยังรองรับโครงสร้างสตรักต์ที่มีรูปประโยคไวยากรณ์สอดคล้องคล้ายกับโครงสร้างทูเพิล เรียกว่า ทูเพิลสตรักต์ (tuple structs) โดยทูเพิลสตรักต์จะได้รับการแนบตราคุณค่าของความหมายตามชื่อที่ระบุตั้งให้ แต่จะละเว้นการระบุชื่อเรียกฟิลด์ภายในไปเหลือเพียงแค่การเขียนประกาศชนิดประเภทของฟิลด์เรียงต่อกันไปเฉยๆ ทูเพิลสตรักต์จะมีประโยชน์อย่างมากเมื่อนักพัฒนาต้องการจัดระบบโครงสร้างข้อมูลทูเพิลแบบรวมให้ได้รับการตั้งชื่อเฉพาะตัว และเพื่อให้ทูเพิลชุดนั้นถูกคอมไพเลอร์มองเป็นชนิดข้อมูลประเภทเฉพาะ (distinct type) ที่มีเอกเทศต่างจากกลุ่มทูเพิลทั่วไป และในกรณีที่การพยายามมานั่งนิยามชื่อฟิลด์แบบสตรักต์ทั่วไปจะดูรกรุงรังเกินความจำเป็น
การสร้างคำนิยามทูเพิลสตรักต์จะป้อนระบุโดยเริ่มจากคำสำคัญ struct ตามด้วยชื่อประเภทโครงสร้าง และเปิดวงเล็บระบุชนิดข้อมูลต่างๆ เรียงต่อกันไป ยกตัวอย่างเช่นตรงจุดนี้เราเขียนนิยามและเรียกใช้งานทูเพิลสตรักต์ 2 ประเภทชื่อว่า 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 จะนับแยกประเภทเป็นชนิดข้อมูลคนละชนิดกันอย่างถาวร (different types) เนื่องจากพวกมันเกิดมาจากการประกาศตัวแทนของทูเพิลสตรักต์คนละตัวกัน โครงสร้างสตรักต์ใดๆ ที่นักพัฒนานิยามขึ้นมาจะถูกคอมไพเลอร์มองเป็นชนิดข้อมูลเอกเทศเฉพาะตัวเสมอ แม้ว่าข้อมูลไส้ในฟิลด์ของสตรักต์เหล่านั้นจะประกาศประเภทชนิดข้อมูลตรงกันทุกสเปกก็ตาม เช่น ฟังก์ชันใดๆ ที่ออกแบบกำหนดให้รับพารามิเตอร์ชนิด Color จะปฏิเสธการสลับนำเอาตัวแปรชนิด Point ส่งผ่านเข้าไปเป็นอาร์กิวเมนต์เด็ดขาด แม้ว่าทั้งคู่จะมีองค์ประกอบย่อยเป็นชนิดตัวเลข i32 สามค่าเรียงต่อกันเหมือนกันก็ตาม ส่วนการหยิบใช้งานตัวแทนทูเพิลสตรักต์ส่วนอื่นจะมีพฤติกรรมสอดคล้องกับทูเพิลปกติทั่วไป กล่าวคือคุณสามารถเขียนกระจายโครงสร้างตัวแปร (destructure) แตกออกมาเป็นรายการเดี่ยวๆ หรือสามารถเขียนระบุเครื่องหมายจุด . ตามด้วยหมายเลขลำดับดัชนี (index) เพื่อเข้าอ่านข้อมูลรายตําแหน่งได้ แต่สิ่งที่แตกต่างจากทูเพิลคือ ทูเพิลสตรักต์ต้องการให้นักพัฒนาป้อนระบุชื่อประเภทชนิดของสตรักต์พ่วงไปด้วยยามต้องการสั่งกระจายโครงสร้างตัวแปร เช่น เราจะป้อนประโยคระบุเป็น let Point(x, y, z) = origin; เพื่อกระจายข้อมูลในพิกัดจุด origin มาเก็บแยกไว้ในตัวแปรแยกย่อยชื่อ x, y และ z ตามลำดับ
การนิยามสตรักต์แบบว่างเปล่า (Defining Unit-Like Structs)
คุณยังสามารถนิยามโครงสร้างสตรักต์ที่มีรูปแบบว่างเปล่าไม่มีฟิลด์บรรจุภายในเลยได้อีกด้วย! ซึ่งจะถูกขนานนามว่า ยูนิตสตรักต์ (unit-like structs) เนื่องจากพวกมันทำงานตอบสนองและทำตัวคล้ายกับชนิดข้อมูลยูนิต () ที่เราเอ่ยถึงในหัวข้อ “ชนิดข้อมูลแบบทูเพิล” โครงสร้างยูนิตสตรักต์จะมีประโยชน์อย่างมากเมื่อสภาวะโปรแกรมมีความจำเป็นต้องเข้าเขียนตราคุณสมบัติเทรต (trait) ครอบใส่ตัวแปรชนิดประเภทใดประเภทหนึ่ง แต่เราปราศจากความต้องการจดบันทึกเนื้อหาข้อมูลใดๆ ไว้ในตัวแปรชนิดประเภทนั้นเลย เราจะได้มาพูดคุยรายละเอียดลึกซึ้งเรื่องตราเทรตในบทที่ 10 ตัวอย่างการเขียนประโยคประกาศนิยามและสร้างตัวแทน instance ของยูนิตสตรักต์ชื่อ AlwaysEqual มีทิศทางดังนี้:
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
การสร้างรหัสนิยามของ AlwaysEqual ป้อนระบุโดยพิมพ์คำสำคัญ struct ตามด้วยชื่อที่ต้องการและตบท้ายคำสั่งด้วยเครื่องหมายอัฒภาค ; ทันที โดยปราศจากความจำเป็นต้องใส่วงเล็บปีกกาหรือวงเล็บกลมใดๆ พ่วงประกบ! จากนั้นตอนสั่งจัดสร้างตัวแทน instance มาเก็บในตัวแปร subject ก็ใช้วิธีเรียกผ่านชื่อที่เรากำหนดขึ้นเดี่ยวๆ โดยไม่ต้องเปิดประกบวงเล็บปีกกาหรือวงเล็บกลมเช่นกัน ให้นึกภาพสมมติว่าในอนาคตอันใกล้เราต้องการเขียนออกแบบพฤติกรรมเฉพาะให้แก่ชนิดประเภทนี้ เพื่อกำหนดให้ตัวแทนของ AlwaysEqual ทุกๆ ตัวมีค่าเปรียบเทียบเท่ากับอ็อบเจกต์ชนิดประเภทอื่นใดก็ได้เสมอ เช่น เพื่อประโยชน์ในการทำสถานะรายงานผลการทดสอบระบบ (testing) ซึ่งกระบวนการประกาศพฤติกรรมลักษณะนี้เราไม่จำเป็นต้องนำเนื้อหาข้อมูลฟิลด์ใดๆ มาร่วมทำงานเลย คุณจะรับทราบกระบวนการสเปกตราเทรตและขั้นตอนการนำไปประดับใส่ให้แก่ชนิดประเภทข้อมูลย่อยต่างๆ รวมถึงยูนิตสตรักต์ลักษณะนี้ได้ในบทที่ 10 ครับ
สิทธิ์การครอบครองและโครงสร้างข้อมูลภายใน Struct (Ownership of Struct Data)
ในซอร์สโค้ดคำนิยาม struct
Userประจำรายการที่ 5-1 เราจงใจเลือกใช้ชนิดประเภทข้อมูลที่ถือสิทธิ์ครอบครองเนื้อความได้เองอย่างชนิดStringแทนชนิดข้อมูลส่วนแบ่งข้อความประเภท&strสิ่งนี้ถือเป็นการตัดสินใจออกแบบโครงสร้างโปรเจกต์ที่สอดคล้องกับเจตจำนงของเรา เนื่องจากเราต้องการให้ตัวแทน instance แต่ละตัวของ struct มีสิทธิ์ขาดครอบครองข้อมูลภายในของตนเองอย่างสมบูรณ์ และทำให้ข้อมูลเหล่านั้นคงอยู่และถูกต้องตราบเท่าที่สตรักต์ตัวแม่ขอบเขตหลักยังมีสถานะทำงานอยู่นอกจากนี้ โครงสร้าง structs ยังสามารถรับรองการเก็บตัวแปรอ้างอิงชี้ไปยังชิ้นข้อมูลของตัวแปรอื่นข้างนอกระบบได้เช่นกัน แต่ทว่าจำเป็นต้องนำความสามารถเรื่อง อายุขัยช่วงเวลาครอบครอง (lifetimes) ซึ่งเป็นเครื่องมือพิเศษของ Rust ที่เราจะพูดคุยในบทที่ 10 มาช่วยประคองคุมพฤติกรรม ระบบ lifetimes จะช่วยการันตีค้ำสิทธิ์ความปลอดภัยให้มั่นใจได้ว่า ข้อมูลอ้างอิงภายนอกที่ struct เรียกใช้ จะยังคงสถานะสมบูรณ์ตราบเท่าอายุขัยของสตรักต์ตัวแม่นั้นยังไม่มอดม้วย สมมติว่าหากคุณฝืนป้อนพิมพ์รหัสตัวแปรอ้างอิงใส่ในสตรักต์โดยหลีกเลี่ยงการเขียนสเปก lifetimes ดังตัวอย่างในโค้ด src/main.rs ด้านล่างนี้ คอมไพเลอร์ Rust จะไม่อนุญาตให้รันผ่าน:
struct User { active: bool, username: &str, email: &str, sign_in_count: u64, } fn main() { let user1 = User { active: true, username: "someusername123", email: "someone@example.com", sign_in_count: 1, }; }ตัวคอมไพเลอร์จะส่งเสียงแจ้งเตือนสกัดบ่นรายงานความต้องการกำกับตรา lifetime specifiers:
$ 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 เราจะได้เรียนรู้วิธีการแก้ไขปรับแก้โค้ดเพื่อปลดล็อกข้อกังวลข้อผิดพลาดรายงานลักษณะนี้ ทำให้สามารถเก็บตัวแปรอ้างอิงชี้หาข้อมูลไว้ใน structs ได้อย่างปลอดภัย แต่สำหรับเนื้อหาในช่วงต้นนี้ เราจะเลือกตัดปัญหาหลีกเลี่ยงข้อผิดพลาดโดยเปลี่ยนมาเรียกใช้งานประเภทชนิดที่ครอบครอบกรรมสิทธิ์เองได้เช่น
Stringแทนการประยุกต์ใช้งานตัวแปรอ้างอิง&strไปก่อนครับ