एक पुनरावर्ती निर्माता कॉल अमान्य C # कोड संकलन क्यों करता है?


82

वेबिनार जॉन स्कीट इंस्पेक्ट्स रेस्परर को देखने के बाद , मैंने पुनरावर्ती रचनाकार कॉल के साथ थोड़ा खेलना शुरू कर दिया है और पाया है, कि निम्न कोड मान्य सी # कोड है (मान्य से मेरा मतलब है कि यह संकलित है)।

class Foo
{
    int a = null;
    int b = AppDomain.CurrentDomain;
    int c = "string to int";
    int d = NonExistingMethod();
    int e = Invalid<Method>Name<<Indeeed();

    Foo()       :this(0)  { }
    Foo(int v)  :this()   { }
}

जैसा कि हम सभी जानते हैं, कंपाइलर द्वारा फील्ड इनिशियलाइज़ेशन को कंस्ट्रक्टर में ले जाया जाता है। इसलिए यदि आपके पास एक क्षेत्र जैसा है int a = 42;, तो आपके पास सभी निर्माणकर्ताओं a = 42में होगा । लेकिन अगर आपके पास कंस्ट्रक्टर एक और कंस्ट्रक्टर को बुला रहा है, तो आपके पास केवल एक में इनिशियलाइज़ेशन कोड होगा।

उदाहरण के लिए यदि आपके पास डिफॉल्ट कंस्ट्रक्टर को कॉल करने वाले मापदंडों के साथ कंस्ट्रक्टर है, तो आपके पास a = 42केवल डिफॉल्ट कंस्ट्रक्टर में असाइनमेंट होगा ।

दूसरे मामले का वर्णन करने के लिए, अगला कोड:

class Foo
{
    int a = 42;

    Foo() :this(60)  { }
    Foo(int v)       { }
}

में संकलित:

internal class Foo
{
    private int a;

    private Foo()
    {
        this.ctor(60);
    }

    private Foo(int v)
    {
        this.a = 42;
        base.ctor();
    }
}

इसलिए मुख्य मुद्दा यह है कि इस प्रश्न के प्रारंभ में दिया गया मेरा कोड निम्नलिखित में संकलित है:

internal class Foo
{
    private int a;
    private int b;
    private int c;
    private int d;
    private int e;

    private Foo()
    {
        this.ctor(0);
    }

    private Foo(int v)
    {
        this.ctor();
    }
}

जैसा कि आप देख सकते हैं, संकलक तय नहीं कर सकता है कि फ़ील्ड इनिशियलाइज़ेशन कहाँ रखा जाए और, परिणामस्वरूप, इसे कहीं भी नहीं रखा जाए। यह भी ध्यान दें, कोई baseरचनाकार कॉल नहीं है। बेशक, कोई भी वस्तु नहीं बनाई जा सकती है, और आप हमेशा के लिए समाप्त StackOverflowExceptionहो जाएंगे यदि आप एक उदाहरण बनाने की कोशिश करेंगे Foo।

मेरे दो सवाल हैं:

कंपाइलर पुनरावर्ती निर्माता को कॉल की अनुमति क्यों देता है?

हम खेतों के लिए संकलक के ऐसे व्यवहार का निरीक्षण क्यों करते हैं, इस तरह के वर्ग के भीतर आरम्भ में?


कुछ नोट: ReSharper आपको चेतावनी देता है Possible cyclic constructor calls। इसके अलावा, जावा में इस तरह के कंस्ट्रक्टर कॉल का संकलन नहीं होगा, इसलिए जावा कंपाइलर इस परिदृश्य में अधिक प्रतिबंधक है (जॉन ने वेबिनार में इस जानकारी का उल्लेख किया है)।

यह इन सवालों को अधिक दिलचस्प बनाता है, क्योंकि जावा समुदाय के सभी सम्मान के साथ, सी # संकलक कम से कम अधिक आधुनिक है।

यह C # 4.0 और C # 5.0 संकलक का उपयोग करके संकलित किया गया था और dotPeek का उपयोग करके विघटित किया गया था ।


