Rust में जेनेरिक्स: इनके बिना काम क्यों नहीं चलता?
\n\nजब आप Rust में लिखना शुरू करते हैं, तो ऐसा लगता है कि टाइप सिस्टम आपका दुश्मन है। कंपाइलर हर कॉमा पर डांटता है, और Generic शब्द सुनते ही घुटने काँपने लगते हैं। लेकिन वास्तव में जेनेरिक्स (जनरलाइज़्ड टाइप) आपके सबसे अच्छे दोस्त हैं। वे आपको प्रदर्शन खोए बिना लचीला, सुरक्षित और पुन: प्रयोग करने योग्य कोड लिखने की अनुमति देते हैं।
इस लेख में हम समझेंगे कि Rust में generics क्या हैं, उन्हें फ़ंक्शन, स्ट्रक्चर और ट्रेट्स में कैसे उपयोग करें। आप देखेंगे कि जेनेरिक प्रोग्रामिंग कोई जादू नहीं, बल्कि एक शक्तिशाली उपकरण है जो आपके कोड को साफ और तेज़ बनाता है।
\n\nजेनेरिक्स क्या हैं और इनकी आवश्यकता क्यों है?
\n\nकल्पना करें कि आप एक फ़ंक्शन लिख रहे हैं जो किसी ऐरे में अधिकतम एलिमेंट ढूंढता है। यदि जेनेरिक्स न होते, तो आपको i32, f64, String आदि के लिए अलग-अलग इम्प्लीमेंटेशन लिखने पड़ते। यह लंबा, उबाऊ और DRY (Don't Repeat Yourself) सिद्धांत का उल्लंघन करने वाला है।
जेनेरिक्स इस समस्या को हल करते हैं। वे टाइप को पैरामीटराइज़ करने की अनुमति देते हैं: आप एक फ़ंक्शन लिखते हैं जो किसी भी डेटा टाइप के साथ काम करता है। कंपाइलर स्वयं प्रत्येक विशिष्ट मामले के लिए कोड के आवश्यक संस्करण उत्पन्न करता है। इस प्रक्रिया को मोनोमोर्फ़ाइज़ेशन कहा जाता है।
\n\nजेनेरिक्स का सिंटैक्स: सरल से जटिल तक
\n\n1. जेनेरिक फ़ंक्शन
\n\nचलिए एक बुनियादी उदाहरण से शुरू करते हैं। मान लीजिए हम एक फ़ंक्शन बनाना चाहते हैं जो दिए गए मान को वापस लौटाता है (आइडेंटिटी फ़ंक्शन):
\n\nfn identity<T>(value: T) -> T {\n value\n}\n\nfn main() {\n let n = identity(42);\n let s = identity("hello");\n println!("{}, {}", n, s); // 42, hello\n}\n\nयहाँ <T> एक टाइप पैरामीटर है। T कोई भी टाइप हो सकता है: संख्या, स्ट्रिंग, स्ट्रक्चर। कॉल करने पर कंपाइलर स्वयं अनुमान लगाता है कि कौन सा टाइप उपयोग करना है। यह डायनामिक डिस्पैच (जैसे Java या Python में) के बजाय कंपाइल टाइम पर विशेष कोड जनरेशन के कारण काम करता है। यही कारण है कि Rust प्रदर्शन का त्याग नहीं करता।
2. टाइप प्रतिबंध (Trait Bounds)
\n\nलेकिन क्या होगा अगर हम सिर्फ़ मान पास नहीं करना चाहते, बल्कि दो संख्याओं की तुलना करना चाहते हैं? केवल <T> काम नहीं करेगा, क्योंकि सभी टाइप के लिए तुलना ऑपरेशन परिभाषित नहीं है। हमें यह बताना होगा कि T को PartialOrd ट्रेट को इम्प्लीमेंट करना चाहिए:
fn max<T: PartialOrd>(a: T, b: T) -> T {\n if a > b { a } else { b }\n}\n\nfn main() {\n println!("{}", max(10, 20)); // 20\n println!("{}", max(3.14, 2.71)); // 3.14\n}\n\nT: PartialOrd लिखना एक trait bound है। यह कंपाइलर को बताता है: "केवल उन टाइप का उपयोग करें जिनके लिए तुलना क्रम परिभाषित है।" max में दो स्ट्रिंग पास करने का प्रयास करें - और यह काम करेगा, क्योंकि String भी PartialOrd को इम्प्लीमेंट करता है। लेकिन यदि आप बिना इम्प्लीमेंटेशन के कोई स्ट्रक्चर पास करते हैं - तो आपको कंपाइलेशन एरर मिलेगी।
3. एकाधिक प्रतिबंध
\n\nकभी-कभी आवश्यकता होती है कि टाइप एक साथ कई ट्रेट्स को संतुष्ट करे। उदाहरण के लिए, वह तुलनीय और क्लोन करने योग्य दोनों हो:
\n\nfn clone_and_max<T: PartialOrd + Clone>(a: T, b: T) -> T {\n let a_cloned = a.clone();\n let b_cloned = b.clone();\n if a_cloned > b_cloned { a } else { b }\n}\n\nT: PartialOrd + Clone सिंटैक्स का मतलब है कि टाइप को दोनों ट्रेट्स को इम्प्लीमेंट करना चाहिए। यह शक्तिशाली है, लेकिन इसका दुरुपयोग न करें: बहुत अधिक प्रतिबंध पठनीयता को जटिल बनाते हैं।
स्ट्रक्चर और एनम में जेनेरिक्स
\n\nजेनेरिक स्ट्रक्चर
\n\nजेनेरिक्स न केवल फ़ंक्शन के साथ, बल्कि यूज़र-डिफ़ाइंड टाइप के साथ भी बढ़िया काम करते हैं। क्लासिक उदाहरण Point स्ट्रक्चर है, जो किसी भी टाइप के निर्देशांक संग्रहीत कर सकता है:
struct Point<T> {\n x: T,\n y: T,\n}\n\nimpl<T> Point<T> {\n fn new(x: