परिचय: वह जाल जिसमें कई फंस चुके हैं
\nएक स्थिति की कल्पना करें: आप एक ऑनलाइन लाइब्रेरी के लिए एप्लिकेशन लिख रहे हैं। आपको अपनी खुद की Library क्लास चाहिए, जो किताबों को स्टोर करे, उन्हें सॉर्ट करे, खोजे और आँकड़े दिखाए। सबसे पहले जो विचार आता है वह है List<Book> से विरासत प्राप्त करना। आखिरकार, इसमें पहले से ही सब कुछ है: Add, Remove, इंडेक्सिंग, खोज। तार्किक? ऐसा लगता है, हाँ।
लेकिन अनुभवी C# डेवलपर्स आपको बताएंगे: «List<T> से कभी विरासत न लें»। Stack Overflow पर इस प्रश्न को हज़ारों बार देखा गया है, और यह व्यर्थ नहीं है। स्पष्ट सादगी के पीछे आर्किटेक्चर, प्रदर्शन और सबसे महत्वपूर्ण, ऑब्जेक्ट-ओरिएंटेड डिज़ाइन सिद्धांतों के उल्लंघन से जुड़ी गंभीर समस्याएँ छिपी हैं।
\nइस लेख में, हम List<T> के अंदर झाँकेंगे, समझेंगे कि इससे विरासत प्राप्त करना एक एंटी-पैटर्न क्यों है, और इसके बजाय क्या उपयोग करना चाहिए। तैयार हैं? चलिए शुरू करते हैं!
डिज़ाइन के दृष्टिकोण से List<T> में क्या खराबी है?
\nचलिए मुख्य बात से शुरू करते हैं: List<T> को पदानुक्रम के आधार के रूप में नहीं, बल्कि डायनामिक ऐरे के एक विशिष्ट, उच्च-प्रदर्शन कार्यान्वयन के रूप में डिज़ाइन किया गया था। इसका सार्वजनिक API संग्रह के साथ काम करने के तरीकों का एक सेट है, विस्तार के लिए कोई अनुबंध नहीं।
.NET स्रोतों में इसकी घोषणा देखें (सरलीकृत):
\npublic class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IList, ICollection, IEnumerable\n{\n private T[] _items;\n private int _size;\n \n public void Add(T item) { /* ... */ }\n public bool Remove(T item) { /* ... */ }\n // ... दर्जनों अन्य विधियाँ\n}\nध्यान दें: List<T> को abstract के रूप में चिह्नित नहीं किया गया है। यह ओवरराइड करने के लिए कोई वर्चुअल विधि प्रदान नहीं करता (कुछ को छोड़कर, जिनके बारे में बाद में बताया जाएगा)। इसका मतलब है कि आप सुरक्षित रूप से इसके व्यवहार को नहीं बदल सकते। यदि आप Add को ओवरराइड करते हैं, तो आप आंतरिक तर्क को तोड़ने का जोखिम उठाते हैं जो इस विधि पर निर्भर करता है।
समस्या #1: Liskov Substitution Principle (LSP) का उल्लंघन
\nलिस्कोव प्रतिस्थापन सिद्धांत कहता है: "वंशज वर्ग को पूर्वज के व्यवहार को बदलना नहीं, बल्कि पूरक करना चाहिए।" जब आप List<T> से विरासत प्राप्त करते हैं, तो आप वादा करते हैं कि आपकी क्लास का उपयोग वहाँ किया जा सकता है जहाँ List<T> की अपेक्षा की जाती है। लेकिन यह शायद ही कभी पूरा होता है।
उदाहरण: आप एक क्लास ReadOnlyLibrary : List<Book> बनाते हैं और Add को ओवरराइड करते हैं ताकि यह एक अपवाद फेंके:
public class ReadOnlyLibrary : List<Book>\n{\n public new void Add(Book book)\n {\n throw new InvalidOperationException("कलेक्शन केवल पढ़ने के लिए है!");\n }\n}\n\n// कोड में कहीं:\nList<Book> list = new ReadOnlyLibrary();\nlist.Add(new Book()); // ओह! कोई अपवाद नहीं होगा!\n// क्योंकि new ओवरराइड नहीं करता, बल्कि विधि को छुपाता है।\n// List.Add को कॉल किया जाएगा, हमारा नहीं।\nभले ही आप override का उपयोग करें (और List.Add वर्चुअल नहीं है), आप जोड़ने पर प्रतिबंध नहीं लगा सकते — इंटरफ़ेस IList इसकी अनुमति नहीं देता। आप बस अनुबंध तोड़ देंगे।
समस्या #2: एनकैप्सुलेशन का उल्लंघन
\nList<T> से विरासत प्राप्त करके, आप इसका पूरा सार्वजनिक API बाहर खोल देते हैं। आपकी Library क्लास को Sort, BinarySearch, Reverse, Insert आदि विधियाँ मिलेंगी। लेकिन क्या होगा यदि आप चाहते हैं कि किताबें हमेशा लेखक के अनुसार क्रमबद्ध हों? आपकी क्लास का उपयोगकर्ता library.Reverse() कॉल कर सकता है और इनवेरिएंट को तोड़ सकता है।
आप सभी विधियों को फिर से लिखे बिना इसे रोक नहीं सकते — और उनमें से दर्जनों हैं। यह एनकैप्सुलेशन का उल्लंघन है: आप आंतरिक कार्यान्वयन को बाहर उजागर कर रहे हैं जिसे आप नियंत्रित नहीं कर सकते।
\n\nअंदर झाँकें: List<T> आंतरिक रूप से कैसे काम करता है
\nसमझने के लिए