Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ตรวจสอบ Reference ด้วย Lifetime

Lifetime เป็น generic อีกชนิดที่เราใช้มาแล้ว แทนการรับประกันว่า type มี พฤติกรรมที่เราอยาก lifetime รับประกันว่า reference valid ตราบที่เราต้อง การ

รายละเอียดหนึ่งที่เราไม่พูดถึงในส่วน “Reference และ Borrowing” ใน บทที่ 4 คือทุก reference ใน Rust มี lifetime ซึ่งคือ scope ที่ reference นั้น valid โดยปกติ lifetime เป็น implicit และ infer เช่นเดียวกับโดยปกติ type ถูก infer เราถูกบังคับให้ annotate type แค่เมื่อ type หลายตัวเป็นไป ได้ ในแบบคล้ายกัน เราต้อง annotate lifetime เมื่อ lifetime ของ reference สัมพันธ์กันได้ในไม่กี่แบบ Rust บังคับให้เรา annotate ความสัมพันธ์โดยใช้ generic lifetime parameter เพื่อรับประกันว่า reference จริงที่ใช้ตอน runtime จะ valid แน่นอน

การ annotate lifetime ไม่ใช่แม้แต่แนวคิดที่ภาษาโปรแกรมอื่นส่วนใหญ่มี นี่จึงจะรู้สึกไม่คุ้นเคย แม้เราจะไม่ครอบคลุม lifetime ทั้งหมดในบทนี้ เรา จะพูดถึงวิธีที่ใช้บ่อยที่คุณอาจเจอ syntax lifetime เพื่อให้คุณคุ้นเคยกับ แนวคิด

Dangling Reference

จุดประสงค์หลักของ lifetime คือป้องกัน dangling reference ซึ่ง ถ้าอนุญาต ให้มีอยู่ จะทำให้โปรแกรมอ้างถึงข้อมูลอื่นนอกจากข้อมูลที่ตั้งใจอ้าง พิจารณาโปรแกรมใน Listing 10-16 ซึ่งมี outer scope และ inner scope

fn main() {
    let r;

    {
        let x = 5;
        r = &x;
    }

    println!("r: {r}");
}
Listing 10-16: พยายามใช้ reference ที่ค่าออกจาก scope

หมายเหตุ: ตัวอย่างใน Listing 10-16, 10-17 และ 10-23 ประกาศตัวแปรโดยไม่ ให้ค่าเริ่มต้น ดังนั้นชื่อตัวแปรมีอยู่ใน outer scope แวบแรก นี่อาจดู ขัดกับการที่ Rust ไม่มีค่า null อย่างไรก็ตาม ถ้าเราลองใช้ตัวแปรก่อนให้ ค่ามัน เราจะได้ compile-time error ซึ่งแสดงว่า Rust ไม่อนุญาตค่า null จริง

Outer scope ประกาศตัวแปรชื่อ r โดยไม่มีค่าเริ่มต้น และ inner scope ประกาศตัวแปรชื่อ x ด้วยค่าเริ่มต้น 5 ภายใน inner scope เราพยายาม set ค่าของ r เป็น reference ของ x จากนั้น inner scope จบ และเราพยายาม พิมพ์ค่าใน r โค้ดนี้จะ compile ไม่ผ่าน เพราะค่าที่ r อ้างถึงออกจาก scope ก่อนเราลองใช้ นี่คือ error message:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `x` does not live long enough
 --> src/main.rs:6:13
  |
5 |         let x = 5;
  |             - binding `x` declared here
6 |         r = &x;
  |             ^^ borrowed value does not live long enough
7 |     }
  |     - `x` dropped here while still borrowed
8 |
9 |     println!("r: {r}");
  |                   - borrow later used here

For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error

Error message บอกว่าตัวแปร x “ไม่อยู่นานพอ” เหตุผลคือ x จะออกจาก scope เมื่อ inner scope จบในบรรทัด 7 แต่ r ยัง valid สำหรับ outer scope เพราะ scope ของมันใหญ่กว่า เราบอกว่ามัน “อยู่นานกว่า” ถ้า Rust อนุญาตให้โค้ดนี้ทำงาน r จะอ้างถึงหน่วยความจำที่ถูก deallocate เมื่อ x ออกจาก scope และอะไรที่เราพยายามทำกับ r จะไม่ทำงานถูก แล้ว Rust กำหนดยังไงว่าโค้ดนี้ invalid? มันใช้ borrow checker

Borrow Checker

Rust compiler มี borrow checker ที่เปรียบเทียบ scope กำหนดว่าการ borrow ทั้งหมด valid ไหม Listing 10-17 แสดงโค้ดเดียวกับ Listing 10-16 แต่มี annotation แสดง lifetime ของตัวแปร

fn main() {
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {r}");   //          |
}                         // ---------+
Listing 10-17: Annotation ของ lifetime ของ r และ x ชื่อ 'a และ 'b ตามลำดับ

ที่นี่ เรา annotate lifetime ของ r ด้วย 'a และ lifetime ของ x ด้วย 'b อย่างที่คุณเห็น block 'b ภายในเล็กกว่า block lifetime 'a ภายนอก มาก ตอน compile time Rust เปรียบเทียบขนาดของสอง lifetime และเห็นว่า r มี lifetime 'a แต่อ้างถึงหน่วยความจำที่มี lifetime 'b โปรแกรมถูก ปฏิเสธเพราะ 'b สั้นกว่า 'a — เรื่องของ reference ไม่อยู่นานเท่า reference

Listing 10-18 แก้โค้ดเพื่อให้ไม่มี dangling reference และมัน compile ผ่านโดยไม่มี error

fn main() {
    let x = 5;            // ----------+-- 'b
                          //           |
    let r = &x;           // --+-- 'a  |
                          //   |       |
    println!("r: {r}");   //   |       |
                          // --+       |
}                         // ----------+
Listing 10-18: Reference ที่ valid เพราะข้อมูลมี lifetime ยาวกว่า reference

ที่นี่ x มี lifetime 'b ซึ่งในกรณีนี้ใหญ่กว่า 'a นี่หมายความว่า r อ้างถึง x ได้ เพราะ Rust รู้ว่า reference ใน r จะ valid เสมอ ขณะที่ x valid

ตอนนี้คุณรู้ว่า lifetime ของ reference อยู่ที่ไหน และวิธีที่ Rust วิเคราะห์ lifetime เพื่อรับประกันว่า reference จะ valid เสมอ มาสำรวจ generic lifetime ใน parameter ฟังก์ชันและ return value

Generic Lifetime ในฟังก์ชัน

เราจะเขียนฟังก์ชันที่ return string slice ที่ยาวกว่าของสองตัว ฟังก์ชันนี้ จะรับ string slice สองตัวและ return string slice หนึ่งตัว หลังเรา implement ฟังก์ชัน longest แล้ว โค้ดใน Listing 10-19 ควรพิมพ์ The longest string is abcd

Filename: src/main.rs
fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {result}");
}
Listing 10-19: ฟังก์ชัน main ที่เรียกฟังก์ชัน longest หา string slice ที่ยาวกว่าของสองตัว

หมายเหตุว่าเราอยากให้ฟังก์ชันรับ string slice ซึ่งเป็น reference แทน string เพราะเราไม่อยากให้ฟังก์ชัน longest รับ ownership ของ parameter ดู “String Slice เป็น Parameter” ในบทที่ 4 สำหรับการพูดถึงเพิ่มเติมว่าทำไม parameter ที่เราใช้ใน Listing 10-19 คือตัวที่เราอยากได้

ถ้าเราลอง implement ฟังก์ชัน longest ดังที่แสดงใน Listing 10-20 มันจะ compile ไม่ผ่าน

Filename: src/main.rs
fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {result}");
}

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}
Listing 10-20: Implementation ของฟังก์ชัน longest ที่ return string slice ที่ยาวกว่าของสองตัว แต่ยัง compile ไม่ผ่าน

แทน เราได้ error ต่อไปนี้ที่พูดถึง lifetime:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0106]: missing lifetime specifier
 --> src/main.rs:9:33
  |
