तुम्हाला तो अनुभव माहित आहे का, जेव्हा तुम्ही एखादी नवीन भाषा शिकायला सुरुवात करता आणि तुम्हाला वाटते की तुम्ही वेगाने पुढे जाल, पण त्याऐवजी तुम्ही पहिल्याच पावलावर अडखळता? माझ्यासोबत Rust शिकताना अगदी तसेच झाले. माझ्याकडे एक उत्तम JavaScript utility होती — जी काही arrays parse आणि mutate करत असे — आणि मला वाटले की मी कॉफी ब्रेक दरम्यान ती पुन्हा लिहून काढू शकेन. मी माझा editor उघडला, मला जे नैसर्गिक वाटले ते कोड टाईप केले आणि compiler चालवला. तो अक्षरशः फुटला. सिंटॅक्स एरर्समुळे नाही. किंवा सेमीकोलन विसरल्यामुळे नाही. त्याने मला सांगितले की मी अशा व्हॅल्यूचा वापर करण्याचा प्रयत्न केला आहे जी आधीच 'move' झाली आहे. मी स्क्रीनकडे पाहत राहिलो. Move? मी काहीही हलवले नाही. मी फक्त ती दुसऱ्या व्हेरिएबलला असाइन केली होती.

JavaScript चा कम्फर्ट झोन

JavaScript तुम्हाला डेटाकडे एका सामायिक व्हाईटबोर्डप्रमाणे (communal whiteboard) वागवायला शिकवते. तुम्ही एखादा object किंवा array घोषित करता, तो एखाद्या function ला देता, त्या function ला एखादी property push करू देता आणि मग तो पुढे कुठे तरी पाठवता. Garbage collector पार्श्वभूमीवर एका अदृश्य सफाई कामगाराप्रमाणे बसलेला असतो, तुम्ही सोडून दिलेली कोणतीही गोष्ट साफ करण्यासाठी तो तयार असतो. तुम्ही कधीही विचारत नाही, "या string चा मालक कोण आहे?" तुम्ही विचारता, "मी ती वाचू शकतो का?" जर उत्तर 'हो' असेल, तर तुम्ही तिचा वापर करता. जर तुम्हाला दुसऱ्या संदर्भची (reference) गरज असेल, तर तुम्ही फक्त let b = a म्हणता आणि पुढे जाता. दोन्ही variables एकाच मेमरी भागाकडे निर्देश करतात आणि जर a मध्ये बदल (mutate) झाला, तर b ला तो बदल लगेच दिसतो. हे सोयीचे आहे. हे गोंधळात टाकणारे देखील आहे, पण तो गोंधळ क्वचितच तुम्हाला त्रास देतो कारण runtime सर्व साफसफाई स्वतः हाताळते.

पहिला Compiler Shock

Rust तुमच्यावर विश्वास ठेवत नाही. हे ऐकायला कठोर वाटते, पण हीच पहिली गोष्ट आहे जी तुम्ही शिकता. ही भाषा तीन कडक नियमांच्या ownership system वर आधारित आहे. पहिले, प्रत्येक व्हॅल्यूचा नेमका एकच मालक (owner) असतो. दुसरे, एका वेळी फक्त एकच मालक असू शकतो. तिसरे, जेव्हा तो मालक 'scope' च्या बाहेर जातो, तेव्हा Rust ती व्हॅल्यू आपोआप काढून टाकते (drop करते). कोणताही garbage collector नाही. जोपर्यंत तुम्ही स्वतःहून निवडत नाही, तोपर्यंत पार्श्वभूमीवर कोणतेही reference counting नसते. फक्त हे तीन नियम, जे तुमचा कोड रन होण्यापूर्वीच compiler द्वारे लागू केले जातात.

जेव्हा मी त्या JavaScript utility ची Rust आवृत्ती लिहिली, तेव्हा मी तेच केले जे मला नैसर्गिक वाटले. मी एक vector तयार केला (जो array साठी Rust चे उत्तर आहे) आणि तो दुसऱ्या व्हेरिएबलला असाइन केला. त्यानंतर मी पहिल्या व्हेरिएबलचा वापर करण्याचा प्रयत्न केला. Compiler ने नकार दिला. JavaScript मध्ये, let b = a संदर्भ (reference) कॉपी करते. Rust मध्ये, ते मालकी हक्क (ownership) हलवते (move करते). मूळ व्हेरिएबल अवैध (invalid) ठरते. Compiler हे यासाठी करते जेणेकरून दोन मार्ग चुकून एकाच मेमरीवर प्रक्रिया करणार नाहीत, ज्यामुळे इतर सिस्टममध्ये 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 मध्ये, तीच अंतर्ज्ञान (intuition) तुमची टाईपिंग पूर्ण होण्यापूर्वीच compiler कडून तुम्हाला नाकारली जाईल. एकदा का b कडे vector ची मालकी आली की, a हे केवळ एक रिकामे नाव उरते. तुम्ही त्यात काहीही push करू शकत नाही. तुम्ही त्याच्याकडे पाहू देखील शकत नाही. आता ती मेमरी b ची आहे.

जोपर्यंत तुम्हाला हे समजत नाही की यामुळे काय रोखले जाते, तोपर्यंत हे एखाद्या शिक्षणासारखे वाटते. जर दोन व्हेरिएबल्स कोणत्याही समन्वयाशिवाय एकाच heap डेटाला mutate करू शकले असते, तर मेमरी करप्शनचा (memory corruption) धोका निर्माण झाला असता. Rust ही मालकी हस्तांतरण प्रक्रिया स्पष्ट (explicit) करून बग्सची ही संपूर्ण श्रेणी काढून टाकते. Compiler येथे केवळ नियम पाळत नाहीये, तर तो एक द्वारपाल (gatekeeper) म्हणून काम करत आहे. तो तुम्हाला प्रत्येक टप्प्यावर निर्णय घेण्यास भाग पाडतो की तुमच्या कोडचा कोणता भाग कोणत्या डेटासाठी जबाबदार आहे.

Borrowing: एक उपाय जो प्रत्यक्षात एक वैशिष्ट्य (Feature) आहे

अर्थात, जर प्रत्येक असाइनमेंटने मालकी हक्क कायमचा हस्तांतरित केला असता, तर प्रोग्राम लिहिणे खूप थकवणारे झाले असते. तुम्हाला प्रत्येक गोष्ट clone करावी लागली असती, ज्यामुळे मेमरी आणि वेग वाया गेला असता. Rust हे borrowing द्वारे सोडवते.

Borrowing तुम्हाला एखादी व्हॅल्यू न घेता तिचा वापर करण्याची परवानगी देते. याचे दोन प्रकार आहेत आणि त्यातील फरक महत्त्वाचा आहे.

  • Immutable borrows, जे &T असे लिहिले जातात: हे तुम्हाला डेटा वाचू देतात. तुम्ही एकाच वेळी हवे तितके घेऊ शकता