เหตุใดจึงต้องมีการกำหนดอายุการใช้งานที่ชัดเจนใน Rust


199

ฉันกำลังอ่านบทในช่วงชีวิตของหนังสือ Rust และฉันเจอตัวอย่างนี้สำหรับอายุการใช้งานที่ตั้งชื่อ / ชัดเจน:

struct Foo<'a> {
    x: &'a i32,
}

fn main() {
    let x;                    // -+ x goes into scope
                              //  |
    {                         //  |
        let y = &5;           // ---+ y goes into scope
        let f = Foo { x: y }; // ---+ f goes into scope
        x = &f.x;             //  | | error here
    }                         // ---+ f and y go out of scope
                              //  |
    println!("{}", x);        //  |
}                             // -+ x goes out of scope

มันค่อนข้างจะชัดเจนกับผมว่าข้อผิดพลาดที่มีการป้องกันโดยการคอมไพเลอร์คือการใช้งานหลังฟรีของการอ้างอิงที่กำหนดให้กับx: หลังจากขอบเขตภายในจะทำfและดังนั้นจึงกลายเป็นที่ไม่ถูกต้องและไม่ควรได้รับมอบหมายให้&f.xx

ปัญหาของฉันคือว่าปัญหาสามารถวิเคราะห์ได้อย่างง่ายดายโดยไม่ใช้อายุการใช้งานที่ชัดเจน 'aตัวอย่างเช่นโดยอนุมานการกำหนดที่ผิดกฎหมายของการอ้างอิงถึงขอบเขตที่กว้างขึ้น ( x = &f.x;)

ในกรณีใดบ้างที่จำเป็นต้องใช้อายุการใช้งานที่ชัดเจนเพื่อป้องกันข้อผิดพลาดจากการใช้งาน (หรือคลาสอื่น)?



2
สำหรับผู้อ่านในอนาคตของคำถามนี้โปรดทราบว่ามันเชื่อมโยงกับหนังสือฉบับพิมพ์ครั้งแรกและตอนนี้ก็มีฉบับที่สอง :)
carols10cents

คำตอบ:


205

คำตอบอื่น ๆ ทุกคนมีจุดเด่น ( ตัวอย่างที่เป็นรูปธรรมของ fjh ที่อายุการใช้งานที่ชัดเจนเป็นสิ่งจำเป็น ) แต่จะหายไปสิ่งที่สำคัญอย่างหนึ่ง: ทำไมชีวิตอย่างชัดเจนจำเป็นเมื่อคอมไพเลอร์จะบอกคุณว่าคุณได้มีพวกเขาผิด ?

นี่เป็นคำถามเดียวกันกับ "ทำไมต้องเป็นประเภทที่ชัดเจนเมื่อคอมไพเลอร์สามารถอนุมานได้" ตัวอย่างสมมุติ:

fn foo() -> _ {  
    ""
}

แน่นอนคอมไพเลอร์สามารถเห็นว่าฉันกลับมา&'static strแล้วทำไมโปรแกรมเมอร์ต้องพิมพ์มัน?

เหตุผลหลักคือในขณะที่คอมไพเลอร์สามารถดูว่าโค้ดของคุณทำอะไร แต่ไม่รู้ว่าเจตนาของคุณคืออะไร

ฟังก์ชั่นเป็นขอบเขตธรรมชาติของไฟร์วอลล์สำหรับผลกระทบของการเปลี่ยนรหัส หากเราอนุญาตให้มีการตรวจสอบอายุการใช้งานของโค้ดอย่างสมบูรณ์การเปลี่ยนแปลงที่ดูไร้เดียงสาอาจส่งผลกระทบต่ออายุการใช้งานซึ่งอาจทำให้เกิดข้อผิดพลาดในฟังก์ชันที่อยู่ไกลออกไป นี่ไม่ใช่ตัวอย่างสมมุติ ตามที่ฉันเข้าใจแล้ว Haskell มีปัญหานี้เมื่อคุณพึ่งพาการอนุมานประเภทสำหรับฟังก์ชันระดับบนสุด สนิมทำให้เกิดปัญหาขึ้นในตา