9 | fn longest(x: &str, y: &str) -> &str {
  |               ----     ----     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
  |
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error

ข้อความ help เผยว่า return type ต้อง generic lifetime parameter บนมัน เพราะ Rust บอกไม่ได้ว่า reference ที่ถูก return อ้างถึง x หรือ y จริง ๆ เราก็ไม่รู้เหมือนกัน เพราะ block if ใน body ของฟังก์ชันนี้ return reference ของ x และ block else return reference ของ y!

เมื่อเราประกาศฟังก์ชันนี้ เราไม่รู้ค่าคอนกรีตที่จะส่งเข้าฟังก์ชันนี้ เรา จึงไม่รู้ว่ากรณี if หรือกรณี else จะ execute เรายังไม่รู้ lifetime คอนกรีตของ reference ที่จะส่งเข้า เราจึงดู scope เหมือนที่เราทำใน Listing 10-17 และ 10-18 กำหนดว่า reference ที่เรา return จะ valid เสมอ ไม่ได้ Borrow checker ก็กำหนดไม่ได้ เพราะมันไม่รู้ว่า lifetime ของ x และ y สัมพันธ์กับ lifetime ของ return value อย่างไร ในการแก้ error นี้ เราจะเพิ่ม generic lifetime parameter ที่ประกาศความสัมพันธ์ระหว่าง reference เพื่อให้ borrow checker ทำการวิเคราะห์ได้

Syntax ของ Lifetime Annotation

Lifetime annotation ไม่เปลี่ยนนานแค่ไหนที่ reference ใด ๆ อยู่ ตรงข้าม พวกมันบรรยายความสัมพันธ์ของ lifetime ของ reference หลายตัวกับกันโดยไม่ กระทบ lifetime เช่นเดียวกับฟังก์ชันรับ type ใด ๆ ได้เมื่อ signature ระบุ generic type parameter ฟังก์ชันรับ reference ที่มี lifetime ใด ๆ ได้ โดยระบุ generic lifetime parameter

Lifetime annotation มี syntax ที่ผิดปกตินิดหน่อย — ชื่อ lifetime parameter ต้องขึ้นต้นด้วย apostrophe (') และโดยปกติเป็นตัวพิมพ์เล็ก ทั้งหมดและสั้นมาก เหมือน generic type คนส่วนใหญ่ใช้ชื่อ 'a สำหรับ lifetime annotation แรก เราวาง lifetime parameter annotation หลัง & ของ reference โดยใช้ space คั่น annotation จาก type ของ reference

นี่คือตัวอย่างบางอัน — reference ของ i32 โดยไม่มี lifetime parameter, reference ของ i32 ที่มี lifetime parameter ชื่อ 'a และ mutable reference ของ i32 ที่ก็มี lifetime 'a:

&i32        // reference
&'a i32     // reference ที่มี lifetime explicit
&'a mut i32 // mutable reference ที่มี lifetime explicit

Lifetime annotation หนึ่งตัวเองไม่มีความหมายมาก เพราะ annotation ตั้งใจ บอก Rust ว่า generic lifetime parameter ของ reference หลายตัวสัมพันธ์ กันยังไง มาตรวจสอบว่า lifetime annotation สัมพันธ์กันอย่างไรในบริบทของ ฟังก์ชัน longest

ใน Signature ฟังก์ชัน

ในการใช้ lifetime annotation ใน signature ฟังก์ชัน เราต้องประกาศ generic lifetime parameter ภายใน angle bracket ระหว่างชื่อฟังก์ชันและ list parameter เช่นเดียวกับที่เราทำกับ generic type parameter

เราอยากให้ signature แสดงข้อจำกัดต่อไปนี้ — reference ที่ return จะ valid ตราบที่ parameter ทั้งสองตัว valid นี่คือความสัมพันธ์ระหว่าง lifetime ของ parameter และ return value เราจะตั้งชื่อ lifetime ว่า 'a แล้วเพิ่ม เข้าแต่ละ reference ดังที่แสดงใน Listing 10-21

Filename: src/main.rs
fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {result}");
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
Listing 10-21: Definition ฟังก์ชัน longest ระบุว่า reference ทั้งหมดใน signature ต้องมี lifetime 'a เดียวกัน

โค้ดนี้ควร compile และให้ผลที่เราอยากเมื่อเราใช้กับฟังก์ชัน main ใน Listing 10-19

Signature ฟังก์ชันตอนนี้บอก Rust ว่าสำหรับ lifetime 'a บางตัว ฟังก์ชัน รับสอง parameter ที่ทั้งคู่เป็น string slice ที่อยู่อย่างน้อยนานเท่า lifetime 'a Signature ฟังก์ชันยังบอก Rust ว่า string slice ที่ return จากฟังก์ชันจะอยู่อย่างน้อยนานเท่า lifetime 'a ในทางปฏิบัติ มันหมายความ ว่า lifetime ของ reference ที่ return โดยฟังก์ชัน longest เหมือนกับ lifetime ที่เล็กกว่าของ lifetime ของค่าที่อ้างถึงโดย argument ของ ฟังก์ชัน ความสัมพันธ์เหล่านี้คือสิ่งที่เราอยากให้ Rust ใช้เมื่อวิเคราะห์ โค้ดนี้

จำไว้ เมื่อเราระบุ lifetime parameter ใน signature ฟังก์ชันนี้ เราไม่ เปลี่ยน lifetime ของค่าใด ๆ ที่ส่งเข้าหรือ return ตรงข้าม เรากำลังระบุ ว่า borrow checker ควรปฏิเสธค่าใด ๆ ที่ไม่ตามข้อจำกัดเหล่านี้ หมายเหตุว่า ฟังก์ชัน longest ไม่ต้องรู้เป๊ะ ๆ ว่า x และ y จะอยู่นานเท่าไร แค่ ว่า scope บางตัวสามารถแทน 'a ที่จะ satisfy signature นี้

เมื่อ annotate lifetime ในฟังก์ชัน annotation ไปใน signature ฟังก์ชัน ไม่ใช่ใน body ฟังก์ชัน Lifetime annotation กลายเป็นส่วนของสัญญาของ ฟังก์ชัน เหมือน type ใน signature การให้ signature ฟังก์ชันมีสัญญา lifetime หมายความว่าการวิเคราะห์ที่ Rust compiler ทำง่ายกว่า ถ้ามีปัญหา กับวิธีที่ฟังก์ชันถูก annotate หรือวิธีที่มันถูกเรียก compiler error ชี้ ไปยังส่วนของโค้ดเราและข้อจำกัดอย่างแม่นยำกว่า ถ้าตรงข้าม Rust compiler ทำการ infer มากกว่าเรื่องสิ่งที่เราตั้งใจให้ความสัมพันธ์ของ lifetime เป็น compiler อาจชี้ไปยังการใช้โค้ดของเราที่ห่างหลายขั้นจากต้นเหตุของ ปัญหาเท่านั้น

เมื่อเราส่ง reference คอนกรีตให้ longest lifetime คอนกรีตที่แทน 'a คือส่วนของ scope ของ x ที่ทับซ้อนกับ scope ของ y พูดอีกอย่าง generic lifetime 'a จะได้ lifetime คอนกรีตที่เท่ากับเล็กกว่าของ lifetime ของ x และ y เพราะเรา annotate reference ที่ return ด้วย lifetime parameter 'a เดียวกัน reference ที่ return จะ valid สำหรับ ความยาวของเล็กกว่าของ lifetime ของ x และ y ด้วย

มาดูว่า lifetime annotation จำกัดฟังก์ชัน longest ยังไง โดยส่ง reference ที่มี lifetime คอนกรีตต่างกัน Listing 10-22 เป็นตัวอย่างตรงไป ตรงมา

Filename: src/main.rs
fn main() {
    let string1 = String::from("long string is long");

    {
        let string2 = String::from("xyz");
        let result = longest(string1.as_str(), string2.as_str());
        println!("The longest string is {result}");
    }
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
Listing 10-22: ใช้ฟังก์ชัน longest กับ reference ของค่า String ที่มี lifetime คอนกรีตต่างกัน

ในตัวอย่างนี้ string1 valid จนถึงท้าย outer scope, string2 valid จนถึงท้าย inner scope และ result อ้างถึงบางอย่างที่ valid จนถึงท้าย inner scope รันโค้ดนี้และคุณจะเห็นว่า borrow checker อนุมัติ มันจะ compile และพิมพ์ The longest string is long string is long

ถัดไป ลองตัวอย่างที่แสดงว่า lifetime ของ reference ใน result ต้องเป็น lifetime เล็กกว่าของสอง argument เราจะย้ายการประกาศตัวแปร result ออก นอก inner scope แต่ทิ้งการ assign ค่าให้ตัวแปร result ภายใน scope กับ string2 จากนั้นเราจะย้าย println! ที่ใช้ result ออกนอก inner scope หลัง inner scope จบ โค้ดใน Listing 10-23 จะ compile ไม่ผ่าน

Filename: src/main.rs
fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {result}");
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
Listing 10-23: พยายามใช้ result หลัง string2 ออกจาก scope

เมื่อเราลอง compile โค้ดนี้ เราได้ error นี้:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `string2` does not live long enough
 --> src/main.rs:6:44
  |
5 |         let string2 = String::from("xyz");
  |             ------- binding `string2` declared here
6 |         result = longest(string1.as_str(), string2.as_str());
  |                                            ^^^^^^^ borrowed value does not live long enough
7 |     }
  |     - `string2` dropped here while still borrowed
8 |     println!("The longest string is {result}");
  |                                      ------ borrow later used here

For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error

Error แสดงว่าเพื่อให้ result valid สำหรับ statement println! string2 จะต้อง valid จนถึงท้าย outer scope Rust รู้เพราะเรา annotate lifetime ของ parameter ฟังก์ชันและ return value โดยใช้ lifetime parameter 'a เดียวกัน

ในฐานะมนุษย์ เราดูโค้ดนี้และเห็นว่า string1 ยาวกว่า string2 ดังนั้น result จะมี reference ของ string1 เพราะ string1 ยังไม่ออกจาก scope reference ของ string1 จะยัง valid สำหรับ statement println! อย่างไร ก็ตาม compiler เห็นไม่ได้ว่า reference valid ในกรณีนี้ เราบอก Rust ว่า lifetime ของ reference ที่ return โดยฟังก์ชัน longest เหมือนกับ lifetime เล็กกว่าของ reference ที่ส่งเข้า ดังนั้น borrow checker ไม่ อนุญาตโค้ดใน Listing 10-23 ว่าอาจมี reference invalid

ลองออกแบบการทดลองเพิ่มที่แตกต่างค่าและ lifetime ของ reference ที่ส่งเข้า ฟังก์ชัน longest และวิธีที่ใช้ reference ที่ return ทำสมมติฐานว่าการ ทดลองของคุณจะผ่าน borrow checker หรือไม่ก่อน compile แล้วเช็คว่าคุณถูก!

ความสัมพันธ์

วิธีที่คุณต้องระบุ lifetime parameter ขึ้นกับสิ่งที่ฟังก์ชันของคุณทำ เช่น ถ้าเราเปลี่ยน implementation ของฟังก์ชัน longest ให้ return parameter แรกเสมอ แทน string slice ที่ยาวที่สุด เราไม่ต้องระบุ lifetime บน parameter y โค้ดต่อไปนี้จะ compile ผ่าน:

Filename: src/main.rs
fn main() {
    let string1 = String::from("abcd");
    let string2 = "efghijklmnopqrstuvwxyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {result}");
}

fn longest<'a>(x: &'a str, y: &str) -> &'a str {
    x
}

