2012-08-03 12 views
24

.NET सी # कंपाइलर (.NET 4.0) fixed कथन को एक असाधारण तरीके से संकलित करता है।एमएसएफटी सी # एक फिक्स्ड "पॉइंटर क्षय के सरणी" और "पहले तत्व का पता" अलग-अलग क्यों संकलित करता है?

यहां एक छोटा लेकिन पूरा कार्यक्रम है जो आपको दिखाने के लिए है कि मैं किस बारे में बात कर रहा हूं।

using System; 

public static class FixedExample { 

    public static void Main() { 
     byte [] nonempty = new byte[1] {42}; 
     byte [] empty = new byte[0]; 

     Good(nonempty); 
     Bad(nonempty); 

     try { 
      Good(empty); 
     } catch (Exception e){ 
      Console.WriteLine(e.ToString()); 
      /* continue with next example */ 
     } 
     Console.WriteLine(); 
     try { 
      Bad(empty); 
     } catch (Exception e){ 
      Console.WriteLine(e.ToString()); 
      /* continue with next example */ 
     } 
    } 

    public static void Good(byte[] buffer) { 
     unsafe { 
      fixed (byte * p = &buffer[0]) { 
       Console.WriteLine(*p); 
      } 
     } 
    } 

    public static void Bad(byte[] buffer) { 
     unsafe { 
      fixed (byte * p = buffer) { 
       Console.WriteLine(*p); 
      } 
     } 
    } 
} 

के साथ संकलित "Csc.exe FixedExample.cs/असुरक्षित/ओ +" यदि आप के साथ पालन करना चाहते हैं।

अच्छा()

.maxstack 2 
    .locals init (uint8& pinned V_0) 
    IL_0000: ldarg.0 
    IL_0001: ldc.i4.0 
    IL_0002: ldelema [mscorlib]System.Byte 
    IL_0007: stloc.0 
    IL_0008: ldloc.0 
    IL_0009: conv.i 
    IL_000a: ldind.u1 
    IL_000b: call  void [mscorlib]System.Console::WriteLine(int32) 
    IL_0010: ldc.i4.0 
    IL_0011: conv.u 
    IL_0012: stloc.0 
    IL_0013: ret 

यहाँ विधि Bad के लिए उत्पन्न आईएल है::

बुरा()

यहाँ विधि Good के लिए उत्पन्न आईएल है

.locals init (uint8& pinned V_0, uint8[] V_1) 
    IL_0000: ldarg.0 
    IL_0001: dup 
    IL_0002: stloc.1 
    IL_0003: brfalse.s IL_000a 
    IL_0005: ldloc.1 
    IL_0006: ldlen 
    IL_0007: conv.i4 
    IL_0008: brtrue.s IL_000f 
    IL_000a: ldc.i4.0 
    IL_000b: conv.u 
    IL_000c: stloc.0 
    IL_000d: br.s  IL_0017 
    IL_000f: ldloc.1 
    IL_0010: ldc.i4.0 
    IL_0011: ldelema [mscorlib]System.Byte 
    IL_0016: stloc.0 
    IL_0017: ldloc.0 
    IL_0018: conv.i 
    IL_0019: ldind.u1 
    IL_001a: call  void [mscorlib]System.Console::WriteLine(int32) 
    IL_001f: ldc.i4.0 
    IL_0020: conv.u 
    IL_0021: stloc.0 
    IL_0022: ret 

यहाँ Good करता है:

  1. बफर के पते प्राप्त करें [0]।
  2. उस पते को अस्वीकार करें।
  3. उस संदर्भित मान के साथ लिखें WriteLine।

यहाँ 'Bad` करता है:

  1. तो बफर रिक्त है, गोटो 3.
  2. तो buffer.Length = 0, गोटो 5.
  3. स्टोर मान 0 स्थानीय में! स्लॉट 0,
  4. गोटो 6.
  5. बफर का पता प्राप्त करें [0]।
  6. उस पते को अस्वीकार करें (स्थानीय स्लॉट 0 में, जो अब 0 या बफर हो सकता है)।
  7. उस संदर्भित मान के साथ लिखें WriteLine।

