Non-lexical lifetimes क्या है? Rust के सबसे बड़े सुधार से परिचय
\n\nजो कोई भी Rust में कुछ हफ्तों से कोड लिख रहा है, वह इस स्थिति से परिचित होगा: कोड पूरी तरह से तार्किक और सुरक्षित है, लेकिन कंपाइलर (खासकर इसका सख्त borrow checker) इसे कंपाइल करने से मना कर देता है। आप एरर को देखकर सोचते हैं: "लेकिन मैं तो इस रेफरेंस का अब उपयोग नहीं कर रहा! यह क्यों शिकायत कर रहा है?"
\n\nRust के संस्करण 1.31 (2018 edition) से पहले, इसका उत्तर सरल था: "क्योंकि वेरिएबल का lifetime (जीवनकाल) उसके स्कोप (दृश्यता क्षेत्र) से निर्धारित होता है — लेक्सिकली।" इसे lexical lifetimes कहते हैं। Rust 2018 से शुरू करके, कंपाइलर अधिक समझदार हो गया। इसे non-lexical lifetimes (NLL) मिला — गैर-लेक्सिकल जीवनकाल।
\n\nइस लेख में, हम समझेंगे कि यह क्या है, इसके आने से Rust डेवलपर्स के लिए क्यों क्रांति आई, और यह कोड को कैसे साफ और अधिक सहज बनाता है। हम थ्योरी में गहराई से नहीं जाएंगे, बल्कि व्यावहारिक उदाहरणों पर ध्यान केंद्रित करेंगे।
\n\nLexical lifetimes की समस्या: borrow checker इतना सख्त क्यों था
\n\nआइए याद करें कि Rust में बॉरोइंग (उधार लेना) कैसे काम करता है। हमारे पास नियम हैं:
\n- \n
- किसी भी समय, या तो एक mutable रेफरेंस (&mut T) हो सकता है, या कितनी भी immutable रेफरेंस (&T) हो सकती हैं। \n
- रेफरेंस हमेशा वैध होनी चाहिए (डिलीट किए गए डेटा को रेफर नहीं कर सकते)। \n
NLL से पहले, कंपाइलर रेफरेंस के lifetime को उसके लेक्सिकल स्कोप से निर्धारित करता था। यानी { से } तक, जहां वेरिएबल घोषित किया गया है। अगर कोई रेफरेंस किसी ब्लॉक के अंदर बनाई गई है, तो वह उस ब्लॉक के अंत तक "जीवित" रहती है, भले ही आप उसका उपयोग न कर रहे हों।
इस क्लासिक उदाहरण को देखें, जो Rust के पुराने संस्करणों में कंपाइल नहीं होता था:
\n\nfn main() {\n let mut data = vec![1, 2, 3];\n\n let x = &data[0]; // immutable रेफरेंस\n\n data.push(4); // mutable बॉरो करने का प्रयास\n\n println!("{}", x); // immutable रेफरेंस का उपयोग\n}\n\nमानवीय दृष्टिकोण से: हमने रेफरेंस x बनाया, फिर वेक्टर को बदला, फिर x का उपयोग किया। लेकिन push कॉल करने पर वेक्टर data मेमोरी को पुनः आवंटित कर सकता है, और रेफरेंस x अमान्य हो जाएगी। कंपाइलर सही है — यह खतरनाक है।
लेकिन क्या होगा अगर हम रेफरेंस का उपयोग बदलाव से पहले करें? यहाँ असली दर्द है:
\n\nfn main() {\n let mut name = String::from("Alice");\n\n let first_char = &name[0..1]; // immutable रेफरेंस\n println!("First char: {}", first_char); // इसका उपयोग करें\n\n name.push_str(" Johnson"); // स्ट्रिंग को बदलें\n println!("Full name: {}", name);\n}\n\nयह कोड सुरक्षित है। हमने रेफरेंस first_char का उपयोग किया, इसे प्रिंट किया, और फिर स्ट्रिंग को बदला। लेकिन Rust 2018 से पहले, कंपाइलर एक एरर देता था! क्यों? क्योंकि रेफरेंस first_char का लेक्सिकल lifetime फंक्शन main के अंत तक चलता था, यानी अंतिम क्लोजिंग ब्रेस तक। और इस अंतराल के अंदर mutable बॉरोइंग (push_str) निषिद्ध थी।
डेवलपर्स को जुगाड़ का उपयोग करना पड़ता था: रेफरेंस के lifetime को मैन्युअल रूप से सीमित करने के लिए अतिरिक्त ब्लॉक {} डालने पड़ते थे:
fn main() {\n let mut name = String::from("Alice");\n\n {\n let first_char = &name[0..1];\n println!("First char: {}", first_char);\n } // रेफरेंस first_char यहाँ खत्म हो जाती है\n\n name.push_str(" Johnson");\n println!("Full name: {}", name);\n}\n\nयह काम करता था, लेकिन असुविधाजनक था, कोड में अनावश्यक ब्रैकेट भर देता था और पढ़ने में कठिनाई पैदा करता था। खासकर जटिल फंक्शन में जहां कई बॉरोइंग होती हैं।
\n\nNon-lexical lifetimes (NLL): यह कैसे काम करता है
\n\nNLL का विचार अपनी सादगी में शानदार है: रेफरेंस का lifetime स्कोप से नहीं, बल्कि इस बात से निर्धारित होता है कि इसका वास्तव में उपयोग कहाँ किया जा रहा है।