เราระบุ lifetime parameter 'a สำหรับ parameter x และ return type แต่ไม่สำหรับ parameter y เพราะ lifetime ของ y ไม่มีความสัมพันธ์ใด กับ lifetime ของ x หรือ return value

เมื่อ return reference จากฟังก์ชัน lifetime parameter สำหรับ return type ต้อง match lifetime parameter สำหรับ parameter หนึ่งตัว ถ้า reference ที่ return ไม่ อ้างถึง parameter ตัวหนึ่ง มันต้องอ้างถึงค่าที่สร้างภายใน ฟังก์ชันนี้ อย่างไรก็ตาม นี่จะเป็น dangling reference เพราะค่าจะออกจาก scope ที่ท้ายฟังก์ชัน พิจารณา implementation ที่ลองของฟังก์ชัน longest ที่จะ compile ไม่ผ่าน:

Filename: src/main.rs
fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {result}");
}

fn longest<'a>(x: &str, y: &str) -> &'a str {
    let result = String::from("really long string");
    result.as_str()
}

ที่นี่ แม้เราระบุ lifetime parameter 'a สำหรับ return type implementation นี้จะ compile ไม่ผ่าน เพราะ lifetime ของ return value ไม่สัมพันธ์กับ lifetime ของ parameter เลย นี่คือ error message ที่เราได้:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0515]: cannot return value referencing local variable `result`
  --> src/main.rs:11:5
   |