3
वह कैसे ** मैं इस वीडियो को याद किया ???
— रॉय नमिर

7
बहुत बढ़िया सवाल।
— डेनिस

2
वहां अच्छा फील्ड इनिशियलाइज़र: int a = null; int b = AppDomain.CurrentDomain; int c = "string to int"; int d = NonExistingMethod(); int e = Invalid<Method>Name<<Indeeed();किसी को एक क्विज़ बनाना चाहिए: "किस क्षेत्र में ये क्षेत्र घोषणाएँ ठीक हैं?" (खेतों में इस्तेमाल नहीं होने के बारे में एक चेतावनी है, लेकिन आप इंटेंस कंस्ट्रक्टर्स (या कहीं और) के शरीर के अंदर प्रत्येक फ़ील्ड को पढ़कर उस चेतावनी से छुटकारा पा सकते हैं।
— जेपी स्टिग नील्सन

4
मेरा मानना ​​है कि इसी कारण से अनुमति दी गई है ।
— GSerg

4
फ़ील्ड आरंभीकरण उन सभी कंस्ट्रक्टरों में डाल दिया जाता है जो बेस कंस्ट्रक्टर कहते हैं। परिणामस्वरूप, अगर कोई कंस्ट्रक्टर नहीं है जो बेस कंस्ट्रक्टर को कॉल करता है, तो फ़ील्ड इनिशियलाइज़ेशन कहीं भी नहीं मिलता है। कम से कम वह हिस्सा मेरे लिए एकदम सही है। ऐसा नहीं है कि कंपाइलर यह पता नहीं लगा सकता है कि इसे कहां रखा जाए, यह इसलिए है क्योंकि कंपाइलर ने इसे कहीं भी नहीं रखा है।

जवाबों:


11

दिलचस्प खोज।

ऐसा प्रतीत होता है कि वास्तव में केवल दो प्रकार के उदाहरण निर्माता हैं:

  1. एक इंस्टेंट कंस्ट्रक्टर जो सिंटेक्स के साथ एक ही प्रकार के कंस्ट्रक्टर के एक और इंस्टेंस को चेन : this( ...)करता है।
  2. एक इंस्टेंस कंस्ट्रक्टर जो बेस क्लास के एक इंस्ट्रक्टर को चेन करता है । इसमें ऐसे उदाहरण निर्माता शामिल हैं जहाँ कोई भी चेनग निर्दिष्ट नहीं है, चूंकि : base()डिफ़ॉल्ट है।

(मैंने उदाहरण के निर्माता की अवहेलना की, System.Objectजो एक विशेष मामला है। System.Objectजिसका कोई आधार वर्ग नहीं है! लेकिन System.Objectइसका कोई क्षेत्र भी नहीं है)।

उदाहरण फ़ील्ड प्रारंभकर्ता जो कक्षा में मौजूद हो सकता है, को टाइप 2 के सभी इंस्टेंस कंस्ट्रक्टर्स के शरीर की शुरुआत में कॉपी करने की आवश्यकता है । ऊपर, जबकि टाइप 1 के किसी भी प्रकार के कंस्ट्रक्टर्स को फील्ड असाइनमेंट कोड की आवश्यकता नहीं है।

तो जाहिर तौर पर C # कंपाइलर के लिए टाइप 1 के कंस्ट्रक्टर्स का एनालिसिस करने की कोई जरूरत नहीं है। यह देखने के लिए कि साइकल हैं या नहीं।

अब आपका उदाहरण एक स्थिति देता है जहां सभी उदाहरण निर्माता टाइप 1 के हैं । उस स्थिति में फील्ड इनइयेटाइज़र कोड को कहीं भी डालने की आवश्यकता नहीं है। इसलिए इसका बहुत गहराई से विश्लेषण नहीं किया गया है, ऐसा लगता है।

यह पता चला है कि जब सभी उदाहरण निर्माता टाइप 1 के होते हैं , तो आप बेस क्लास से भी प्राप्त कर सकते हैं, जिसमें कोई सुलभ कंस्ट्रक्टर नहीं है। आधार वर्ग को गैर-सील होना चाहिए, हालांकि। उदाहरण के लिए यदि आप केवल privateउदाहरण के निर्माणकर्ताओं के साथ एक वर्ग लिखते हैं , तो लोग तब भी आपकी कक्षा से प्राप्त कर सकते हैं यदि वे व्युत्पन्न वर्ग में सभी उदाहरणों को टाइप 1 से ऊपर का बनाते हैं । हालांकि, एक नई वस्तु निर्माण अभिव्यक्ति निश्चित रूप से खत्म नहीं होगी। व्युत्पन्न वर्ग के उदाहरण बनाने के लिए, किसी को "धोखा" देना होगा और System.Runtime.Serialization.FormatterServices.GetUninitializedObjectविधि की तरह सामान का उपयोग करना होगा ।

एक अन्य उदाहरण: System.Globalization.TextInfoकक्षा में केवल एक internalउदाहरण निर्माता है। लेकिन आप अभी भी mscorlib.dllइस तकनीक के अलावा एक विधानसभा में इस वर्ग से प्राप्त कर सकते हैं ।

अंत में, के बारे में

Invalid<Method>Name<<Indeeed()

वाक्य - विन्यास। C # नियमों के अनुसार, इसे पढ़ा जाना चाहिए

(Invalid < Method) > (Name << Indeeed())

क्योंकि बाएं-शिफ्ट ऑपरेटर के <<पास ऑपरेटर की तुलना में अधिक पूर्वता है <और ऑपरेटर की तुलना में अधिक से अधिक >। बाद के दो ऑपरेटर्स की एक ही मिसाल है, और इसलिए उनका मूल्यांकन वाम-सहयोगी शासन द्वारा किया जाता है। यदि प्रकार थे

MySpecialType Invalid;
int Method;
int Name;
int Indeed() { ... }

और अगर MySpecialTypeएक (MySpecialType, int)अधिभार का परिचय दिया operator <, तो अभिव्यक्ति

Invalid < Method > Name << Indeeed()

कानूनी और सार्थक होगा।


मेरी राय में, यह बेहतर होगा कि कंपाइलर इस परिदृश्य में चेतावनी जारी करे। उदाहरण के लिए, यह कह सकता है unreachable code detectedऔर फ़ील्ड इनिशियलाइज़र की पंक्ति और स्तंभ संख्या को इंगित कर सकता है जिसे कभी भी IL में अनुवादित नहीं किया जाता है।


1
मुझे समझ में नहीं आता ... ctor से पहले doesnt क्षेत्र का तात्पर्य है?
— रॉय नमिर

2
@RoyiNamir हां। लेकिन यदि आप आईएल को देखते हैं, तो यह काम करता है जैसे पूछने वाला लिखते हैं: "जैसा कि हम सभी शायद जानते हैं, कंपाइलर द्वारा फील्ड इनिशियलाइज़ेशन को कंस्ट्रक्टर में स्थानांतरित कर दिया जाता है।" इसका क्या मतलब है, मान लीजिए कि आप इस वर्ग को C # में लिखते हैं: class Example { int field = 42; internal Example() { /* some code here */ field = 100; } }तो उस द्वारा उत्पादित IL 42, उदाहरण के निर्माण में असाइनमेंट को सब कुछ से पहले रखता है, ठीक वैसे ही जैसे कि आपने लिखा था:class Example { int field; internal Example() { field = 42; /* some code here */ field = 100; } }
— Jeppe Stig Nielsen

5

मुझे लगता है कि क्योंकि भाषा विनिर्देश केवल उसी निर्माणकर्ता को सीधे लागू करता है जो परिभाषित किया जा रहा है।

10.11.1 से:

सभी उदाहरण कंस्ट्रक्टर (वर्ग को छोड़कर object) को स्पष्ट रूप से कंस्ट्रक्टर-निकाय से पहले किसी अन्य इंस्टेंस कंस्ट्रक्टर का एक आह्वान शामिल है। स्पष्ट रूप से आह्वान करने वाले का निर्माण कंस्ट्रक्टर-इनिशियलाइज़र द्वारा निर्धारित किया जाता है

...

  • फॉर्म का एक इंस्ट्रक्टर कंस्ट्रक्टर इनिशियलाइज़र , क्लास से ही एक इंस्ट्रक्टर कंस्ट्रक्टर का कारण बनता है ... यदि इंस्टेंस कंस्ट्रक्टर डिक्लेरेशन में कंस्ट्रक्टर इनिशियलाइज़र शामिल होता है, जो कंस्ट्रक्टर को ही इनवाइट करता है, तो कंपाइल-टाइम एरर होता हैthis(argument-listopt)

यह अंतिम वाक्य केवल एक संकलन समय त्रुटि, उदा

Foo() : this() {}

गैरकानूनी है।


हालांकि मैं मानता हूं - मैं इसे अनुमति देने के लिए कोई विशेष कारण नहीं देख सकता। बेशक, IL स्तर पर ऐसे निर्माणों की अनुमति है, क्योंकि विभिन्न प्रकार के निर्माणकर्ताओं को रनटाइम पर चुना जा सकता है, मेरा मानना ​​है - इसलिए आप पुनरावृत्ति कर सकते थे बशर्ते कि यह समाप्त हो जाए।


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

जब यह प्रत्येक निर्माता के लिए कोड जनरेशन कर रहा होता है, तो सभी इसे मानते हैं constructor-initializer, फ़ील्ड इनिशियलाइज़र, और कंस्ट्रक्टर का शरीर - यह किसी अन्य कोड पर विचार नहीं करता है:

  • यदि constructor-initializerवर्ग के लिए एक उदाहरण निर्माता है, तो यह क्षेत्र इनिशियलाइज़र का उत्सर्जन नहीं करता है - यह constructor-initializerकॉल और फिर निकाय का उत्सर्जन करता है।

  • यदि constructor-initializerप्रत्यक्ष बेस क्लास के लिए एक इंस्टा कंस्ट्रक्टर है, तो यह फील्ड इनिशियलाइज़र, फिर constructor-initializerकॉल, और फिर बॉडी का उत्सर्जन करता है।

न तो मामले में इसे कहीं और देखने की आवश्यकता है - इसलिए यह यह तय करने के लिए "असमर्थ" होने का मामला नहीं है कि फ़ील्ड इनिशियलाइज़र को कहाँ रखा जाए - यह केवल कुछ सरल नियमों का पालन कर रहा है जो केवल वर्तमान कंस्ट्रक्टर पर विचार करते हैं।


2
लेकिन इस तथ्य के बारे में क्या है कि यह इस संकलन की तरह लाइनें देता है int e = Invalid<Method>Name<<Indeeed();:। मैं कहता हूं कि कंपाइलर बग है।
— मैथ्यू वाटसन

@MatthewWatson की व्याख्या int e = Invalid < Method > Name << Indeed();द्विआधारी ऑपरेटरों के साथ "कम से कम", "अधिक से अधिक", और "बाएं पारी" से की जा सकती है। यह वाक्यात्मक रूप से ठीक है, लेकिन मजबूत टाइपिंग के साथ इसे ठीक बनाने के लिए ऑपरेटरों के कुछ वास्तव में पागल अधिभार होंगे।
— जेपी स्टिग नीलसन

1
@JeppeStigNielsen Aye, लेकिन यह संकलित नहीं करेगा यदि आप पुनरावर्ती निर्माता कोड को हटाने के अलावा कोड को एक ही छोड़ देते हैं। इसलिए मुझे लगता है कि यह एक बग है।
— मैथ्यू वाटसन

4
@MatthewWatson त्रुटि का पता नहीं लगाया जा सकता क्योंकि वर्ग अपूर्ण है। (हो सकता है कि आपकी कक्षा Invalidआदि कहे जाने वाले सदस्यों को परिभाषित कर दे जो इसे वैध बना देगा।) आम तौर पर कोड पीढ़ी में त्रुटि का पता लगाया जाता है, लेकिन आपको ऐसा कोड लिखने का एक तरीका मिला जो कभी उत्पन्न नहीं होता है। आपको कंपाइलर में एक डरपोक छेद मिला (कोड लिखने का एक तरीका जो कभी भी संकलित नहीं किया जाएगा), लेकिन एक गंभीर एक नहीं है क्योंकि आक्रामक कोड वैसे भी अगम्य है।
— रेमंड चेन

2

आपका उदाहरण

class Foo
{
    int a = 42;

    Foo() :this(60)  { }
    Foo(int v)       { }
}

ठीक काम करेगा, इस अर्थ में कि आप उस Foo ऑब्जेक्ट को बिना किसी समस्या के तुरंत कर सकते हैं। हालाँकि, निम्नलिखित उस कोड की तरह अधिक होगा, जिसके बारे में आप पूछ रहे हैं

class Foo
{
    int a = 42;

    Foo() :this(60)     { }
    Foo(int v) : this() { }
}

वह और आपका कोड दोनों एक स्टैकओवरफ़्लो (!) बनाएंगे, क्योंकि रिकर्सन कभी भी बाहर नहीं निकलता है। इसलिए आपके कोड को अनदेखा कर दिया जाता है क्योंकि यह कभी भी निष्पादित नहीं होता है।

दूसरे शब्दों में, संकलक यह तय नहीं कर सकता है कि दोषपूर्ण कोड कहां रखा जाए क्योंकि यह बता सकता है कि पुनरावृत्ति कभी भी समाप्त नहीं होती है। मुझे लगता है कि यह इसलिए है क्योंकि इसे लगाना होगा जहां इसे केवल एक बार बुलाया जाएगा, लेकिन निर्माणकर्ताओं की पुनरावृत्ति प्रकृति को असंभव बनाती है।

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


1
हां, मैं सहमत हूं, इसीलिए मैंने यह प्रश्न बनाया है। कंपाइलर यह क्यों तय नहीं कर सकता है कि आरंभीकरण तर्क कहां रखा जाए और इसलिए, यह पुनरावर्ती कॉल की अनुमति क्यों देता है? क्या इसका कोई कारण है?
— इल्या इवानोव

यह मुझे स्पष्ट लगता है कि संकलक यह तय नहीं कर सकता है कि दोषपूर्ण कोड कहां रखा जाए क्योंकि यह बता सकता है कि पुनरावृत्ति कभी भी समाप्त नहीं होती है। वह एक रहस्य क्यों है?
— स्टोकेस्टिक रूप से

यदि C # कॉल करने के लिए कौन सी विधि तय नहीं कर सकता है, तो यह एक त्रुटि फेंकता है ambiguous method call, यह इस तरह के कॉल को नहीं छोड़ता है। यदि मैं एक संकलक होता, तो मैं इस परिदृश्य में एक त्रुटि भी फेंक देता।
— इल्या इवानोव

1
यह बुरा है, कि जवाब इतने सारे डाउनवोट प्राप्त करते हैं, मैं उनमें से किसी को भी डाउनवोट नहीं कर रहा हूं (बस मामला है)। इस परिदृश्य में यह भी तय नहीं किया जा सकता है कि आरंभीकरण तर्क कहाँ रखा जाए। तो मेरा मुख्य सवाल यह है कि पुनरावर्ती कॉल की अनुमति क्यों है? क्या इसके पीछे कोई कारण है? शायद मुझे कुछ याद आ रहा है
— इल्या इवानोव

3
@ इलियोनोव - मुझे लगता है कि अधिक प्रासंगिक सवाल है - संकलक में पुनरावर्ती निर्माता कॉल का पता लगाने के लिए एक चक्र डिटेक्टर क्यों लिखें ?
— डेमियन_इन_अनबेलियर २०'१३ को

0

मुझे लगता है कि यह अनुमति है क्योंकि आप अभी भी अपवाद को पकड़ सकते हैं और इसके साथ कुछ सार्थक कर सकते हैं।

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

जैसा कि यहाँ बताया गया है https://stackoverflow.com/a/1599236/869482

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.