डिजाइनरों के खतरे

नमस्कार, हेब्र! मैं आपको अर्नसे कल्लोव के लेख "पेरिल्स ऑफ कंस्ट्रक्टर्स" का अनुवाद प्रस्तुत करता हूं।


मेरे पसंदीदा जंग ब्लॉग पोस्ट में से एक है ग्रेडसन होरे द्वारा रस्ट शॉक्ड विदाउट रस्ट । मेरे लिए, पैर में गोली मार सकने वाली भाषा में किसी भी विशेषता की कमी आमतौर पर अभिव्यंजना से अधिक महत्वपूर्ण है। इस थोड़े से दार्शनिक निबंध में, मैं रूस्ट से गायब मेरी विशेष रूप से पसंदीदा विशेषता के बारे में बात करना चाहता हूं - निर्माणकर्ताओं के बारे में।


कंस्ट्रक्टर क्या है?


कंस्ट्रक्टर्स आमतौर पर OO भाषाओं में उपयोग किए जाते हैं। कंस्ट्रक्टर का काम बाकी दुनिया को देखने से पहले ऑब्जेक्ट को पूरी तरह से इनिशियलाइज़ करना है। पहली नज़र में, यह वास्तव में एक अच्छा विचार है:


  1. आप निर्माता को निर्माणकर्ता में सेट करते हैं
  2. प्रत्येक विधि आक्रमणकारियों के संरक्षण का ध्यान रखती है।
  3. साथ में, इन दो गुणों का मतलब है कि आप वस्तुओं को हमलावर के रूप में सोच सकते हैं, और विशिष्ट आंतरिक अवस्थाओं के रूप में नहीं।

यहां निर्माणकर्ता एक प्रेरण आधार की भूमिका निभाता है, जो एक नई वस्तु बनाने का एकमात्र तरीका है।


दुर्भाग्य से, इन तर्कों में एक छेद है: डिजाइनर खुद वस्तु को अधूरा राज्य में देखता है, जो कई समस्याएं पैदा करता है।


यह मान


जब कंस्ट्रक्टर ऑब्जेक्ट को इनिशियलाइज़ करता है, तो यह कुछ खाली अवस्था से शुरू होता है। लेकिन आप इस खाली स्थिति को मनमानी वस्तु के लिए कैसे परिभाषित करते हैं?


ऐसा करने का सबसे आसान तरीका सभी फ़ील्ड्स को उनके डिफ़ॉल्ट मानों पर सेट करना है: बूल के लिए गलत, संख्याओं के लिए 0, सभी लिंक के लिए अशक्त। लेकिन इस दृष्टिकोण के लिए सभी प्रकार के डिफ़ॉल्ट मानों की आवश्यकता होती है, और यह भाषा में कुख्यात नल का परिचय देता है। यह वह रास्ता है जिसे जावा ने लिया था: ऑब्जेक्ट के निर्माण की शुरुआत में, सभी फ़ील्ड 0 या शून्य हैं।


इस दृष्टिकोण के साथ, बाद में अशक्त से छुटकारा पाना बहुत मुश्किल होगा। सीखने के लिए एक अच्छा उदाहरण कोटलिन है। कोटलिन डिफ़ॉल्ट रूप से गैर-अशक्त प्रकारों का उपयोग करता है, लेकिन यह पहले से मौजूद जेवीएम शब्दार्थों के साथ काम करने के लिए मजबूर है। भाषा का डिजाइन इस तथ्य को अच्छी तरह से छिपाता है और व्यवहार में अच्छी तरह से लागू होता है, लेकिन यह अस्थिर है । दूसरे शब्दों में, कंस्ट्रक्टरों का उपयोग करते हुए, कोटलिन में नल की जांच को बायपास करना संभव है।


कोटलिन की मुख्य विशेषता तथाकथित "प्राथमिक निर्माता" बनाने का प्रोत्साहन है जो एक साथ एक क्षेत्र की घोषणा करता है और किसी भी कस्टम कोड को निष्पादित करने से पहले इसके लिए एक मूल्य प्रदान करता है:


class Person( val firstName: String, val lastName: String ) { ... } 

एक अन्य विकल्प: यदि क्षेत्र को कंस्ट्रक्टर में घोषित नहीं किया गया है, तो प्रोग्रामर को तुरंत इसे शुरू करना चाहिए:


 class Person(val firstName: String, val lastName: String) { val fullName: String = "$firstName $lastName" } 