11 |     result.as_str()
   |     ------^^^^^^^^^
   |     |
   |     returns a value referencing data owned by the current function
   |     `result` is borrowed here

For more information about this error, try `rustc --explain E0515`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error

ปัญหาคือ result ออกจาก scope และถูก cleanup ที่ท้ายฟังก์ชัน longest เรายังพยายาม return reference ของ result จากฟังก์ชัน ไม่มีวิธีที่เรา ระบุ lifetime parameter ที่จะเปลี่ยน dangling reference และ Rust จะไม่ ให้เราสร้าง dangling reference ในกรณีนี้ การแก้ดีที่สุดคือ return type ข้อมูลที่ owned แทน reference เพื่อให้ฟังก์ชันที่เรียกรับผิดชอบในการ cleanup ค่า

สุดท้าย syntax lifetime เกี่ยวกับการเชื่อม lifetime ของ parameter และ return value ต่าง ๆ ของฟังก์ชัน เมื่อเชื่อมกัน Rust มีข้อมูลพอที่จะ อนุญาต operation memory-safe และไม่อนุญาต operation ที่จะสร้าง dangling pointer หรือฝ่าฝืน memory safety

ใน Definition Struct

ที่ผ่านมา struct ที่เราประกาศทั้งหมดเก็บ type ที่ owned เราประกาศ struct ให้เก็บ reference ได้ แต่ในกรณีนั้น เราต้องเพิ่ม lifetime annotation บนทุก reference ใน definition struct Listing 10-24 มี struct ชื่อ ImportantExcerpt ที่เก็บ string slice

Filename: src/main.rs
struct ImportantExcerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}
Listing 10-24: struct ที่เก็บ reference ต้องการ lifetime annotation

struct นี้มี field เดียว part ที่เก็บ string slice ซึ่งเป็น reference เช่นเดียวกับ generic data type เราประกาศชื่อ generic lifetime parameter ภายใน angle bracket หลังชื่อ struct เพื่อให้เราใช้ lifetime parameter ใน body ของ definition struct annotation นี้หมายความว่า instance ของ ImportantExcerpt outlive reference ที่มันเก็บใน field part ไม่ได้