นอกจากนี้ยังมีประโยชน์อย่างมีประสิทธิภาพสำหรับคอมไพเลอร์ - ต้องมีการแยกวิเคราะห์เฉพาะฟังก์ชันลายเซ็นเพื่อตรวจสอบประเภทและอายุการใช้งาน ที่สำคัญกว่านั้นมีประโยชน์อย่างมีประสิทธิภาพสำหรับโปรแกรมเมอร์ หากเราไม่มีช่วงอายุที่ชัดเจนฟังก์ชั่นนี้จะทำอะไร:

fn foo(a: &u8, b: &u8) -> &u8

เป็นไปไม่ได้ที่จะบอกได้โดยไม่ต้องตรวจสอบแหล่งที่มาซึ่งขัดแย้งกับแนวทางปฏิบัติที่ดีที่สุดในการเข้ารหัสจำนวนมาก

โดยอนุมานการมอบหมายที่ผิดกฎหมายของการอ้างอิงถึงขอบเขตที่กว้างขึ้น

ขอบเขตคืออายุการใช้งานเป็นหลัก ชัดเจนยิ่งขึ้นตลอดชีวิต'aคือพารามิเตอร์อายุการใช้งานทั่วไปที่สามารถใช้เฉพาะกับขอบเขตที่เฉพาะเจาะจงในเวลารวบรวมตามไซต์การโทร

เหตุใดจึงจำเป็นต้องใช้อายุการใช้งานที่ชัดเจนเพื่อป้องกันข้อผิดพลาด [... ]

ไม่ใช่เลย. จำเป็นต้องใช้อายุการใช้งานเพื่อป้องกันข้อผิดพลาด แต่จำเป็นต้องใช้อายุการใช้งานที่ชัดเจนเพื่อปกป้องโปรแกรมเมอร์ผู้มีสติปัญญาน้อย


18
@jco ลองนึกภาพคุณมีฟังก์ชั่นระดับบนสุดf x = x + 1โดยไม่มีลายเซ็นประเภทที่คุณใช้ในโมดูลอื่น หากคุณเปลี่ยนคำจำกัดความในภายหลังf x = sqrt $ x + 1ประเภทจะเปลี่ยนจากNum a => a -> aเป็นFloating a => a -> aซึ่งจะทำให้เกิดข้อผิดพลาดประเภทที่ไซต์การโทรทั้งหมดที่fมีการเรียกเช่นIntอาร์กิวเมนต์ การมีลายเซ็นประเภทช่วยให้มั่นใจว่ามีข้อผิดพลาดเกิดขึ้นในเครื่อง
fjh

11
“ ขอบเขตเป็นช่วงชีวิตโดยพื้นฐานแล้วชัดเจนกว่านิดหน่อยว่าตลอดชีวิต 'a เป็นพารามิเตอร์อายุการใช้งานทั่วไปที่สามารถใช้เฉพาะกับขอบเขตที่เฉพาะเจาะจงในเวลาโทร "ว้าวนั่นเป็นจุดที่ยอดเยี่ยมมากและสว่างไสว ฉันต้องการมันถ้ามันรวมอยู่ในหนังสือเล่มนี้อย่างชัดเจน
corazza

2
@fjh ขอบคุณ เพียงเพื่อดูว่าฉันคร่ำครวญ - ประเด็นคือถ้าพิมพ์อย่างชัดเจนก่อนที่จะเพิ่มsqrt $เฉพาะข้อผิดพลาดในท้องถิ่นจะเกิดขึ้นหลังจากการเปลี่ยนแปลงและไม่ผิดพลาดมากในสถานที่อื่น ๆ (ซึ่งดีกว่ามากถ้าเราไม่ได้ ต้องการเปลี่ยนประเภทจริง) หรือไม่
corazza

