रुस्त में स्पष्ट जीवनकाल की आवश्यकता क्यों है?


199

मैं रस्ट बुक के जीवन काल के अध्याय को पढ़ रहा था , और मैं इस उदाहरण के लिए नामांकित / स्पष्ट जीवनकाल के लिए आया था:

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.xअमान्य हो गया है, और उसे सौंपा नहीं जाना चाहिए था x

मेरा मुद्दा यह है कि स्पष्ट जीवनकाल का उपयोग किए बिना समस्या का आसानी से विश्लेषण किया जा सकता था , उदाहरण के लिए एक व्यापक दायरे के संदर्भ में एक अवैध असाइनमेंट का संदर्भ देकर ( )। 'ax = &f.x;

किन मामलों में स्पष्ट जीवनकाल वास्तव में उपयोग-मुक्त (या कुछ अन्य वर्ग?) त्रुटियों को रोकने के लिए आवश्यक है।



2
इस प्रश्न के भविष्य के पाठकों के लिए, कृपया इसे पुस्तक के पहले संस्करण से लिंक करें और अब दूसरा संस्करण है :)
carols10cents

जवाबों:


205

अन्य जवाबों में सभी मुख्य बिंदु होते हैं ( fjh का ठोस उदाहरण जहां एक स्पष्ट जीवनकाल की आवश्यकता होती है ), लेकिन एक महत्वपूर्ण बात याद आ रही है: जब संकलक आपको बताएंगे कि आपको गलत मिला है तो स्पष्ट जीवनकाल की आवश्यकता क्यों है ?

यह वास्तव में एक ही सवाल है "जब संकलक उन्हें अनुमान लगा सकते हैं तो स्पष्ट प्रकार की आवश्यकता क्यों होती है"। एक काल्पनिक उदाहरण:

fn foo() -> _ {  
    ""
}

बेशक, संकलक देख सकता है कि मैं एक लौट रहा हूं &'static str, इसलिए प्रोग्रामर को इसे क्यों लिखना है?

मुख्य कारण यह है कि जबकि कंपाइलर देख सकता है कि आपका कोड क्या करता है, यह नहीं जानता कि आपका इरादा क्या था।

फ़ंक्शंस कोड बदलने के प्रभावों को फ़ायरवॉल के लिए एक प्राकृतिक सीमा है। यदि हम आजीवन कोड से पूरी तरह से निरीक्षण करने की अनुमति देते हैं, तो एक निर्दोष दिखने वाला परिवर्तन जीवनकाल को प्रभावित कर सकता है, जो तब तक एक समारोह में त्रुटियों का कारण बन सकता है। यह एक काल्पनिक उदाहरण नहीं है। जैसा कि मैं इसे समझता हूं, हास्केल को यह समस्या तब होती है जब आप शीर्ष स्तर के कार्यों के लिए प्रकार के अनुमान पर भरोसा करते हैं। जंग ने कली में उस विशेष समस्या को खत्म कर दिया।

कंपाइलर के लिए एक दक्षता लाभ भी है - प्रकार और जीवनकाल को सत्यापित करने के लिए केवल फ़ंक्शन हस्ताक्षरों को पार्स करने की आवश्यकता है। इससे भी महत्वपूर्ण बात, यह प्रोग्रामर के लिए एक दक्षता लाभ है। यदि हमारे पास स्पष्ट जीवनकाल नहीं था, तो यह कार्य क्या करता है:

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
"स्कोप्स जीवनकाल हैं, अनिवार्य रूप से। थोड़ा और अधिक स्पष्ट रूप से, एक जीवनकाल 'ए एक सामान्य जीवनकाल पैरामीटर है जिसे कॉल समय पर एक विशिष्ट दायरे के साथ विशेष किया जा सकता है।" वाह यह वास्तव में बहुत अच्छा, रोशन बिंदु है। मुझे यह पसंद है अगर यह पुस्तक में स्पष्ट रूप से शामिल किया गया था।
corazza

2
@fjh धन्यवाद बस यह देखने के लिए कि क्या मैंने इसे ग्रो किया है - बिंदु यह है कि यदि जोड़ने से पहले स्पष्ट रूप से टाइप किया गया था sqrt $, तो परिवर्तन के बाद केवल एक स्थानीय त्रुटि हुई होगी, और अन्य स्थानों पर बहुत अधिक त्रुटियां नहीं हुईं (जो कि हमने किया तो बहुत बेहतर है) 'वास्तविक प्रकार को बदलना नहीं चाहते हैं)?
कॉर्ज़ा

5
@jco बिल्कुल। किसी प्रकार को निर्दिष्ट नहीं करने का मतलब है कि आप गलती से किसी फ़ंक्शन के इंटरफ़ेस को बदल सकते हैं। यही एक कारण है कि हास्केल में सभी शीर्ष स्तरीय वस्तुओं की व्याख्या करने के लिए इसे दृढ़ता से प्रोत्साहित किया जाता है।
fjh

5
यदि कोई फ़ंक्शन दो संदर्भों को भी प्राप्त करता है और एक संदर्भ देता है तो यह कभी-कभी पहला संदर्भ और कभी-कभी दूसरा भी वापस कर सकता है। इस मामले में लौटे संदर्भ के लिए जीवन भर का अनुमान लगाना असंभव है। स्पष्ट जीवनकाल ऐसी स्थिति से बचने / स्पष्ट करने में मदद करते हैं।
माइकलमोसर

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, refpair.2 से अधिक समय तक नहीं रह सकता है और इसके विपरीत - इसलिए दोनों ही refs को एक ही मालिक के साथ कुछ इंगित करने की आवश्यकता है। हालाँकि पहले केवल इसके लिए आवश्यक है कि RefPair अपने दोनों भागों की रूपरेखा तैयार करे।
लौगलिक

2
@AB, यह संकलित करता है क्योंकि दोनों जीवन काल एकीकृत हैं - क्योंकि स्थानीय जीवन रेखाएं छोटी होती हैं 'static, 'staticजिनका उपयोग हर जगह किया जा सकता है जहां स्थानीय जीवन रेखाओं का उपयोग किया जा सकता है, इसलिए आपके उदाहरण pमें इसका जीवनकाल पैरामीटर स्थानीय जीवनकाल के रूप में माना जाएगा y
व्लादिमीर मतवेव

5
@AB का RefPair<'a>(&'a u32, &'a u32)अर्थ है कि 'aदोनों इनपुट जीवन काल का प्रतिच्छेदन होगा, अर्थात इस मामले में जीवनकाल y
fjh

1
@llogiq "के लिए आवश्यक है कि RefPair अपने दोनों हिस्सों की रूपरेखा तैयार करे"? हालांकि मैं इसके विपरीत था ... एक और u32 अभी भी RefPair के बिना समझ में आ सकता है, जबकि एक RefPair अपने refs मृत के साथ अजीब होगा।
१३'१६ को

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

आपके उदाहरण से काम नहीं करने का कारण सिर्फ इसलिए है कि रस्ट में केवल स्थानीय जीवनकाल और प्रकार का अनुमान है। आप जो सुझाव दे रहे हैं, वह वैश्विक संदर्भ में है। जब भी आपके पास एक संदर्भ होता है जिसका जीवनकाल समाप्त नहीं हो सकता है, तो इसे एनोटेट किया जाना चाहिए।


1

रस्ट के एक नवागंतुक के रूप में, मेरी समझ यह है कि स्पष्ट जीवनकाल दो उद्देश्यों की सेवा करता है।

  1. किसी फ़ंक्शन पर एक स्पष्ट जीवनकाल एनोटेशन डालना कोड के प्रकार को प्रतिबंधित करता है जो उस फ़ंक्शन के अंदर दिखाई दे सकता है। स्पष्ट जीवनकाल संकलक को यह सुनिश्चित करने की अनुमति देता है कि आपका कार्यक्रम वही कर रहा है जो आपने इरादा किया था।

  2. यदि आप (संकलक) चाहते हैं कि कोड का एक टुकड़ा वैध है या नहीं, तो आप (संकलक) को यह कहा जाता है कि वह प्रत्येक फ़ंक्शन के अंदर दिखना नहीं चाहता। यह उन कार्यों के एनोटेशन पर एक नज़र रखने के लिए पर्याप्त है जो सीधे उस कोड के कोड द्वारा कहे जाते हैं। यह आपके कार्यक्रम को आपके (कंपाइलर) के बारे में तर्क करने में बहुत आसान बनाता है, और संकलन समय को प्रबंधनीय बनाता है।

बिंदु 1 पर, पायथन में लिखे गए निम्नलिखित कार्यक्रम पर विचार करें:

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 दूसरी के बजाय पहली पंक्ति में लौट रहा है; सौभाग्य कि डिबगिंग।

रस्ट में लिखे एक समान कार्यक्रम के बजाय विचार करें:

#[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

मुझे लगता है कि किसी दिए गए रेफरी के बारे में एक आजीवन एनोटेशन प्राप्त करने के दायरे में ही मान्य है, जबकि यह स्रोत के दायरे में मान्य है। समान जीवनकाल में अधिक संदर्भों की घोषणा करने से स्कैप्स का विलय हो जाता है, जिसका अर्थ है कि सभी स्रोत रेफ को इस अनुबंध को पूरा करना होगा। इस तरह के एनोटेशन अनुबंध की पूर्ति के लिए संकलक की जांच करने की अनुमति देते हैं।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.