ฟังก์ชัน main ที่นี่สร้าง instance ของ struct ImportantExcerpt ที่ เก็บ reference ของประโยคแรกของ String ที่ owned โดยตัวแปร novel ข้อมูลใน novel มีอยู่ก่อน instance ImportantExcerpt ถูกสร้าง นอกจาก นี้ novel ไม่ออกจาก scope จนกว่าหลัง ImportantExcerpt ออกจาก scope ดังนั้น reference ใน instance ImportantExcerpt valid

Lifetime Elision

คุณได้เรียนว่าทุก reference มี lifetime และคุณต้องระบุ lifetime parameter สำหรับฟังก์ชันหรือ struct ที่ใช้ reference อย่างไรก็ตาม เรามีฟังก์ชันใน Listing 4-9 แสดงอีกครั้งใน Listing 10-25 ที่ compile ผ่านโดยไม่มี lifetime annotation

Filename: src/lib.rs
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {
    let my_string = String::from("hello world");

    // first_word works on slices of `String`s
    let word = first_word(&my_string[..]);

    let my_string_literal = "hello world";

    // first_word works on slices of string literals
    let word = first_word(&my_string_literal[..]);

    // Because string literals *are* string slices already,
    // this works too, without the slice syntax!
    let word = first_word(my_string_literal);
}
Listing 10-25: ฟังก์ชันที่เราประกาศใน Listing 4-9 ที่ compile ผ่านโดยไม่มี lifetime annotation แม้ parameter และ return type เป็น reference

เหตุผลที่ฟังก์ชันนี้ compile ผ่านโดยไม่มี lifetime annotation เป็น ประวัติศาสตร์ — ใน version ก่อน 1.0 ของ Rust โค้ดนี้จะ compile ไม่ผ่าน เพราะทุก reference ต้อง lifetime explicit ในเวลานั้น signature ฟังก์ชัน จะถูกเขียนแบบนี้:

fn first_word<'a>(s: &'a str) -> &'a str {

หลังเขียนโค้ด Rust เยอะ ทีม Rust พบว่าโปรแกรมเมอร์ Rust กำลังป้อน lifetime annotation เดียวกันซ้ำ ๆ ในสถานการณ์เฉพาะ สถานการณ์เหล่านี้คาด เดาได้และตาม pattern กำหนดได้ไม่กี่ตัว นักพัฒนา program pattern เหล่านี้ เข้าโค้ดของ compiler เพื่อให้ borrow checker infer lifetime ในสถานการณ์ เหล่านี้ และไม่ต้องการ annotation explicit

ประวัติ Rust ชิ้นนี้เกี่ยวข้องเพราะเป็นไปได้ที่ pattern กำหนดได้มากขึ้น จะเกิดและถูกเพิ่มเข้า compiler ในอนาคต อาจต้องการ lifetime annotation น้อยลงอีก

Pattern ที่ program เข้าการวิเคราะห์ reference ของ Rust เรียกว่า lifetime elision rules เหล่านี้ไม่ใช่กฎสำหรับโปรแกรมเมอร์ตาม — พวกมัน เป็นชุดของกรณีเฉพาะที่ compiler จะพิจารณา และถ้าโค้ดของคุณตรงกับกรณีเหล่า นี้ คุณไม่ต้องเขียน lifetime explicit

Elision rules ไม่ให้การ infer เต็ม ถ้ายังมีความกำกวมเรื่อง lifetime ที่ reference มี หลัง Rust ใช้กฎ compiler จะไม่เดาว่า lifetime ของ reference ที่เหลือควรเป็น แทนการเดา compiler จะให้ error ที่คุณแก้ได้โดยเพิ่ม lifetime annotation

Lifetime บน parameter ฟังก์ชันหรือเมธอดเรียกว่า input lifetime และ lifetime บน return value เรียกว่า output lifetime

Compiler ใช้สามกฎหา lifetime ของ reference เมื่อไม่มี annotation explicit กฎแรกใช้กับ input lifetime และกฎที่สองและสามใช้กับ output lifetime ถ้า compiler ถึงท้ายของสามกฎและยังมี reference ที่หา lifetime ไม่ได้ compiler จะหยุดด้วย error กฎเหล่านี้ใช้กับ definition fn และ block impl

กฎแรกคือ compiler assign lifetime parameter ให้แต่ละ parameter ที่เป็น reference พูดอีกอย่าง ฟังก์ชันที่มี parameter หนึ่งตัวได้ lifetime parameter หนึ่งตัว: fn foo<'a>(x: &'a i32); ฟังก์ชันที่มี parameter สองตัวได้ lifetime parameter แยกสองตัว: fn foo<'a, 'b>(x: &'a i32, y: &'b i32); และต่อไป