जब buffer दोनों गैर-शून्य और खाली नहीं हैं, तो ये दो कार्य एक ही काम करते हैं। ध्यान दें कि BadWriteLine फ़ंक्शन कॉल पर जाने से पहले बस कुछ हुप्स के माध्यम से कूदता है।

जब buffer रिक्त है, Goodनिश्चित सूचक declarator (byte * p = &buffer[0]) में एक NullReferenceException फेंकता है। संभवतः यह एक प्रबंधित सरणी को ठीक करने के लिए वांछित व्यवहार है, क्योंकि सामान्य रूप से के अंदर कोई भी ऑपरेशन फिक्स्ड-स्टेटमेंट ऑब्जेक्ट की वैधता पर निर्भर करेगा। अन्यथा यह कोड fixed ब्लॉक के अंदर क्यों होगा? जब Good एक शून्य संदर्भ पारित किया गया है, तो यह fixed ब्लॉक की शुरुआत में तुरंत विफल रहता है, जो एक प्रासंगिक और सूचनात्मक स्टैक ट्रेस प्रदान करता है। डेवलपर इसे देखेगा और महसूस करेगा कि उसे इसका उपयोग करने से पहले buffer को सत्यापित करना चाहिए, या शायद उसके तर्क को null से buffer पर गलत तरीके से असाइन किया जाना चाहिए।किसी भी तरह से, fixed ब्लॉक को null प्रबंधित सरणी के साथ स्पष्ट रूप से दर्ज करना वांछनीय नहीं है।

Bad इस मामले को अलग-अलग, यहां तक ​​कि अवांछनीय रूप से संभालता है। आप देख सकते हैं कि Bad वास्तव में p को अस्वीकार नहीं किया गया है जब तक अपवाद फेंक नहीं है। यह के चौराहे के रास्ते में को उसी स्थानीय स्लॉट में p रखता है, फिर बाद में अपवाद फेंकता है जब fixed ब्लॉक स्टेटमेंट अव्यवस्था p

null हैंडलिंग इस तरह ऑब्जेक्ट मॉडल को सी # संगत में रखने का लाभ है। यही है, fixed ब्लॉक के अंदर, p अभी भी "एक प्रबंधित सरणी के सूचक" के रूप में अर्थात् इलाज किया जाता है, जो शून्य नहीं होने पर समस्याएं उत्पन्न करता है (या जब तक) इसे अस्वीकार नहीं किया जाता है। संगति सभी अच्छी और अच्छी है, लेकिन समस्या यह है कि पी एक प्रबंधित सरणी पर सूचक नहीं है। यह buffer के पहले तत्व के लिए एक सूचक है, और जिसने इस कोड को लिखा है (Bad) इस अर्थपूर्ण अर्थ की व्याख्या करेगा। आपको से buffer का आकार नहीं मिल सकता है, और आप p.ToString() पर कॉल नहीं कर सकते हैं, तो इसका इलाज क्यों करें क्योंकि यह एक वस्तु थी? ऐसे मामलों में जहां buffer शून्य है, स्पष्ट रूप से कोडिंग गलती है, और मेरा मानना ​​है कि Bad विधि के अंदर फिक्स्ड-पॉइंटर घोषणाकर्ता पर अपवाद फेंक देगा, तो यह बहुत उपयोगी होगा।

तो ऐसा लगता है कि Good से बेहतर null संभालता है। खाली बफर के बारे में क्या?

buffer लंबाई 0 होता है तो Goodनिश्चित सूचक declarator पर IndexOutOfRangeException फेंकता है। यह बाउंड सरणी पहुंच से निपटने के लिए एक पूरी तरह से उचित तरीका लगता है। आखिरकार, कोड &buffer[0] को &(buffer[0]) जैसा ही माना जाना चाहिए, जो स्पष्ट रूप से IndexOutOfRangeException फेंकना चाहिए।

