परिचय: Generics क्या हैं और Rust में इनकी ज़रूरत क्यों?
कल्पना करें कि आपको एक ऐसा फंक्शन लिखना है जो किसी लिस्ट में सबसे बड़ा एलिमेंट ढूंढे। पूर्णांकों (integers) के लिए अलग फंक्शन, दशमलव संख्याओं (floats) के लिए अलग, और स्ट्रिंग्स के लिए तीसरा। इससे कोड बार-बार दोहराया जाएगा और उसे मेंटेन करना मुश्किल हो जाएगा। Rust में इस समस्या का हल Generics (जेनेरिक प्रोग्रामिंग) है।
Generics आपको ऐसा कोड लिखने की अनुमति देते हैं जो अलग-अलग डेटा टाइप्स के साथ काम करता है, बिना सख्त टाइपिंग (strict typing) को खोए। हर टाइप के लिए फंक्शन डुप्लिकेट करने के बजाय, आप एक ही जेनेरिक वर्जन लिखते हैं। Rust कंपाइल टाइम पर विशिष्ट टाइप को सेट करता है और सही होने की जांच करता है। इससे मिलता है:
- कोड रीयूज़ — एक फंक्शन सभी टाइप्स के लिए काम करता है।
- सुरक्षा — सभी टाइप एरर कंपाइल टाइम पर ही पकड़ लिए जाते हैं।
- परफॉरमेंस — मोनोमॉर्फिज़ेशन (कंपाइलर हर इस्तेमाल किए गए टाइप के लिए अलग कोड जनरेट करता है) के कारण, जेनेरिक कोड उतनी ही तेज़ी से चलता है जितना स्पेशलाइज़्ड कोड।
इस लेख में, हम प्रैक्टिकल उदाहरणों के साथ Rust में Generics का उपयोग करना सीखेंगे, सरल फंक्शन से लेकर स्ट्रक्चर और ट्रेट्स तक।
1. फंक्शन में Generics: सार्वभौमिकता की ओर पहला कदम
सिंटैक्स और बेसिक उदाहरण
फंक्शन को जेनेरिक बनाने के लिए, एंगुलर ब्रैकेट्स <T> में टाइप पैरामीटर जोड़ें। आमतौर पर बड़े अक्षरों का उपयोग किया जाता है: T, U, V। यहाँ एक फंक्शन है जो दिए गए मान को वापस लौटाता है:
fn identity<T>(value: T) -> T {
value
}
fn main() {
let num = identity(42);
let text = identity("Hello");
println!("{} {}", num, text); // आउटपुट: 42 Hello
}
कंपाइलर दिए गए आर्गुमेंट के आधार पर खुद टाइप का पता लगाता है। आप टाइप को स्पष्ट रूप से भी बता सकते हैं: identity::<i32>(42)।
प्रतिबंध: ट्रेट्स को टाइप बाउंड्री के रूप में
क्या होगा अगर हम वैल्यूज़ की तुलना करना चाहते हैं? सीधे value1 > value2 लिखना काम नहीं करेगा — सभी टाइप्स तुलना को सपोर्ट नहीं करते। हमें यह बताना होगा कि टाइप को PartialOrd ट्रेट को इम्प्लीमेंट करना चाहिए:
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let numbers = vec![34, 50, 25, 100, 65];
println!("सबसे बड़ी संख्या: {}", largest(&numbers));
let chars = vec!['y', 'm', 'a', 'q'];
println!("सबसे बड़ा अक्षर: {}", largest(&chars));
}
यहाँ T: PartialOrd एक ट्रेट बाउंड है। यह गारंटी देता है कि टाइप T के लिए तुलना ऑपरेटर परिभाषित है। इस बाउंड के बिना, कोड कंपाइल नहीं होगा।
2. स्ट्रक्चर और एनम में Generics
जेनेरिक फील्ड वाले स्ट्रक्चर
Generics यूज़र-डिफ़ाइंड टाइप्स के साथ भी बढ़िया काम करते हैं। उदाहरण के लिए, Point स्ट्रक्चर किसी भी टाइप के कोऑर्डिनेट स्टोर कर सकता है:
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer_point = Point { x: 5, y: 10 };
let float_point = Point { x: 1.0, y: 4.0 };
// let mixed = Point { x: 5, y: 4.0 }; // एरर! दोनों फील्ड एक ही टाइप के होने चाहिए
}
अगर फील्ड अलग-अलग टाइप के हो सकते हैं, तो कई पैरामीटर का उपयोग करें:
struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let mixed = Point { x: 5, y: 4.0 }; // अब काम करता है
}
Generics के साथ एनम
स्टैंडर्ड एनम Option<T> जेनेरिक एनम का एक बेहतरीन उदाहरण है। आप अपना खुद का बना सकते हैं:
enum Result<T, E> {
Ok(T),
Err(E),
}
// उपयोग
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b