กฎที่สองคือ ถ้ามี input lifetime parameter เดียวเป๊ะ ๆ lifetime นั้นถูก assign ให้ output lifetime parameter ทั้งหมด: fn foo<'a>(x: &'a i32) -> &'a i32

กฎที่สามคือ ถ้ามี input lifetime parameter หลายตัว แต่หนึ่งในนั้นเป็น &self หรือ &mut self เพราะนี่คือเมธอด lifetime ของ self ถูก assign ให้ output lifetime parameter ทั้งหมด กฎที่สามนี้ทำให้เมธอดอ่าน และเขียนได้ดีกว่ามาก เพราะต้องการสัญลักษณ์น้อยกว่า

ลองสมมติเราเป็น compiler เราจะใช้กฎเหล่านี้หา lifetime ของ reference ใน signature ของฟังก์ชัน first_word ใน Listing 10-25 Signature เริ่มโดย ไม่มี lifetime ผูกกับ reference:

fn first_word(s: &str) -> &str {

จากนั้น compiler ใช้กฎแรก ซึ่งระบุว่าแต่ละ parameter ได้ lifetime ของ ตัวเอง เราจะเรียก 'a ตามปกติ ดังนั้นตอนนี้ signature เป็นนี้:

fn first_word<'a>(s: &'a str) -> &str {

กฎที่สองใช้เพราะมี input lifetime เดียวเป๊ะ ๆ กฎที่สองระบุว่า lifetime ของ input parameter หนึ่งตัวถูก assign ให้ output lifetime ดังนั้น signature เป็นนี้ตอนนี้:

fn first_word<'a>(s: &'a str) -> &'a str {

ตอนนี้ reference ทั้งหมดใน signature ฟังก์ชันนี้มี lifetime และ compiler ดำเนินการวิเคราะห์ต่อโดยไม่ต้องการให้โปรแกรมเมอร์ annotate lifetime ใน signature ฟังก์ชันนี้

มาดูตัวอย่างอีก ครั้งนี้ใช้ฟังก์ชัน longest ที่ไม่มี lifetime parameter เมื่อเราเริ่มทำงานกับมันใน Listing 10-20:

fn longest(x: &str, y: &str) -> &str {

มาใช้กฎแรก — แต่ละ parameter ได้ lifetime ของตัวเอง ครั้งนี้เรามีสอง parameter แทนหนึ่ง เราจึงมีสอง lifetime:

fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {

คุณเห็นว่ากฎที่สองไม่ใช้ เพราะมี input lifetime มากกว่าหนึ่งตัว กฎที่สาม ก็ไม่ใช้ เพราะ longest เป็นฟังก์ชันแทนเมธอด parameter ไม่มีตัวไหนเป็น self หลังทำงานผ่านสามกฎทั้งหมด เรายังไม่หา lifetime ของ return type ได้ นี่คือเหตุผลที่เราได้ error พยายาม compile โค้ดใน Listing 10-20 — compiler ทำงานผ่าน lifetime elision rules แต่ยังหา lifetime ของ reference ใน signature ทั้งหมดไม่ได้

เพราะกฎที่สามจริง ๆ ใช้แค่ใน signature เมธอด เราจะดู lifetime ในบริบทนั้น ถัดไป เพื่อดูว่าทำไมกฎที่สามหมายความว่าเราไม่ต้อง annotate lifetime ใน signature เมธอดบ่อย

ใน Definition เมธอด

เมื่อเรา implement เมธอดบน struct ที่มี lifetime เราใช้ syntax เดียวกัน กับของ generic type parameter ดังที่แสดงใน Listing 10-11 ตำแหน่งที่เรา ประกาศและใช้ lifetime parameter ขึ้นกับว่าพวกมันสัมพันธ์กับ field struct หรือ parameter เมธอดและ return value

ชื่อ lifetime สำหรับ field struct ต้องประกาศหลัง keyword impl เสมอ และ ใช้หลังชื่อ struct เพราะ lifetime เหล่านั้นเป็นส่วนของ type ของ struct

ใน signature เมธอดภายใน block impl reference อาจผูกกับ lifetime ของ reference ใน field struct หรืออาจอิสระ นอกจากนี้ lifetime elision rules มักทำให้ lifetime annotation ไม่จำเป็นใน signature เมธอด มาดูตัวอย่าง บางตัวที่ใช้ struct ชื่อ ImportantExcerpt ที่เราประกาศใน Listing 10-24

ก่อนอื่น เราจะใช้เมธอดชื่อ level ที่ parameter เดียวคือ reference ของ self และ return value คือ i32 ซึ่งไม่ใช่ reference ของอะไร:

struct ImportantExcerpt<'a> {
    part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {announcement}");
        self.part
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}

การประกาศ lifetime parameter หลัง impl และการใช้หลังชื่อ type บังคับ แต่เพราะกฎ elision แรก เราไม่ต้อง annotate lifetime ของ reference ของ self

นี่คือตัวอย่างที่ lifetime elision rule ที่สามใช้:

struct ImportantExcerpt<'a> {
    part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {announcement}");
        self.part
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}