5
@ jco แน่นอน การไม่ระบุประเภทหมายความว่าคุณสามารถเปลี่ยนอินเทอร์เฟซของฟังก์ชันได้โดยไม่ตั้งใจ นี่คือหนึ่งในเหตุผลที่เราขอแนะนำให้ใส่คำอธิบายประกอบรายการระดับบนสุดทั้งหมดใน Haskell
fjh

5
นอกจากนี้หากฟังก์ชั่นได้รับการอ้างอิงสองครั้งและส่งคืนการอ้างอิงดังนั้นบางครั้งมันอาจส่งคืนการอ้างอิงแรกและบางครั้งก็เป็นการอ้างอิงที่สอง ในกรณีนี้มันเป็นไปไม่ได้ที่จะอนุมานอายุการใช้งานสำหรับการอ้างอิงที่ส่งคืน อายุการใช้งานที่ชัดเจนช่วยหลีกเลี่ยง / ชี้แจงสถานการณ์ดังกล่าว
MichaelMoser

93

ลองมาดูตัวอย่างต่อไปนี้

fn foo<'a, 'b>(x: &'a u32, y: &'b u32) -> &'a u32 {
    x
}

fn main() {
    let x = 12;
    let z: &u32 = {
        let y = 42;
        foo(&x, &y)
    };
}

ที่นี่อายุการใช้งานที่ชัดเจนมีความสำคัญ การคอมไพล์นี้เนื่องจากผลลัพธ์ของfooการมีอายุการใช้งานเท่ากับอาร์กิวเมนต์แรก ( 'a) ดังนั้นจึงอาจอยู่ได้นานกว่าอาร์กิวเมนต์ที่สอง fooนี้จะแสดงด้วยชื่ออายุการใช้งานในลายเซ็นของ หากคุณสลับอาร์กิวเมนต์ในการเรียกไปfooยังคอมไพเลอร์จะบ่นว่าyมีชีวิตไม่นานพอ:

error[E0597]: `y` does not live long enough
  --> src/main.rs:10:5
   |
9  |         foo(&y, &x)
   |              - borrow occurs here
10 |     };
   |     ^ `y` dropped here while still borrowed
11 | }
   | - borrowed value needs to live until here

16

คำอธิบายประกอบตลอดชีวิตในโครงสร้างต่อไปนี้:

struct Foo<'a> {
    x: &'a i32,
}

ระบุว่าFooอินสแตนซ์ไม่ควรอยู่ได้นานกว่าการอ้างอิงที่มี ( xฟิลด์)

ตัวอย่างที่คุณเจอในหนังสือสนิมไม่ได้แสดงให้เห็นสิ่งนี้เพราะfและyตัวแปรไม่อยู่ในขอบเขตในเวลาเดียวกัน

ตัวอย่างที่ดีกว่านี้คือ:

fn main() {
    let f : Foo;
    {
        let n = 5;  // variable that is invalid outside this block
        let y = &n;
        f = Foo { x: y };
    };
    println!("{}", f.x);
}

ทีนี้fตัวแปรที่ชี้ไปยังมีf.xชีวิตอยู่


9

โปรดทราบว่าไม่มีช่วงอายุที่ชัดเจนในส่วนของรหัสยกเว้นข้อกำหนดโครงสร้าง main()คอมไพเลอร์เป็นอย่างดีสามารถที่จะสรุปในช่วงชีวิต

อย่างไรก็ตามในคำจำกัดความของประเภทนั้นอายุการใช้งานที่แน่ชัดไม่สามารถหลีกเลี่ยงได้ ตัวอย่างเช่นมีความกำกวมที่นี่:

struct RefPair(&u32, &u32);

สิ่งเหล่านี้ควรเป็นช่วงอายุที่แตกต่างกันหรือไม่ มันไม่สำคัญว่าจากมุมมองของการใช้งานที่แตกต่างอย่างมากจากstruct RefPair<'a, 'b>(&'a u32, &'b u32)struct RefPair<'a>(&'a u32, &'a u32)