प्रारंभ करने से पहले एक क्षेत्र का उपयोग करने का प्रयास सांख्यिकीय रूप से नकारा जाता है:


 class Person(val firstName: String, val lastName: String) { val fullName: String init { println(fullName) // :     fullName = "$firstName $lastName" } } 

लेकिन थोड़ी रचनात्मकता के साथ, कोई भी इन जांचों के आसपास पहुंच सकता है। उदाहरण के लिए, एक विधि कॉल इसके लिए उपयुक्त है:


 class A { val x: Any init { observeNull() x = 92 } fun observeNull() = println(x) //  null } fun main() { A() } 

इसके अलावा एक लैम्ब्डा के साथ इसे हथियाना (जो कोटलिन में इस प्रकार बनाया गया है: {args -> body}) भी उपयुक्त है:


 class B { val x: Any = { y }() val y: Any = x } fun main() { println(B().x) //  null } 

इस तरह के उदाहरण वास्तविकता में अवास्तविक लगते हैं (और यह है), लेकिन मुझे वास्तविक कोड में समान त्रुटि मिली (सॉफ्टवेयर विकास में कोलमोगोरोव की संभावना नियम 0-1: काफी बड़े डेटाबेस में, कोड का कोई भी टुकड़ा लगभग मौजूद होने की गारंटी है, कम से कम नहीं तो संकलक द्वारा सांख्यिकीय रूप से निषिद्ध, इस मामले में, यह लगभग निश्चित रूप से मौजूद नहीं है)।


कोटलिन के इस विफलता के साथ मौजूद होने का कारण जावा सहसंयोजक सरणियों के साथ ही हो सकता है: चेक अभी भी रनटाइम में होते हैं। अंत में, मैं उपरोक्त मामलों को संकलन के स्तर पर गलत बनाने के लिए कोटलिन प्रकार प्रणाली को जटिल नहीं करना चाहूंगा: मौजूदा सीमाओं (जेवीएम शब्दार्थ) को देखते हुए, रनटाइम में मान्यताओं का मूल्य / लाभ अनुपात स्थैतिक लोगों की तुलना में बहुत बेहतर है।


लेकिन क्या होगा अगर भाषा में प्रत्येक प्रकार के लिए एक उचित डिफ़ॉल्ट मान नहीं है? उदाहरण के लिए, C ++ में, जहां उपयोगकर्ता-परिभाषित प्रकार आवश्यक रूप से संदर्भ नहीं हैं, आप प्रत्येक क्षेत्र को शून्य नहीं बता सकते हैं और कहेंगे कि यह काम करेगा! इसके बजाय, C ++ फ़ील्ड के लिए प्रारंभिक मान सेट करने के लिए विशेष सिंटैक्स का उपयोग करता है: आरंभीकरण सूची:


 #include <string> #include <utility> class person { person(std::string first_name, std::string last_name) : first_name(std::move(first_name)) , last_name(std::move(last_name)) {} std::string first_name; std::string last_name; }; 

चूंकि यह एक विशेष वाक्यविन्यास है, इसलिए बाकी भाषा दोषपूर्ण तरीके से काम नहीं करती है। उदाहरण के लिए, प्रारंभिक सूचियों में मनमाना संचालन करना मुश्किल है, क्योंकि C ++ एक अभिव्यक्ति-उन्मुख भाषा नहीं है (जो अपने आप में सामान्य है)। प्रारंभिक सूचियों में होने वाले अपवादों के साथ काम करने के लिए, आपको भाषा की एक और अस्पष्ट विशेषता का उपयोग करने की आवश्यकता है।


कंस्ट्रक्टर से कॉलिंग के तरीके


कोटलिन संकेत से उदाहरण के रूप में, सब कुछ चिप्स में बिखर जाता है जैसे ही हम कंस्ट्रक्टर से एक विधि को कॉल करने का प्रयास करते हैं। मूल रूप से, तरीकों से उम्मीद की जाती है कि इसके माध्यम से सुलभ वस्तु पहले से ही पूरी तरह से निर्मित और सही है (इनवियर्स के अनुरूप)। लेकिन कोटलिन या जावा में, कुछ भी आपको निर्माणकर्ता से तरीकों को लागू करने से रोकता है, और इस तरह हम गलती से एक अर्ध-निर्मित वस्तु पर काम कर सकते हैं। डिजाइनर आक्रमणकारियों को स्थापित करने का वादा करता है, लेकिन साथ ही यह उनके संभावित उल्लंघन के लिए सबसे आसान जगह है।


विशेष रूप से अजीब चीजें तब होती हैं जब बेस क्लास कंस्ट्रक्टर एक विधि को एक व्युत्पन्न वर्ग में ओवरराइड करता है:


 abstract class Base { init { initialize() } abstract fun initialize() } class Derived: Base() { val x: Any = 92 override fun initialize() = println(x) //  null! } 

बस इसके बारे में सोचो: एक मनमाना वर्ग के कोड को उसके निर्माता को कॉल करने से पहले निष्पादित किया जाता है! समान सी ++ कोड और भी अधिक दिलचस्प परिणाम देगा। व्युत्पन्न वर्ग के कार्य को कॉल करने के बजाय, आधार वर्ग के कार्य को कहा जाएगा। यह थोड़ा समझ में आता है क्योंकि व्युत्पन्न वर्ग अभी तक आरंभीकृत नहीं किया गया है (याद रखें, हम सिर्फ यह नहीं कह सकते हैं कि सभी फ़ील्ड शून्य हैं)। हालांकि, यदि बेस क्लास में फ़ंक्शन शुद्ध आभासी है, तो इसकी कॉल UB को ले जाएगी।


डिजाइनर हस्ताक्षर


डिजाइनरों के लिए केवल आक्रमणकारियों का उल्लंघन ही समस्या नहीं है। उनके पास एक निश्चित नाम (खाली) और वापसी प्रकार (स्वयं वर्ग) के साथ एक हस्ताक्षर है। इससे लोगों के लिए डिजाइन ओवरलोड को समझना मुश्किल हो जाता है।


बैकफ़िल प्रश्न: एसटीडी :: वेक्टर <int> xs (92, 2) क्या है?

एक। दो लंबाई के वेक्टर 92

ख। [92, 92]

सी। [९ २, २]

वापसी मूल्य के साथ समस्याएं उत्पन्न होती हैं, एक नियम के रूप में, जब एक वस्तु बनाना असंभव है। आप केवल परिणाम नहीं दे सकते हैं <MyClass, io :: त्रुटि> या निर्माता से शून्य!


यह अक्सर इस तथ्य के पक्ष में एक तर्क के रूप में उपयोग किया जाता है कि अपवादों के बिना सी ++ का उपयोग करना मुश्किल है, और यह कि निर्माणकर्ताओं का उपयोग आपको अपवादों का उपयोग करने के लिए भी मजबूर करता है। हालाँकि, मुझे नहीं लगता कि यह तर्क सही है: फ़ैक्टरी विधियाँ इन दोनों समस्याओं को हल करती हैं, क्योंकि उनके मनमाने नाम हो सकते हैं और मनमाने प्रकार लौट सकते हैं। मेरा मानना ​​है कि निम्न पैटर्न कभी-कभी ओओ भाषाओं में उपयोगी हो सकता है:


  • सभी क्षेत्रों के मूल्यों को तर्क के रूप में लेने वाला एक निजी निर्माणकर्ता बनाएं और बस उन्हें असाइन करें। इस प्रकार, इस तरह के एक निर्माता रस्ट में संरचना शाब्दिक के रूप में काम करेंगे। यह किसी भी आक्रमणकारी के लिए भी जाँच कर सकता है, लेकिन इसे तर्क या क्षेत्र के साथ कुछ और नहीं करना चाहिए।


  • सार्वजनिक एपीआई के लिए उपयुक्त नाम और वापसी प्रकार के साथ सार्वजनिक कारखाने के तरीके प्रदान किए जाते हैं।



कंस्ट्रक्टरों के साथ एक समान समस्या यह है कि वे विशिष्ट हैं और इसलिए उन्हें सामान्यीकृत नहीं किया जा सकता है। C ++ में, "एक डिफ़ॉल्ट कंस्ट्रक्टर है" या "एक कॉपी कंस्ट्रक्टर है" को "कुछ वाक्य रचना कार्यों" की तुलना में अधिक सरलता से व्यक्त नहीं किया जा सकता है। इसकी तुलना रस्ट से करें, जहां इन अवधारणाओं में उपयुक्त हस्ताक्षर हैं:


 trait Default { fn default() -> Self; } trait Clone { fn clone(&self) -> Self; } 

डिजाइनरों के बिना जीवन


संरचना बनाने के लिए जंग का केवल एक ही तरीका है: सभी क्षेत्रों के लिए मूल्य प्रदान करना। फ़ैक्टरी फ़ंक्शंस, जैसे कि आम तौर पर स्वीकार किए जाते हैं नए, निर्माणकर्ताओं की भूमिका निभाते हैं, लेकिन, सबसे महत्वपूर्ण बात, वे आपको किसी भी तरीके को कॉल करने की अनुमति नहीं देते हैं जब तक कि आपके पास संरचना का कम से कम या अधिक सही उदाहरण न हो।


इस दृष्टिकोण का नुकसान यह है कि कोई भी कोड एक संरचना बना सकता है, इसलिए आक्रमणकारियों को बनाए रखने के लिए एक निर्माता के रूप में एक भी जगह नहीं है। व्यवहार में, यह गोपनीयता द्वारा आसानी से हल किया जाता है: यदि संरचना के क्षेत्र निजी हैं, तो यह संरचना केवल एक ही मॉड्यूल में बनाई जा सकती है। एक मॉड्यूल के भीतर, "संरचना बनाने के सभी तरीकों को नई विधि का उपयोग करना चाहिए" समझौते का पालन करना मुश्किल नहीं है। आप एक भाषा विस्तार की भी कल्पना कर सकते हैं जो आपको # [निर्माणकर्ता] विशेषता के साथ कुछ कार्यों को चिह्नित करने की अनुमति देता है, ताकि संरचना शाब्दिक सिंटैक्स केवल चिह्नित कार्यों में उपलब्ध हो। लेकिन, फिर से, अतिरिक्त भाषाई तंत्र मुझे बेमानी लगता है: स्थानीय सम्मेलनों के बाद बहुत कम प्रयास की आवश्यकता होती है।


व्यक्तिगत रूप से, मेरा मानना ​​है कि यह समझौता सामान्य रूप से अनुबंध प्रोग्रामिंग के लिए बिल्कुल वैसा ही दिखता है। "शून्य नहीं" या "सकारात्मक मूल्य" जैसे अनुबंध सर्वोत्तम प्रकारों में एन्कोड किए गए हैं। जटिल आक्रमणकारियों के लिए, प्रत्येक विधि में सिर्फ मुखर लिखना! (स्व। स्वाइप करना ()) इतना मुश्किल नहीं है। इन दो पैटर्न के बीच भाषा के स्तर पर या मैक्रोज़ पर आधारित # [पूर्व] और # [पोस्ट] स्थितियों के लिए बहुत कम जगह है।

स्विफ्ट के बारे में क्या?


स्विफ्ट एक और दिलचस्प भाषा है जो डिज़ाइन तंत्र पर एक नज़र डालने लायक है। कोटलिन की तरह, स्विफ्ट एक शून्य सुरक्षित भाषा है। कोटलिन के विपरीत, स्विफ्ट की अशक्त जांच अधिक मजबूत है, इसलिए भाषा निर्माणकर्ताओं द्वारा किए गए नुकसान को कम करने के लिए दिलचस्प चाल का उपयोग करती है।


सबसे पहले , स्विफ्ट नामित तर्कों का उपयोग करता है, और यह "सभी निर्माणकर्ताओं का समान नाम है" के साथ थोड़ी मदद करता है। विशेष रूप से, एक ही प्रकार के मापदंडों वाले दो निर्माणकर्ता कोई समस्या नहीं हैं:


 Celsius(fromFahrenheit: 212.0) Celsius(fromKelvin: 273.15) 

दूसरे , समस्या को हल करने के लिए "निर्माता ऑब्जेक्ट के वर्ग की आभासी विधि को कॉल करता है जो अभी तक पूरी तरह से नहीं बनाया गया है" स्विफ्ट एक अच्छी तरह से सोचा-आउट दो-चरण आरंभीकरण प्रोटोकॉल का उपयोग करता है। हालांकि आरंभीकरण सूचियों के लिए कोई विशेष वाक्यविन्यास नहीं है, संकलक सांख्यिकीय रूप से जांचता है कि निर्माणकर्ता के शरीर का सही और सुरक्षित रूप है। उदाहरण के लिए, कॉलिंग के तरीके कक्षा के सभी क्षेत्रों और उसके वंश के बाद ही संभव हैं।


तीसरा , भाषा के स्तर पर, कंस्ट्रक्टर्स के लिए समर्थन है, जिसके कॉल विफल हो सकते हैं। कंस्ट्रक्टर को अशक्त के रूप में नामित किया जा सकता है, जो क्लास को कॉल करने का परिणाम बनाता है। कंस्ट्रक्टर में थ्रो मॉडिफ़ायर भी हो सकता है, जो स्विफ्ट में सी-++ में इनिशियलाइज़ लिस्ट के सिंटैक्स के साथ दो-चरण के आरंभीकरण के शब्दार्थ के साथ बेहतर काम करता है।


स्विफ्ट उन कंस्ट्रक्टरों के सभी छेदों को बंद करने का प्रबंधन करता है जिनके बारे में मैंने शिकायत की थी। यह, हालांकि, एक मूल्य पर आता है: आरंभीकरण अध्याय स्विफ्ट पुस्तक में सबसे बड़ा है।


जब कंस्ट्रक्टरों की वास्तव में जरूरत होती है


सभी बाधाओं के खिलाफ, मैं कम से कम दो कारणों के साथ आ सकता हूं कि क्यों बिल्डरों को संरचना के शाब्दिक रूप से प्रतिस्थापित नहीं किया जा सकता है, जैसे कि जंग में।


पहले , विरासत, एक डिग्री या किसी अन्य के लिए, भाषा को निर्माणकर्ताओं के लिए मजबूर करता है। आप आधार वर्गों के समर्थन के साथ संरचनाओं के वाक्यविन्यास के विस्तार की कल्पना कर सकते हैं:


 struct Base { ... } struct Derived: Base { foo: i32 } impl Derived { fn new() -> Derived { Derived { Base::new().., foo: 92, } } } 

लेकिन यह साधारण विरासत के साथ एक OO भाषा के विशिष्ट ऑब्जेक्ट लेआउट में काम नहीं करेगा! आमतौर पर, एक ऑब्जेक्ट एक शीर्षक के साथ शुरू होता है जिसके बाद वर्ग फ़ील्ड होते हैं, आधार से सबसे व्युत्पन्न तक। इस प्रकार, व्युत्पन्न वर्ग के ऑब्जेक्ट का उपसर्ग बेस क्लास का सही ऑब्जेक्ट है। हालांकि, काम करने के लिए इस तरह के लेआउट के लिए, डिजाइनर को एक बार में पूरे ऑब्जेक्ट के लिए मेमोरी आवंटित करने की आवश्यकता होती है। यह केवल बेस क्लास के लिए मेमोरी आवंटित नहीं कर सकता है, और फिर व्युत्पन्न फ़ील्ड संलग्न कर सकता है। लेकिन टुकड़ों में स्मृति का ऐसा आवंटन आवश्यक है यदि हम संरचना बनाने के लिए वाक्यविन्यास का उपयोग करना चाहते हैं जहां हम आधार वर्ग के लिए एक मूल्य निर्दिष्ट कर सकते हैं।


दूसरे , संरचना शाब्दिक वाक्य रचना के विपरीत, डिजाइनरों में एक ABI होता है जो ऑब्जेक्ट सब -जेक्ट को मेमोरी (प्लेसमेंट-अनुकूल ABI) में रखने के साथ अच्छी तरह से काम करता है। निर्माता इसके लिए एक सूचक के साथ काम करता है, जो स्मृति के क्षेत्र को इंगित करता है कि नई वस्तु पर कब्जा करना चाहिए। सबसे महत्वपूर्ण बात, एक कंस्ट्रक्टर आसानी से सब -जेक्ट कंस्ट्रक्टर्स को पॉइंटर पास कर सकता है, जिससे जटिल वैल्यू ट्री बनाने की अनुमति मिलती है। इसके विपरीत, जंग में, संरचनाओं के निर्माण में शब्दशः काफी कुछ प्रतियां शामिल होती हैं, और यहां हम आशावादी की कृपा के लिए आशा करते हैं। यह कोई संयोग नहीं है कि रुस्त के पास अभी तक स्मृति में सबजेक्ट के प्लेसमेंट के संबंध में एक स्वीकृत कार्य प्रस्ताव नहीं है!


अपडेट 1: एक टाइपो तय किया। "संरचना शाब्दिक" के साथ "राइट लिटरल" को प्रतिस्थापित किया।

Source: https://habr.com/ru/post/hi460831/


All Articles