มีสอง input lifetime Rust จึงใช้ lifetime elision rule แรก และให้ทั้ง &self และ announcement lifetime ของตัวเอง จากนั้น เพราะ parameter ตัวหนึ่งเป็น &self return type ได้ lifetime ของ &self และ lifetime ทั้งหมดถูกบัญชี

Static Lifetime

Lifetime พิเศษหนึ่งที่เราต้องพูดถึงคือ 'static ซึ่งบ่งบอกว่า reference ที่ได้รับผลกระทบ สามารถ อยู่ตลอดระยะเวลาของโปรแกรม String literal ทั้งหมดมี lifetime 'static ซึ่งเรา annotate ดังนี้:

#![allow(unused)]
fn main() {
let s: &'static str = "I have a static lifetime.";
}

ข้อความของ string นี้ถูกเก็บโดยตรงใน binary ของโปรแกรม ซึ่งมีให้เสมอ ดังนั้น lifetime ของ string literal ทั้งหมดเป็น 'static

คุณอาจเห็นคำแนะนำใน error message ให้ใช้ lifetime 'static แต่ก่อนระบุ 'static เป็น lifetime สำหรับ reference คิดว่า reference ที่คุณมีจริง ๆ อยู่ตลอด lifetime ของโปรแกรมของคุณไหม และคุณอยากให้มันเป็นแบบนั้นไหม โดยส่วนใหญ่ error message ที่แนะนำ lifetime 'static เป็นผลของการ พยายามสร้าง dangling reference หรือ mismatch ของ lifetime ที่มี ในกรณี เช่นนั้น คำตอบคือแก้ปัญหาเหล่านั้น ไม่ใช่ระบุ lifetime 'static

Generic Type Parameter, Trait Bound และ Lifetime

มาดูสั้น ๆ ที่ syntax ของการระบุ generic type parameter, trait bound และ lifetime ทั้งหมดในฟังก์ชันเดียว!

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest_with_an_announcement(
        string1.as_str(),
        string2,
        "Today is someone's birthday!",
    );
    println!("The longest string is {result}");
}

use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: Display,
{
    println!("Announcement! {ann}");
    if x.len() > y.len() { x } else { y }
}

นี่คือฟังก์ชัน longest จาก Listing 10-21 ที่ return string slice ที่ ยาวกว่าของสองตัว แต่ตอนนี้มี parameter เพิ่มชื่อ ann ของ generic type T ซึ่งเติมได้ด้วย type ใด ๆ ที่ implement trait Display ตามที่ระบุ โดย clause where parameter เพิ่มนี้จะถูกพิมพ์โดยใช้ {} นี่คือเหตุผล ที่ trait bound Display จำเป็น เพราะ lifetime เป็น type ของ generic การประกาศ lifetime parameter 'a และ generic type parameter T ไปใน list เดียวกันภายใน angle bracket หลังชื่อฟังก์ชัน

สรุป

เราครอบคลุมเยอะในบทนี้! ตอนนี้คุณรู้เรื่อง generic type parameter, trait และ trait bound และ generic lifetime parameter คุณพร้อมเขียนโค้ดโดยไม่ มีการซ้ำที่ทำงานในสถานการณ์ต่างกันหลายตัว Generic type parameter ให้คุณ ใช้โค้ดกับ type ต่างกัน Trait และ trait bound รับประกันว่าแม้ type เป็น generic พวกมันจะมีพฤติกรรมที่โค้ดต้องการ คุณได้เรียนวิธีใช้ lifetime annotation รับประกันว่าโค้ดยืดหยุ่นนี้จะไม่มี dangling reference และการ วิเคราะห์ทั้งหมดนี้เกิดตอน compile time ซึ่งไม่กระทบ performance ตอน runtime!

เชื่อหรือไม่ มีอีกมากให้เรียนเรื่องหัวข้อที่เราพูดถึงในบทนี้ — บทที่ 18 พูดถึง trait object ซึ่งเป็นอีกวิธีใช้ trait ยังมี scenario ซับซ้อนกว่า ที่เกี่ยวกับ lifetime annotation ที่คุณจะต้องการแค่ใน scenario ขั้นสูง มาก สำหรับเหล่านั้น คุณควรอ่าน Rust Reference แต่ถัดไป คุณ จะเรียนวิธีเขียนเทสใน Rust เพื่อให้แน่ใจว่าโค้ดของคุณทำงานในแบบที่ควร