ตอนนี้สำหรับกรณีง่าย ๆ เช่นเดียวกับที่คุณให้ไว้คอมไพเลอร์สามารถช่วยลดอายุการใช้งานตามทฤษฎีเหมือนในที่อื่น แต่กรณีดังกล่าวมี จำกัด มากและไม่คุ้มค่ากับความซับซ้อนเพิ่มเติมในคอมไพเลอร์ สงสัยน้อยมาก


2
คุณช่วยอธิบายได้ไหมว่าทำไมพวกเขาถึงแตกต่างกันมาก
AB

@AB ประการที่สองต้องการให้การอ้างอิงทั้งสองมีอายุการใช้งานร่วมกัน ซึ่งหมายความว่า refpair.1 ไม่สามารถอยู่ได้นานกว่า refpair2 และในทางกลับกันดังนั้นผู้อ้างอิงทั้งสองจึงต้องชี้ไปที่บางสิ่งกับเจ้าของเดียวกัน อย่างไรก็ตามสิ่งแรกนั้นต้องการให้ RefPair อยู่ได้ทั้งสองส่วน
llogiq

2
@AB ก็รวบรวมเพราะทั้งสองอายุการใช้งานเป็นปึกแผ่น - เพราะชีวิตท้องถิ่นขนาดเล็กที่'static, 'staticสามารถนำมาใช้ทุกที่อายุการใช้งานในท้องถิ่นสามารถนำมาใช้ดังนั้นในตัวอย่างของคุณจะมีพารามิเตอร์ที่อายุการใช้งานอนุมานเป็นอายุการใช้งานของท้องถิ่นp y
Vladimir Matveev

5
@AB RefPair<'a>(&'a u32, &'a u32)หมายความว่าจะเป็นจุดตัดของทั้งชีวิตการป้อนข้อมูลเช่นในกรณีนี้อายุการใช้งานของ'a y
fjh

1
@llogiq "ต้องการให้ RefPair อยู่ได้ทั้งสองส่วน" หรือไม่? ฉันคิดว่ามันเป็นสิ่งที่ตรงกันข้าม ... & u32 ยังคงสามารถเข้าใจได้โดยปราศจาก RefPair ในขณะที่ RefPair ที่มีผู้อ้างอิงตายจะแปลก
qed

6

กรณีจากหนังสือนั้นง่ายมากโดยการออกแบบ หัวข้ออายุการใช้งานถือว่าซับซ้อน

คอมไพเลอร์ไม่สามารถอนุมานอายุการใช้งานในฟังก์ชันที่มีอาร์กิวเมนต์หลายตัวได้อย่างง่ายดาย

นอกจากนี้ลังไม้ที่เป็นทางเลือกของฉันเองยังมีOptionBoolประเภทที่มีas_sliceวิธีการที่ลายเซ็นคือ

fn as_slice(&self) -> &'static [bool] { ... }

ไม่มีทางที่คอมไพเลอร์อาจจะคิดออกได้


IINM การอนุมานอายุการใช้งานของชนิดส่งคืนของฟังก์ชันสองอาร์กิวเมนต์จะเท่ากับปัญหาการหยุดชะงัก - IOW ไม่สามารถตัดสินใจได้ในระยะเวลาที่ จำกัด
dstromberg

4

ฉันได้พบอีกคำอธิบายที่ดีที่นี่: http://doc.rust-lang.org/0.12.0/guide-lifetimes.html#returning-references

โดยทั่วไปเป็นไปได้ที่จะส่งคืนการอ้างอิงหากได้มาจากพารามิเตอร์ไปยังกระบวนการ ในกรณีนั้นผลลัพธ์ของตัวชี้จะมีอายุการใช้งานเหมือนกับพารามิเตอร์อย่างใดอย่างหนึ่งเสมอ ชื่ออายุการใช้งานบ่งชี้ว่าพารามิเตอร์ที่เป็น


4

หากฟังก์ชั่นได้รับการอ้างอิงสองรายการเป็นอาร์กิวเมนต์และส่งคืนการอ้างอิงดังนั้นการนำไปใช้ของฟังก์ชันอาจส่งคืนการอ้างอิงแรกและบางครั้งเป็นการอ้างอิงที่สอง ไม่สามารถคาดการณ์ได้ว่าจะส่งคืนการอ้างอิงใดสำหรับการโทรที่ระบุ ในกรณีนี้มันเป็นไปไม่ได้ที่จะอนุมานอายุการใช้งานสำหรับการอ้างอิงที่ส่งคืนเนื่องจากการอ้างอิงอาร์กิวเมนต์แต่ละรายการอาจอ้างถึงการโยงตัวแปรที่แตกต่างกันกับอายุการใช้งานที่แตกต่างกัน อายุการใช้งานที่ชัดเจนช่วยหลีกเลี่ยงหรือชี้แจงสถานการณ์ดังกล่าว

ในทำนองเดียวกันถ้าโครงสร้างมีสองการอ้างอิง (เป็นสองเขตข้อมูลสมาชิก) จากนั้นฟังก์ชั่นสมาชิกของโครงสร้างอาจส่งกลับการอ้างอิงครั้งแรกและบางครั้งการอ้างอิงที่สอง อีกครั้งในช่วงชีวิตที่ชัดเจนป้องกันความคลุมเครือดังกล่าว

ในบางสถานการณ์ที่เรียบง่ายมีช่วงชีวิตที่ผู้แปลสามารถอนุมานอายุการใช้งานได้


1

เหตุผลที่ตัวอย่างของคุณใช้งานไม่ได้เป็นเพราะ Rust มีเพียงอายุการใช้งานในท้องถิ่นและการอนุมานประเภท สิ่งที่คุณเสนอแนะนั้นต้องการการอนุมานระดับโลก เมื่อใดก็ตามที่คุณมีข้อมูลอ้างอิงที่ไม่สามารถลบอายุการใช้งานได้จะต้องมีคำอธิบายประกอบ


1

ในฐานะผู้ใช้ใหม่สู่สนิมความเข้าใจของฉันคืออายุการใช้งานที่ชัดเจนมีจุดประสงค์สองประการ

  1. การใส่คำอธิบายประกอบตลอดชีวิตอย่างชัดเจนในฟังก์ชั่น จำกัด ประเภทของรหัสที่อาจปรากฏขึ้นภายในฟังก์ชั่นนั้น อายุขัยที่ชัดเจนช่วยให้คอมไพเลอร์มั่นใจว่าโปรแกรมของคุณกำลังทำสิ่งที่คุณตั้งใจไว้

  2. หากคุณ (คอมไพเลอร์) ต้องการตรวจสอบว่าส่วนของโค้ดถูกต้องหรือไม่คุณ (คอมไพเลอร์) จะไม่ต้องดูในฟังก์ชั่นที่เรียกว่าซ้ำ ๆ มันพอเพียงที่จะดูคำอธิบายประกอบของฟังก์ชั่นที่ถูกเรียกโดยตรงโดยชิ้นส่วนของรหัสนั้น สิ่งนี้ทำให้โปรแกรมของคุณง่ายขึ้นที่จะให้เหตุผล (คอมไพเลอร์) ของคุณและทำให้เวลาในการคอมไพล์จัดการได้

บนจุดที่ 1 พิจารณาโปรแกรมต่อไปนี้เขียนใน Python:

import pandas as pd
import numpy as np

def second_row(ar):
    return ar[0]

def work(second):
    df = pd.DataFrame(data=second)
    df.loc[0, 0] = 1

def main():
    # .. load data ..
    ar = np.array([[0, 0], [0, 0]])

    # .. do some work on second row ..
    second = second_row(ar)
    work(second)

    # .. much later ..
    print(repr(ar))

if __name__=="__main__":
    main()

ซึ่งจะพิมพ์

array([[1, 0],
       [0, 0]])