Bad इस मामले को अलग-अलग संभालता है, और फिर अनचाहे रूप से। जैसे ही buffernull थे, buffer.Length == 0, Badp को अपवाद नहीं फेंक दिया गया है, और उस समय यह NullReferenceException फेंकता है, इंडेक्सऑटऑफेंजेंज अपवाद नहीं! यदि p कभी अस्वीकृत नहीं किया गया है, तो कोड एक अपवाद भी नहीं फेंकता है। दोबारा, ऐसा लगता है कि यहां विचार p "एक प्रबंधित सरणी के सूचक" का अर्थपूर्ण अर्थ देना है। फिर भी, मुझे नहीं लगता कि इस कोड को लिखने वाला कोई भी व्यक्ति p इस तरह से सोचता है। फिक्स्ड-पॉइंटर घोषणाकर्ता में IndexOutOfRangeException फेंकने पर कोड अधिक उपयोगी होगा, इस प्रकार डेवलपर को सूचित किया गया है कि सरणी पास की गई सरणी खाली थी, और null नहीं।

ऐसा लगता है कि fixed(byte * p = buffer) को उसी कोड पर संकलित किया जाना चाहिए जैसा fixed (byte * p = &buffer[0]) था। यह भी ध्यान दें कि buffer किसी भी मनमाने ढंग से अभिव्यक्ति हो सकता था, यह प्रकार (byte[]) संकलन समय पर जाना जाता है और इसलिए Good में कोड किसी भी मनमाने ढंग से अभिव्यक्ति के लिए काम करेगा।

संपादित

वास्तव में, देखा कि Bad के कार्यान्वयन वास्तव में त्रुटि buffer[0]दो बार पर जाँच करता है। यह स्पष्ट रूप से विधि की शुरुआत में करता है, और फिर यह ldelema निर्देश पर फिर से निहित करता है।


तो हम कि Good और Bad शब्दार्थ अलग हैं देखते हैं। Bad लंबा, शायद धीमा है, और निश्चित रूप से हमें वांछनीय अपवाद नहीं देता है जब हमारे कोड में बग होती है, और कुछ मामलों में इसके बाद भी बहुत कुछ विफल हो जाती है।

