Solidity या Javascript बैकग्राउंड से आने वाले पाठकों को Rust में &, mut, <_>, unwrap(), और ? का उपयोग और सिंटैक्स अजीब (या यहाँ तक कि भद्दा) लग सकता है। यह अध्याय बताता है कि इन शब्दों का क्या अर्थ है।
चिंता न करें यदि सब कुछ तुरंत समझ में नहीं आता है। यदि आप सिंटैक्स परिभाषाएं भूल जाते हैं तो आप हमेशा बाद में इस ट्यूटोरियल पर वापस आ सकते हैं।
Ownership और Borrowing (references & deref ऑपरेटर *):
Rust Copy टाइप
& और * को समझने के लिए हमें सबसे पहले Rust में “copy टाइप” को समझना होगा। एक copy टाइप एक ऐसा डेटाटाइप है जो इतना छोटा होता है कि वैल्यू को कॉपी करने का ओवरहेड (overhead) नगण्य (trivial) होता है। निम्नलिखित वैल्यूज़ copy टाइप हैं:
- integers, unsigned, और floats integers
- booleans
- char
इनके “copy टाइप” होने का कारण यह है कि इनका एक छोटा और निश्चित आकार (fixed size) होता है।
दूसरी ओर, vectors, strings, और structs मनमाने ढंग से कितने भी बड़े हो सकते हैं, इसलिए वे copy टाइप नहीं हैं।
Rust, copy टाइप और non-copy टाइप के बीच अंतर क्यों करता है
निम्नलिखित Rust कोड पर विचार करें:
pub fn main() {
let a: u32 = 2;
let b: u32 = 3;
println!("{}", add(a, b)); // a and b a are copied to the add function
let s1 = String::from("hello");
let s2 = String::from(" world");
// if s1 and s2 are copied, this could be a huge data transfer
// if the strings are very long
println!("{}", concat(s1, s2));
}
// implementations of add() and concat() are not shown for brevity
// this code does not compile
कोड के पहले भाग में जहां a और b को जोड़ा गया है, केवल 64 बिट्स डेटा को वेरिएबल्स से फंक्शन में कॉपी करने की आवश्यकता होती है (32 बिट्स * 2 वेरिएबल्स)।
हालांकि स्ट्रिंग के मामले में, हमें हमेशा पहले से पता नहीं होता है कि हम कितना डेटा कॉपी कर रहे हैं। यदि स्ट्रिंग 1 GB लंबी होती, तो प्रोग्राम काफी धीमा (lag) हो जाता।
Rust चाहता है कि हम स्पष्ट रहें कि हम बड़े डेटा को कैसे हैंडल करना चाहते हैं। यह इसे पर्दे के पीछे कॉपी नहीं करेगा जैसा कि डायनामिक भाषाएं करती हैं।
इसलिए, जब हम कुछ ऐसा आसान काम करते हैं जैसे कि एक नई वेरिएबल में स्ट्रिंग को असाइन करना, तो Rust कुछ ऐसा करेगा जो बहुत से लोगों को अप्रत्याशित लग सकता है जैसा कि हम अगले भाग में देखेंगे।
Rust में Ownership
Non-copy टाइप (Strings, vectors, structs, आदि) के लिए, एक बार वेरिएबल को वैल्यू असाइन कर दिए जाने पर, वह वेरिएबल उस पर “own” (स्वामित्व) कर लेता है। Ownership के निहितार्थ जल्द ही प्रदर्शित किए जाएंगे।
निम्नलिखित कोड कंपाइल नहीं होगा। स्पष्टीकरण कमेंट्स में दिया गया है:
// Example of changing ownership on a non-copy datatype (string)
let s1 = String::from("abc");
// s2 becomes the owner of `String::from("abc")`
let s2 = s1;
// The following line will fail to compile because s1 can no longer access its string value.
println!("{}", s1);
// This line compiles successfully because s2 now owns the string value.
println!("{}", s2);
ऊपर दिए गए कोड को ठीक करने के लिए हमारे पास दो विकल्प हैं: & ऑपरेटर का उपयोग करें या s1 को क्लोन (clone) करें।
विकल्प 1: s2 को s1 का व्यू (view) दें
नीचे दिए गए कोड में, s1 से पहले लगे महत्वपूर्ण & पर ध्यान दें:
pub fn main() {
let s1 = String::from("abc");
let s2 = &s1; // s2 can now view `String::from("abc")` but not own it
println!("{}", s1); // This compiles, s1 still holds its original string value.
println!("{}", s2); // This compiles, s2 holds a reference to the string value in s1.
}
यदि हम चाहते हैं कि कोई अन्य वेरिएबल वैल्यू को “देख” (view) सके (यानी read-only एक्सेस प्राप्त करे), तो हम & ऑपरेटर का उपयोग करते हैं।
किसी अन्य वेरिएबल या फंक्शन को एक owned वेरिएबल का व्यू देने के लिए, हम इसके पहले & लगाते हैं।
यह सोचना मददगार हो सकता है कि & एक non-copy टाइप के लिए “view only” (केवल देखने के लिए) मोड है। जिसे हम “view only” कह रहे हैं, उसके लिए तकनीकी शब्द borrowing है।
विकल्प 2: s1 को क्लोन करें
हम किसी वैल्यू को कैसे क्लोन कर सकते हैं, यह समझने के लिए निम्नलिखित उदाहरण पर विचार करें:
fn main() {
let mut message = String::from("hello");
println!("{}", message);
message = message + " world";
println!("{}", message);
}
ऊपर दिया गया कोड उम्मीद के मुताबिक “hello” और फिर “hello world” प्रिंट करेगा।
हालांकि, यदि हम एक और वेरिएबल y जोड़ते हैं जो message को देखता है, तो कोड अब कंपाइल नहीं होगा:
// Does not compile
fn main() {
let mut message = String::from("hello");
println!("{}", message);
let mut y = &message; // y is viewing message
message = message + " world";
println!("{}", message);
println!("{}", y); // should y be "hello" or "hello world"?
}
Rust ऊपर दिए गए कोड को स्वीकार नहीं करता है, क्योंकि जब message को देखा (view) जा रहा हो, तो उसे फिर से असाइन (reassign) नहीं किया जा सकता।
यदि हम चाहते हैं कि y आगे चलकर message के साथ हस्तक्षेप किए बिना message की वैल्यू को कॉपी कर सके, तो हम इसके बजाय इसे क्लोन कर सकते हैं:
fn main() {
let mut message = String::from("hello");
println!("{:?}", message);
let mut y = message.clone(); // change this to clone
message = message + " world";
println!("{:?}", message);
println!("{:?}", y);
}
उपरोक्त कोड प्रिंट करेगा:
hello
hello world
hello
Ownership केवल non-copy टाइप के साथ एक समस्या है
यदि हम अपने String (जो एक non-copy टाइप है) को एक copy टाइप (जैसे एक integer) से बदलते हैं, तो हमें उपरोक्त किसी भी समस्या का सामना नहीं करना पड़ेगा। Rust खुशी-खुशी copy टाइप को कॉपी करेगा क्योंकि इसका ओवरहेड नगण्य है।
let s1 = 3;
let s2 = s1;
println!("{}", s1);
println!("{}", s2);
mut कीवर्ड
डिफ़ॉल्ट रूप से, Rust में सभी वेरिएबल अपरिवर्तनीय (immutable) होते हैं जब तक कि mut कीवर्ड निर्दिष्ट न किया गया हो।
निम्नलिखित कोड कंपाइल नहीं होगा:
pub fn main() {
let counter = 0;
counter = counter + 1;
println!("{}", counter);
}
यदि हम ऊपर दिए गए कोड को कंपाइल करने का प्रयास करते हैं तो हमें निम्नलिखित त्रुटि मिलेगी:

सौभाग्य से, यदि आप mut कीवर्ड शामिल करना भूल जाते हैं, तो कंपाइलर आमतौर पर इतना स्मार्ट होता है कि वह गलती को स्पष्ट रूप से बता सके। निम्नलिखित कोड में mut कीवर्ड डाला गया है जिससे कोड कंपाइल हो जाता है:
pub fn main() {
let mut counter = 0;
counter = counter + 1;
println!("{}", counter);
}
Rust में Generics: < > सिंटैक्स
आइए एक ऐसे फंक्शन पर विचार करें जो एक मनमाने (arbitrary) टाइप की वैल्यू लेता है और एक struct लौटाता है जिसमें foo फील्ड में वह वैल्यू होती है। हर संभावित टाइप के लिए बहुत सारे फंक्शन लिखने के बजाय, हम एक generic का उपयोग कर सकते हैं।
नीचे दिया गया उदाहरण struct एक i32 या एक bool हो सकता है।
// derive the debug trait so we can print the struct to the console
#[derive(Debug)]
struct MyValues<T> {
foo: T,
}
pub fn main() {
let first_struct: MyValues<i32> = MyValues { foo: 1 }; // foo has type i32
let second_struct: MyValues<bool> = MyValues { foo: false }; // foo has type bool
println!("{:?}", first_struct);
println!("{:?}", second_struct);
}
यह उपयोगी क्यों है, इसका कारण यह है: जब हम Solana में वैल्यूज़ को “इन स्टोरेज” (in storage) स्टोर करते हैं, तो हम बहुत फ्लेक्सिबल होना चाहते हैं कि क्या हम कोई संख्या, स्ट्रिंग, या कुछ और स्टोर करने जा रहे हैं।
यदि हमारे struct में एक से अधिक फील्ड्स होते, तो टाइप्स को पैरामीटराइज़ (parameterize) करने का सिंटैक्स इस प्रकार है:
struct MyValues<T, U> {
foo: T,
bar: U,
}
Generics, Rust में एक बहुत बड़ा विषय है, इसलिए हम यहाँ किसी भी तरह से पूरी जानकारी नहीं दे रहे हैं। हालाँकि, अधिकांश Solana प्रोग्राम्स की अच्छी समझ प्राप्त करने के लिए यह पर्याप्त है।
Options, Enums, और Deref *
Options और enums का महत्व दिखाने के लिए, आइए निम्नलिखित उदाहरण पर विचार करें:
fn main() {
let v = Vec::from([1,2,3,4,5]);
assert!(v.iter().max() == 5);
}
यह कोड निम्नलिखित त्रुटि के साथ कंपाइल होने में विफल रहता है:
6 | assert!(v.iter().max() == 5);
| ^ expected `Option<&{integer}>`, found integer
max() का आउटपुट एक integer नहीं है क्योंकि यह एक कॉर्नर केस (corner case) है कि वेक्टर v खाली हो सकता है।
Rust Option
इस कॉर्नर केस को संभालने के लिए, Rust इसके बजाय एक Option लौटाता है। एक Option एक enum है जिसमें या तो अपेक्षित वैल्यू हो सकती है, या एक विशेष वैल्यू हो सकती है जो यह दर्शाती है कि “वहाँ कुछ भी नहीं था।”
एक Option को उसके अंतर्निहित टाइप (underlying type) में बदलने के लिए, हम unwrap() का उपयोग करते हैं। यदि हमें “कुछ नहीं” (nothing) प्राप्त होता है, तो unwrap() एक panic उत्पन्न करेगा, इसलिए हमें इसका उपयोग केवल उन स्थितियों में करना चाहिए जहाँ हम चाहते हैं कि panic उत्पन्न हो या हम सुनिश्चित हों कि हमें खाली वैल्यू नहीं मिलेगी।
कोड को उम्मीद के मुताबिक काम करने के लिए, हम निम्नलिखित कर सकते हैं:
fn main() {
let v = Vec::from([1,2,3,4,5]);
assert!(v.iter().max().unwrap() == 5);
}
Deref * ऑपरेटर
लेकिन यह अभी भी काम नहीं करता है! इस बार हमें एक त्रुटि मिलती है
19 | assert!(v.iter().max().unwrap() == 5);
| ^^ no implementation for `&{integer} == {integer}`
समानता (equality) के बाईं ओर का पद एक integer का व्यू (यानी &) है और दाईं ओर का पद एक वास्तविक integer है।
एक integer के “व्यू” को एक सामान्य integer में बदलने के लिए, हमें “dereference” ऑपरेशन का उपयोग करने की आवश्यकता है। यह तब होता है जब हम वैल्यू से पहले * ऑपरेटर लगाते हैं।
fn main() {
let v = Vec::from([1,2,3,4,5]);
assert!(*v.iter().max().unwrap() == 5);
}
चूंकि एरे (array) के तत्व (elements) copy टाइप हैं, deref ऑपरेटर शांति से (silently) max().unwrap() द्वारा लौटाए गए 5 को कॉपी कर लेगा।
आप * को मूल वैल्यू को प्रभावित किए बिना & को “अनडू” (undoing) करने के रूप में सोच सकते हैं।
Non-copy टाइप पर ऑपरेटर का उपयोग करना एक जटिल विषय है। अभी के लिए, आपको केवल यह जानने की आवश्यकता है कि यदि आपको एक copy टाइप का व्यू (borrow) प्राप्त होता है और इसे “सामान्य” (normal) टाइप में बदलने की आवश्यकता है, तो * ऑपरेटर का उपयोग करें।
Rust में Result बनाम Option
एक Option का उपयोग तब किया जाता है जब हमें कुछ “खाली” (empty) प्राप्त हो सकता है। एक Result (वही Result जो Anchor प्रोग्राम्स लौटा रहे हैं) का उपयोग तब किया जाता है जब हमें कोई त्रुटि प्राप्त हो सकती है।
Result Enum
Rust में Result<T, E> enum का उपयोग तब किया जाता है जब किसी फंक्शन का ऑपरेशन या तो सफल हो सकता है और टाइप T (एक generic टाइप) की वैल्यू लौटा सकता है, या विफल हो सकता है और टाइप E (generic error टाइप) की त्रुटि लौटा सकता है। इसे उन ऑपरेशन्स को संभालने के लिए डिज़ाइन किया गया है जिनका परिणाम या तो सफल हो सकता है या त्रुटि की स्थिति हो सकती है।
enum Result<T, E> {
Ok(T),
Err(E),
}
Rust में, ? ऑपरेटर का उपयोग Result<T, E> enum के लिए किया जाता है, जबकि unwrap() का उपयोग Result<T, E> और Option<T> enums दोनों के लिए किया जाता है।
? ऑपरेटर
? ऑपरेटर का उपयोग केवल उन फंक्शन्स में किया जा सकता है जो Result लौटाते हैं क्योंकि यह या तो Err या Ok लौटाने के लिए एक सिंटैक्टिक शुगर (syntactic sugar) है।
? ऑपरेटर का उपयोग Result<T, E> enum से डेटा निकालने और यदि फंक्शन का निष्पादन (execution) सफल होता है तो OK(T) वैरिएंट लौटाने के लिए किया जाता है, या यदि कोई त्रुटि होती है तो त्रुटि Err(E) को ऊपर भेजने (bubble up) के लिए किया जाता है। unwrap() मेथड उसी तरह काम करती है लेकिन Result<T, E> और Option<T> दोनों enums के लिए, हालाँकि, इसका उपयोग सावधानी से किया जाना चाहिए क्योंकि यदि कोई त्रुटि होती है तो यह प्रोग्राम को क्रैश कर सकती है।
अब, नीचे दिए गए निम्नलिखित कोड पर विचार करें:
pub fn encode_and_decode(_ctx: Context<Initialize>) -> Result<()> {
// Create a new instance of the `Person` struct
let init_person: Person = Person {
name: "Alice".to_string(),
age: 27,
};
// Encode the `init_person` struct into a byte vector
let encoded_data: Vec<u8> = init_person.try_to_vec().unwrap();
// Decode the encoded data back into a `Person` struct
let data: Person = decode(_ctx, encoded_data)?;
// Logs the decoded person's name and age
msg!("My name is {:?}, I am {:?} years old.", data.name, data.age);
Ok(())
}
pub fn decode(_accounts: Context<Initialize>, encoded_data: Vec<u8>) -> Result<Person> {
// Decode the encoded data back into a `Person` struct
let decoded_data: Person = Person::try_from_slice(&encoded_data).unwrap();
Ok(decoded_data)
}
try_to_vec() मेथड एक struct को बाइट वेक्टर में एनकोड करती है और एक Result<T, E> enum लौटाती है जहाँ T बाइट वेक्टर होता है, जबकि unwrap() मेथड का उपयोग OK(T) से बाइट वेक्टर की वैल्यू निकालने के लिए किया जाता है। यदि मेथड struct को बाइट वेक्टर में बदलने में विफल रहती है तो यह प्रोग्राम को क्रैश कर देगा।
RareSkills के साथ और जानें
यह ट्यूटोरियल हमारे मुफ़्त Solana कोर्स का हिस्सा है।
मूल रूप से 14 फरवरी, 2024 को प्रकाशित