พฤติกรรมแบบนี้ทำให้ฉันประหลาดใจเสมอ สิ่งที่เกิดขึ้นคือการที่dfหน่วยความจำที่ใช้ร่วมกันกับarดังนั้นเมื่อบางส่วนของเนื้อหาของdfการเปลี่ยนแปลงในworkที่เปลี่ยนแปลงติดเชื้อarเช่นกัน อย่างไรก็ตามในบางกรณีนี่อาจเป็นสิ่งที่คุณต้องการด้วยเหตุผลประสิทธิภาพหน่วยความจำ (ไม่มีการคัดลอก) ปัญหาที่แท้จริงในรหัสนี้คือฟังก์ชั่นsecond_rowจะกลับแถวแรกแทนที่สอง; ขอให้โชคดีที่แก้จุดบกพร่อง

พิจารณาโปรแกรมที่คล้ายกันที่เขียนใน Rust แทน

#[derive(Debug)]
struct Array<'a, 'b>(&'a mut [i32], &'b mut [i32]);

impl<'a, 'b> Array<'a, 'b> {
    fn second_row(&mut self) -> &mut &'b mut [i32] {
        &mut self.0
    }
}

fn work(second: &mut [i32]) {
    second[0] = 1;
}

fn main() {
    // .. load data ..
    let ar1 = &mut [0, 0][..];
    let ar2 = &mut [0, 0][..];
    let mut ar = Array(ar1, ar2);

    // .. do some work on second row ..
    {
        let second = ar.second_row();
        work(second);
    }

    // .. much later ..
    println!("{:?}", ar);
}

รวบรวมสิ่งนี้คุณจะได้รับ

error[E0308]: mismatched types
 --> src/main.rs:6:13
  |
6 |             &mut self.0
  |             ^^^^^^^^^^^ lifetime mismatch
  |
  = note: expected type `&mut &'b mut [i32]`
             found type `&mut &'a mut [i32]`
note: the lifetime 'b as defined on the impl at 4:5...
 --> src/main.rs:4:5
  |
4 |     impl<'a, 'b> Array<'a, 'b> {
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^
note: ...does not necessarily outlive the lifetime 'a as defined on the impl at 4:5
 --> src/main.rs:4:5
  |
4 |     impl<'a, 'b> Array<'a, 'b> {
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^

ในความเป็นจริงคุณจะได้รับสองข้อผิดพลาดนอกจากนี้ยังเป็นหนึ่งเดียวกับบทบาทของ'aและ'bสบตา เมื่อดูที่คำอธิบายประกอบของsecond_rowเราพบว่าผลลัพธ์ควรเป็น&mut &'b mut [i32]เช่นผลลัพธ์ที่ควรจะอ้างอิงถึงการอ้างอิงกับอายุการใช้งาน'b(อายุการใช้งานของแถวที่สองของArray) อย่างไรก็ตามเนื่องจากเรากำลังส่งคืนแถวแรก (ซึ่งมีอายุการใช้งานยาวนาน'a) คอมไพเลอร์จึงบ่นเกี่ยวกับอายุการใช้งานที่ไม่ตรงกัน ในสถานที่ที่เหมาะสม ในเวลาที่เหมาะสม. การแก้ไขข้อบกพร่องเป็นเรื่องง่าย


0

ฉันคิดว่าคำอธิบายประกอบตลอดชีวิตเป็นสัญญาเกี่ยวกับการอ้างอิงที่ถูกต้องในขอบเขตการรับเท่านั้นในขณะที่ยังคงถูกต้องในขอบเขตแหล่งที่มา ประกาศการอ้างอิงเพิ่มเติมในประเภทเดียวกันตลอดช่วงอายุการรวมขอบเขตซึ่งหมายความว่าแหล่งอ้างอิงทั้งหมดต้องทำตามสัญญานี้ คำอธิบายประกอบดังกล่าวช่วยให้ผู้รวบรวมเพื่อตรวจสอบการปฏิบัติตามสัญญา

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.