उत्सुक लोगों के लिए, कल्पना (सी # 4.0) की धारा 18.6 का कहना है कि व्यवहार "कार्यान्वयन से परिभाषित" इन विफलता दोनों मामलों में है:

एक निश्चित सूचक-प्रारंभकर्ता से एक हो सकता निम्नलिखित:

• टोकन "&" एक परिवर्तनीय-प्रकार (§5.3.3) के बाद एक अप्रबंधित प्रकार टी के एक परिवर्तनीय चर (§18.3) के बाद, टी प्रकार * सूचक रूप से सूचक रूप से परिवर्तनीय है निश्चित कथन में दिया गया प्रकार। इस मामले में, प्रारंभकर्ता निर्दिष्ट चर के पते की गणना करता है, और चर को निश्चित कथन की अवधि के लिए एक निश्चित पते पर रहने की गारंटी दी जाती है।

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

... अन्य मामलों ...

अंतिम बिंदु, MSDN documentation पता चलता है कि दो "बराबर" कर रहे हैं:

// निम्न दो कार्य बराबर हैं ...

तय (डबल * पी = आगमन) {/ ... /}

तय (डबल * पी = & आगमन [0]) {/ ... /}

दो "बराबर" होना चाहिए रहे हैं, तो क्यों पूर्व बयान के लिए अलग त्रुटि से निपटने के अर्थ विज्ञान का उपयोग ?

यह भी प्रतीत होता है कि अतिरिक्त प्रयास को Bad में उत्पन्न कोड पथ लिखने के लिए रखा गया था। Good में संकलित कोड सभी विफलता मामलों के लिए ठीक काम करता है, और गैर-विफलता मामलों में Bad में कोड के समान है। Good के लिए जेनरेट किए गए सरल कोड का उपयोग करने के बजाय नए कोड पथ क्यों लागू करें?

इस तरह इसे क्यों लागू किया गया है?

+0

@pst मैंने 'SoWhat' को हटा दिया और इसके स्पष्टीकरण के लिए संक्षिप्तता। फिक्स्ड। –

+1

क्या आपने जांच की है कि सी # spec 'arr [0] 'के बारे में क्या कहता है? मुझे पूरा यकीन है कि 'arr [0]' अपवाद फेंकता है जब किसी भी संदर्भ में 'arr' शून्य या खाली होता है, चाहे उसके आस-पास के किसी भी 'निश्चित' कथन के बावजूद। – hvd

+0

@ एचवीडी हां यही कारण है कि 'निश्चित (बाइट * पी = बफर)' को केवल 'निश्चित (बाइट * पी = और बफर [0]) के रूप में माना जाना चाहिए। बाद में अधिक स्पष्ट रूप से जांचने में त्रुटि का ख्याल रखता है। मैं इसे प्रश्न में जोड़ दूंगा। –

उत्तर

9

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

बेशक

, कि सिर्फ दो आगे के प्रश्न है कि हम पूछ सकते हैं की ओर जाता है:

  • सी # भाषा समूह क्यों चुना कल्पना इस तरह से लिखने के लिए?
  • क्यों कंपेलर टीम ने विशिष्ट कार्यान्वयन-परिभाषित व्यवहार का चयन क्यों किया?

उपयुक्त टीमों में से किसी एक को दिखाते हुए, हम वास्तव में उन प्रश्नों में से किसी एक का उत्तर देने की उम्मीद नहीं कर सकते हैं। हालांकि, हम अपने तर्क का पालन करने की कोशिश करके दूसरे को जवाब देने पर एक स्टैब ले सकते हैं।

याद कल्पना का कहना है कि, एक निश्चित सूचक-प्रारंभकर्ता को एक सरणी की आपूर्ति के मामले में, कि

कार्यान्वयन से परिभाषित करता है, तो सरणी अभिव्यक्ति है तय बयान के व्यवहार शून्य या अगर सरणी में शून्य तत्व हैं।

के बाद से कार्यान्वयन जो भी हो इस मामले में चाहता है ऐसा करने के लिए चुनने के लिए स्वतंत्र है, तो हम मान सकते हैं कि हो सकता है जो कुछ भी उचित व्यवहार का सबसे आसान और ऐसा करने के लिए संकलक टीम के लिए सबसे सस्ता था।

इस मामले में, कंपाइलर टीम ने क्या करना चुना था "उस बिंदु पर अपवाद फेंक दें जहां आपका कोड कुछ गलत करता है"। गौर करें कि कोड क्या करेगा यदि यह फिक्स्ड-पॉइंटर-प्रारंभकर्ता के अंदर नहीं था और इसके बारे में सोचें कि और क्या हो रहा है। आपके "अच्छे" उदाहरण में, आप उस ऑब्जेक्ट का पता लेने की कोशिश कर रहे हैं जो मौजूद नहीं है: शून्य/खाली सरणी में पहला तत्व। यह ऐसा कुछ नहीं है जिसे आप वास्तव में कर सकते हैं, इसलिए यह अपवाद उत्पन्न करेगा। आपके "खराब" उदाहरण में, आप केवल एक पॉइंटर चर के लिए पैरामीटर का पता निर्दिष्ट कर रहे हैं; byte * p = null एक पूरी तरह से वैध बयान है। यह तब होता है जब आप WriteLine(*p) को आज़माते हैं कि कोई त्रुटि होती है। चूंकि फिक्स्ड-पॉइंटर-प्रारंभकर्ता को इस अपवाद मामले में जो कुछ भी करना है, उसे करने की अनुमति है, इसलिए सबसे आसान काम केवल असाइनमेंट होने की अनुमति है, जैसा कि अर्थहीन है।

स्पष्ट रूप से, दो कथन ठीक समतुल्य हैं।

  • &arr[0] है:: हम तथ्य यह है कि मानक इनकी अलग व्यवहार करता है के द्वारा इस बता सकते हैं "टोकन" & "एक चर-संदर्भ के बाद", और इसलिए संकलक आगमन [0]
  • का पता गणना करता है
  • arr है: "एक सरणी प्रकार की अभिव्यक्ति", और इसलिए संकलक सरणी के पहले तत्व के पते की गणना करता है, जिसमें एक नल या 0-लंबाई सरणी आपके द्वारा देखे जा रहे कार्यान्वयन-परिभाषित व्यवहार का उत्पादन करती है।

दो बराबर परिणाम उत्पादन, इतने लंबे समय सरणी, जो मुद्दा यह है कि MSDN प्रलेखीकरण भर में प्राप्त करने की कोशिश कर रहा है में एक तत्व मौजूद है। प्रश्नों के बारे में पूछते हुए कि स्पष्ट रूप से अपरिभाषित या कार्यान्वयन-परिभाषित व्यवहार इस तरह से कार्य करता है कि वास्तव में किसी विशेष समस्या को हल करने में आपकी मदद करने के लिए वास्तव में नहीं जा रहा है, क्योंकि आप भविष्य में सत्य होने पर भरोसा नहीं कर सकते हैं। (ऐसा कहकर, मैं निश्चित रूप से जानना चाहता हूं कि विचार प्रक्रिया क्या थी, क्योंकि आप स्पष्ट रूप से स्मृति में एक शून्य मान को "ठीक" नहीं कर सकते हैं ...)

+0

spec कार्यान्वयन परिभाषित व्यवहार के अनुसार केवल दूसरे मामले में अनुमति है, लेकिन अच्छा पहला मामला लागू करता है। 'और सरणी [0]' एक * सरणी अभिव्यक्ति नहीं है। – usr

+1

"हमें उम्मीद करनी होगी कि सी # कंपाइलर टीम पर कोई व्यक्ति" यह मेरी आशा है :) –

+0

@usr सच है, मैं पोस्ट में उस बिंदु को पाने की कोशिश कर रहा था लेकिन मैं इसके बारे में और अधिक स्पष्ट हूं :) –

1

तो हम देखते हैं कि अच्छा और बुरा अर्थात् अलग है। क्यूं कर?

क्योंकि अच्छा मामले 1 है और बुरा मामले 2.

अच्छा एक "एक सरणी-प्रकार का एक शब्द" आवंटित नहीं करता है। यह "टोकन" & "एक चर-संदर्भ के बाद" असाइन करता है, इसलिए यह मामला है 1. खराब "केस को बनाने के लिए" एक सरणी प्रकार की अभिव्यक्ति "असाइन करता है। यदि यह सच है तो एमएसडीएन दस्तावेज गलत है।

किसी भी मामले में यह बताता है कि सी # कंपाइलर दो अलग-अलग (और दूसरे मामले में विशिष्ट) कोड पैटर्न क्यों बनाता है।

केस 1 ऐसा सरल कोड क्यों उत्पन्न करता है? मैं यहां अनुमान लगा रहा हूं: एक सरणी तत्व का पता लेना शायद array[index] का उपयोग ref -expression में किया गया है। सीएलआर स्तर पर, ref पैरामीटर और अभिव्यक्ति केवल प्रबंधित पॉइंटर्स हैं। तो अभिव्यक्ति &array[index] है: यह एक प्रबंधित पॉइंटर से संकलित है जिसे पिन नहीं किया गया है लेकिन "इंटीरियर" (यह शब्द प्रबंधित सी ++ से आता है)। जीसी स्वचालित रूप से इसे ठीक करता है। यह एक सामान्य वस्तु संदर्भ की तरह व्यवहार करता है।

तो मामला 1 सामान्य प्रबंधित सूचक उपचार प्राप्त करता है जबकि केस 2 को विशेष, कार्यान्वयन परिभाषित (अनिर्धारित नहीं) व्यवहार मिलता है।

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

+2

"यदि यह सच है तो एमएसडीएन दस्तावेज गलत है।" वास्तव में, बस थोड़ा भ्रामक नहीं, जैसा कि मैं इसे समझता हूं। दो रूप उन बफर के बराबर हैं जिनके लिए दोनों रूपों ने व्यवहार को परिभाषित किया है। – hvd

+0

@ एचवीडी, हाँ उन मामलों के लिए। लेकिन सभी के लिए नहीं। प्रलेखन आपके कथन को योग्यता से योग्य नहीं करता है जैसा आपने अभी किया था। – usr

+0

@ एचवीडी मुझे उम्मीद करनी चाहिए कि "कार्यान्वयन परिभाषित व्यवहार" एमएसडीएन दस्तावेज़ों में परिभाषित किया गया है। स्पष्ट रूप से यह इस मामले में नहीं है। –

संबंधित मुद्दे