क्या आप उस अहसास को जानते हैं जब आप कोई नई भाषा सीखते हैं और उम्मीद करते हैं कि आप तेज़ी से दौड़ेंगे, लेकिन इसके बजाय आप पहले ही कदम पर लड़खड़ा जाते हैं? Rust के साथ मेरे साथ भी यही हुआ। मेरे पास एक बिल्कुल ठीक काम करने वाली JavaScript यूटिलिटी थी — कुछ ऐसा जो कुछ एरेज़ (arrays) को पार्स और म्यूटेट करती थी — और मैंने सोचा कि मैं इसे कॉफी ब्रेक के दौरान फिर से लिख लूँगा। मैंने अपना एडिटर खोला, वह कोड टाइप किया जो स्वाभाविक लग रहा था, और कंपाइलर चलाया। वह फट पड़ा। सिंटैक्स एरर (syntax errors) की वजह से नहीं। सेमीकोलन (semicolons) की कमी की वजह से भी नहीं। उसने मुझे बताया कि मैंने एक ऐसी वैल्यू का उपयोग करने की कोशिश की है जो पहले ही 'move' हो चुकी थी। मैं स्क्रीन को देखता रह गया। Moved? मैंने तो कुछ भी मूव नहीं किया। मैंने तो बस उसे दूसरे वेरिएबल को असाइन कर दिया था।
JavaScript का कंफर्ट ज़ोन
JavaScript आपको डेटा को एक साझा व्हाइटबोर्ड की तरह इस्तेमाल करना सिखाता है। आप एक ऑब्जेक्ट या एरे डिक्लेअर करते हैं, उसे किसी फंक्शन को सौंपते हैं, उस फंक्शन को एक प्रॉपर्टी पुश करने देते हैं, और फिर उसे कहीं और भेज देते हैं। गार्बेज कलेक्टर (garbage collector) बैकग्राउंड में एक अदृश्य सफाईकर्मी की तरह बैठा रहता है, जो आपके द्वारा छोड़ी गई चीज़ों को साफ करने का इंतज़ार करता है। आप कभी यह नहीं पूछते, "इस स्ट्रिंग का मालिक कौन है?" आप पूछते हैं, "क्या मैं इसे पढ़ सकता हूँ?" यदि उत्तर हाँ है, तो आप उसे छू लेते हैं। यदि आपको एक और रेफरेंस की आवश्यकता है, तो आप बस let b = a कहते हैं और आगे बढ़ जाते हैं। दोनों वेरिएबल्स मेमोरी के एक ही हिस्से की ओर इशारा करते हैं, और यदि a उसे म्यूटेट करता है, तो b बदलाव को तुरंत देख लेता है। यह सुविधाजनक है। यह अराजक (chaotic) भी है, लेकिन वह अराजकता आपको शायद ही कभी नुकसान पहुँचाती है क्योंकि रनटाइम सफाई का काम संभाल लेता है।
पहला कंपाइलर शॉक
Rust आप पर भरोसा नहीं करता है। यह सुनने में कठोर लग सकता है, लेकिन यह पहली चीज़ है जो आप सीखते हैं। यह भाषा तीन सख्त नियमों वाले ओनरशिप सिस्टम (ownership system) के इर्द-गिर्द बनी है। पहला, हर वैल्यू का ठीक एक ओनर होता है। दूसरा, एक समय में केवल एक ही ओनर हो सकता है। तीसरा, जब वह ओनर स्कोप (scope) से बाहर जाता है, तो Rust उस वैल्यू को अपने आप ड्रॉप कर देता है। कोई गार्बेज कलेक्टर नहीं। बैकग्राउंड में कोई रेफरेंस काउंटिंग नहीं, जब तक कि आप स्पष्ट रूप से इसे न चुनें। बस ये तीन नियम, जो आपके कोड के चलने से पहले ही कंपाइलर द्वारा लागू किए जाते हैं।
जब मैंने उस JavaScript यूटिलिटी का अपना Rust वर्शन लिखा, तो मैंने वही किया जो स्वाभाविक लगा। मैंने एक वेक्टर (vector) बनाया, जो एरे का Rust वर्शन है, और उसे दूसरे वेरिएबल को असाइन कर दिया। फिर मैंने पहले वाले का उपयोग करने की कोशिश की। कंपाइलर ने मना कर दिया। JavaScript में, let b = a रेफरेंस को कॉपी करता है। Rust में, यह ओनरशिप को 'move' कर देता है। मूल वेरिएबल अमान्य हो जाता है। कंपाइलर ऐसा यह सुनिश्चित करने के लिए करता है कि आपके पास गलती से एक ही मेमोरी पर दो रास्ते न हों, क्योंकि अन्य सिस्टम में डेटा रेस (data races) और use-after-free बग्स इसी तरह होते हैं।
Rust आपके खिलौने क्यों छीन लेता है
JavaScript में इस पैटर्न को देखें:
let a = [1, 2, 3];
let b = a;
a.push(4);
// Both a and b see 4.
यह आपकी स्वाभाविक आदत है। आप इस बारे में दोबारा नहीं सोचेंगे। Rust में, वही सहज ज्ञान आपको टाइपिंग पूरी करने से पहले ही कंपाइलर द्वारा रिजेक्ट कर देगा। एक बार जब b वेक्टर का मालिक बन जाता है, तो a सिर्फ एक खाली नाम रह जाता है। आप इसमें कुछ पुश नहीं कर सकते। आप इसे देख भी नहीं सकते। मेमोरी अब b की है।
यह एक सज़ा जैसा महसूस होता है जब तक कि आप यह न समझ लें कि यह किस चीज़ को रोकता है। यदि दो वेरिएबल्स बिना किसी तालमेल के एक ही हीप डेटा (heap data) को म्यूटेट कर सकते, तो आपको मेमोरी करप्शन का जोखिम उठाना पड़ता। Rust इस प्रकार के सभी बग्स को ट्रांसफर को स्पष्ट बनाकर खत्म कर देता है। कंपाइलर नखरे नहीं दिखा रहा है। वह एक गेटकीपर की तरह काम कर रहा है। वह आपको हर कदम पर यह तय करने के लिए मजबूर करता है कि आपके कोड का कौन सा हिस्सा किस डेटा के लिए जिम्मेदार है।
Borrowing: एक वर्कअराउंड जो वास्तव में एक फीचर है
बेशक, अगर हर असाइनमेंट हमेशा के लिए ओनरशिप ट्रांसफर कर देता, तो प्रोग्राम लिखना थकाऊ हो जाता। आपको हर चीज़ को क्लोन (clone) करना पड़ता, जिससे मेमोरी और स्पीड दोनों बर्बाद होते। Rust इसे 'borrowing' के ज़रिए हल करता है।
बॉरोइंग आपको किसी वैल्यू को लिए बिना उसका उपयोग करने की अनुमति देता है। इसके दो प्रकार हैं, और इनके बीच का अंतर महत्वपूर्ण है।
- Immutable borrows, जिन्हें
&Tलिखा जाता है: ये आपको डेटा पढ़ने की अनुमति देते हैं। आप जितने चाहें उतने रख सकते हैं...
