जिज्ञासु अशक्त-सहवर्ती संचालक कस्टम अंतर्निहित रूपांतरण व्यवहार


542

नोट: ऐसा प्रतीत होता है कि रोज़लिन में तय किया गया था

यह सवाल तब पैदा हुआ जब करने के लिए अपने जवाब लिख यह एक है, जिनमें से संबद्धता के बारे में बात अशक्त-वालों ऑपरेटर

एक अनुस्मारक के रूप में, अशक्त-सहवर्ती ऑपरेटर का विचार यह है कि फॉर्म की एक अभिव्यक्ति

x ?? y

पहले मूल्यांकन करता है x, फिर:

  • यदि का मान xशून्य है, yमूल्यांकन किया जाता है और यह अभिव्यक्ति का अंतिम परिणाम है
  • यदि का मान xगैर-अशक्त है, yतो मूल्यांकन नहीं किया जाता है, और यदि आवश्यक हो तो xसंकलन-समय के प्रकार में रूपांतरण के बाद, अभिव्यक्ति का अंतिम परिणाम है।y

अब आमतौर पर रूपांतरण की कोई आवश्यकता नहीं है, या यह एक अशक्त प्रकार से एक गैर-अशक्त तक है - आमतौर पर प्रकार समान हैं, या बस (कहो) int?सेint । हालाँकि, आप अपने स्वयं के अंतर्निहित रूपांतरण ऑपरेटर बना सकते हैं, और जहाँ आवश्यक हो उनका उपयोग किया जाता है।

के साधारण मामले के लिए x ?? y , मैंने कोई विषम व्यवहार नहीं देखा है। हालाँकि, (x ?? y) ?? zमैं कुछ भ्रामक व्यवहार देखता हूँ।

यहाँ एक छोटा लेकिन पूर्ण परीक्षण कार्यक्रम है - परिणाम टिप्पणियों में हैं:

using System;

public struct A
{
    public static implicit operator B(A input)
    {
        Console.WriteLine("A to B");
        return new B();
    }

    public static implicit operator C(A input)
    {
        Console.WriteLine("A to C");
        return new C();
    }
}

public struct B
{
    public static implicit operator C(B input)
    {
        Console.WriteLine("B to C");
        return new C();
    }
}

public struct C {}

class Test
{
    static void Main()
    {
        A? x = new A();
        B? y = new B();
        C? z = new C();
        C zNotNull = new C();

        Console.WriteLine("First case");
        // This prints
        // A to B
        // A to B
        // B to C
        C? first = (x ?? y) ?? z;

        Console.WriteLine("Second case");
        // This prints
        // A to B
        // B to C
        var tmp = x ?? y;
        C? second = tmp ?? z;

        Console.WriteLine("Third case");
        // This prints
        // A to B
        // B to C
        C? third = (x ?? y) ?? zNotNull;
    }
}

तो हमारे पास तीन कस्टम मूल्य प्रकार हैं A,B और C, सी बी, ए सी के लिए, और बी के लिए एक से रूपांतरण के साथ

मैं दूसरे मामले और तीसरे मामले को समझ सकता हूं ... लेकिन पहले मामले में अतिरिक्त ए से बी रूपांतरण क्यों है? विशेष रूप से, मैं वास्तव में पहले मामले और दूसरे मामले में एक ही बात होने की उम्मीद कर - यह सिर्फ एक स्थानीय चर में अभिव्यक्ति निकाल रहा है, आखिरकार।

क्या हो रहा है पर कोई लेने वाला? जब सी # कंपाइलर की बात आती है तो मैं "बग" रोने से बहुत हिचकिचाता हूं, लेकिन मैं इस बात पर अडिग हूं कि क्या हो रहा है ...

संपादित करें: ठीक है, यहाँ क्या हो रहा है इसका एक नास्टियर उदाहरण है, विन्यासकर्ता के उत्तर के लिए धन्यवाद, जो मुझे यह सोचने के लिए और कारण देता है कि यह एक बग है। संपादित करें: नमूना को अब दो अशक्त-संचालक ऑपरेटरों की भी आवश्यकता नहीं है ...

using System;

public struct A
{
    public static implicit operator int(A input)
    {
        Console.WriteLine("A to int");
        return 10;
    }
}

class Test
{
    static A? Foo()
    {
        Console.WriteLine("Foo() called");
        return new A();
    }

    static void Main()
    {
        int? y = 10;

        int? result = Foo() ?? y;
    }
}

इस का उत्पादन है:

Foo() called
Foo() called
A to int

तथ्य यह है कि Foo()दो बार यहाँ कहा जाता है मेरे लिए बेहद आश्चर्य की बात है - मैं अभिव्यक्ति का दो बार मूल्यांकन करने का कोई कारण नहीं देख सकता ।


32
मुझे यकीन है कि उन्होंने सोचा "कोई भी उस तरह से इसका इस्तेमाल नहीं करेगा" :)
साइबर

57
कुछ बुरा देखना चाहते हैं? सभी अंतर्निहित रूपांतरणों के साथ इस पंक्ति का उपयोग करने का प्रयास करें C? first = ((B?)(((B?)x) ?? ((B?)y))) ?? ((C?)z);:। आपको मिलेगा:Internal Compiler Error: likely culprit is 'CODEGEN'
विन्यासकर्ता

5
यह भी ध्यान रखें कि एक ही कोड को संकलित करने के लिए Linq अभिव्यक्तियों का उपयोग करते समय ऐसा नहीं होता है।
विन्यासकर्ता

8
संभावना नहीं पैटर्न @Peter, लेकिन प्रशंसनीय के लिए(("working value" ?? "user default") ?? "system default")
फैक्टर रहस्यवादी

23
@ Yes123: जब यह सिर्फ रूपांतरण के साथ काम कर रहा था, तो मैं पूरी तरह आश्वस्त नहीं था। दो बार एक विधि को अंजाम देते देख यह स्पष्ट हो गया कि यह एक बग था। आप कुछ व्यवहार पर आश्चर्यचकित होंगे जो गलत लगता है लेकिन वास्तव में पूरी तरह से सही है। C # टीम मुझसे ज्यादा स्मार्ट है - मुझे लगता है कि मैं बेवकूफ साबित हो रहा हूं जब तक मैंने साबित नहीं किया कि कुछ उनकी गलती है।
जॉन स्कीट

जवाबों:


418

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

मैंने अभी तक यह नहीं पहचाना है कि वास्तव में चीजें कहां गलत हैं, लेकिन संकलन के "अशक्त कम करने" चरण के दौरान कुछ बिंदु पर - प्रारंभिक विश्लेषण के बाद लेकिन कोड पीढ़ी से पहले - हम अभिव्यक्ति को कम करते हैं

result = Foo() ?? y;

ऊपर के उदाहरण से नैतिक समकक्ष:

A? temp = Foo();
result = temp.HasValue ? 
    new int?(A.op_implicit(Foo().Value)) : 
    y;

स्पष्ट रूप से यह गलत है; सही कम है

result = temp.HasValue ? 
    new int?(A.op_implicit(temp.Value)) : 
    y;

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

result = Foo() ?? y;

के समान है

A? temp = Foo();
result = temp.HasValue ? 
    (int?) temp : 
    y;

और फिर हम कह सकते हैं कि

conversionResult = (int?) temp 

के समान है

A? temp2 = temp;
conversionResult = temp2.HasValue ? 
    new int?(op_Implicit(temp2.Value)) : 
    (int?) null

लेकिन ऑप्टिमाइज़र इसमें कदम रख सकता है और कह सकता है कि "हूए, एक मिनट रुको, हमने पहले ही चेक कर लिया है कि अस्थायी नहीं है। इसे दूसरी बार शून्य करने के लिए जाँच करने की कोई आवश्यकता नहीं है क्योंकि हम एक उठा हुआ रूपांतरण ऑपरेटर कह रहे हैं"। हम उन्हें सिर्फ इसे दूर अनुकूलित करेंगे

new int?(op_Implicit(temp2.Value)) 

मेरा अनुमान है कि हम कहीं न कहीं इस तथ्य का कैशिंग कर रहे हैं कि अनुकूलित रूप (int?)Foo() , new int?(op_implicit(Foo().Value))लेकिन यह वास्तव में इच्छित रूप नहीं है; हम फू का अनुकूलित रूप चाहते हैं () - प्रतिस्थापित-के साथ-अस्थायी-और-फिर-परिवर्तित।

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

हमने C # 3.0 में अशक्त पुनर्लेखन पास का बहुत पुनर्गठन किया। बग सी # 3.0 और 4.0 में पुन: उत्पन्न होता है लेकिन सी # 2.0 में नहीं है, जिसका अर्थ है कि यह बग शायद मेरा बुरा था। माफ़ करना!

मुझे डेटाबेस में एक बग मिलेगा और हम देखेंगे कि क्या हम इसे भाषा के भविष्य के संस्करण के लिए तय कर सकते हैं। आपके विश्लेषण के लिए फिर से सभी को धन्यवाद; यह बहुत मददगार था!

अद्यतन: मैं रोस्लिन के लिए खरोंच से अशक्त अनुकूलक को फिर से लिखता हूं; यह अब एक बेहतर काम करता है और इस प्रकार की अजीब त्रुटियों से बचा जाता है। रोसेलिन में ऑप्टिमाइज़र कैसे काम करता है, इस बारे में कुछ विचार के लिए, मेरे लेखों की श्रृंखला देखें, जो यहां शुरू होती है: https://ericlippert.com/2012/12/20/nullable-micro-optimifications-part-one/


1
@ मुझे आश्चर्य है कि अगर यह भी समझाएगा: connect.microsoft.com/VisualStudio/feedback/details/642227
MarkPflug

12
अब जबकि मेरे पास रोजलिन का एंड यूजर प्रीव्यू है, मैं पुष्टि कर सकता हूं कि यह वहीं तय है। (यह अभी भी देशी C # 5 संकलक में मौजूद है।)
जॉन स्कीट

84

यह निश्चित रूप से एक बग है।

public class Program {
    static A? X() {
        Console.WriteLine("X()");
        return new A();
    }
    static B? Y() {
        Console.WriteLine("Y()");
        return new B();
    }
    static C? Z() {
        Console.WriteLine("Z()");
        return new C();
    }

    public static void Main() {
        C? test = (X() ?? Y()) ?? Z();
    }
}

यह कोड आउटपुट करेगा:

X()
X()
A to B (0)
X()
X()
A to B (0)
B to C (0)

इससे मुझे लगा कि प्रत्येक ??मोटे अभिव्यक्ति के पहले भाग का मूल्यांकन दो बार किया जाता है। इस कोड ने इसे साबित कर दिया:

B? test= (X() ?? Y());

आउटपुट:

X()
X()
A to B (0)

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


11
वाह - दो बार अभिव्यक्ति का मूल्यांकन करना वास्तव में बहुत गलत लगता है। अच्छी तरह से देखा गया।
जॉन स्कीट

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

2
मैंने अपने प्रश्न के लिए इस "दोहरे मूल्यांकन" का थोड़ा सरल उदाहरण जोड़ा है।
जॉन स्कीट

8
क्या आपके सभी तरीके "एक्स ()" आउटपुट करने वाले हैं? यह कुछ हद तक यह बता पाना मुश्किल है कि वास्तव में कंसोल के लिए कौन सा तरीका है।
जेफोरा

2
ऐसा लगता X() ?? Y()है कि आंतरिक रूप से X() != null ? X() : Y()इसका विस्तार होगा , इसलिए इसका मूल्यांकन दो बार क्यों किया जाएगा।
कोल जॉनसन

54

यदि आप वाम-समूह वाले मामले के लिए उत्पन्न कोड पर एक नज़र डालें तो यह वास्तव में ऐसा कुछ करता है ( csc /optimize-):

C? first;
A? atemp = a;
B? btemp = (atemp.HasValue ? new B?(a.Value) : b);
if (btemp.HasValue)
{
    first = new C?((atemp.HasValue ? new B?(a.Value) : b).Value);
}

दूसरे को ढूंढने में, यदि आप का उपयोग first यह दोनों अगर एक शॉर्टकट उत्पन्न होगा aऔर bबातिल और वापसी कर रहे हैं c। फिर भी अगर aया bगैर-अशक्त है, तो यह लौटने aसे Bपहले निहित रूपांतरण के हिस्से के रूप में पुनर्मूल्यांकन करता है कि कौन सा aया bगैर-अशक्त है।

C # 4.0 विशिष्टता से, .16.1.4:

  • यदि अशक्त रूपांतरण निम्न से S?है T?:
    • यदि स्रोत मान है null( HasValueगुण है false), तो परिणाम nullप्रकार का मूल्य है T?
    • अन्यथा, रूपांतरण से एक unwrapping के रूप में मूल्यांकन किया जाता है S?करने के लिए S, से अंतर्निहित रूपांतरण के बाद Sकरने के लिए T, से एक रैपिंग (§4.1.10) द्वारा पीछा Tकरने के लिए T?

यह दूसरा अलिखित रैपिंग संयोजन को समझाने के लिए प्रतीत होता है।


C # 2008 और 2010 कंपाइलर बहुत समान कोड का उत्पादन करते हैं, हालांकि यह C # 2005 कंपाइलर (8.00.50727.4927) से एक प्रतिगमन जैसा दिखता है जो उपरोक्त के लिए निम्न कोड उत्पन्न करता है:

A? a = x;
B? b = a.HasValue ? new B?(a.GetValueOrDefault()) : y;
C? first = b.HasValue ? new C?(b.GetValueOrDefault()) : z;

मुझे आश्चर्य है कि क्या यह प्रकार की अतिरिक्त प्रणाली को दिए गए अतिरिक्त जादू के कारण नहीं है?


+1, लेकिन मुझे नहीं लगता कि यह वास्तव में बताता है कि रूपांतरण दो बार क्यों किया जाता है। यह केवल एक बार अभिव्यक्ति का मूल्यांकन करना चाहिए, आईएमओ।
जॉन स्कीट

@Jon: मैं चारों ओर खेल रहा था और पाया (जैसा @configurator ने किया था) कि जब एक्सप्रेशन ट्री में किया जाता है तो यह उम्मीद के मुताबिक काम करता है। इसे मेरी पोस्ट में जोड़ने के लिए अभिव्यक्तियों को साफ करने पर काम कर रहा हूं। मुझे यह बताना होगा कि यह एक "बग" है।
user7116

@Jon: ठीक है जब अभिव्यक्ति पेड़ का उपयोग करते हुए यह (x ?? y) ?? zनेस्टेड लैम्ब्डा में बदल जाता है , जो दोहरे मूल्यांकन के बिना आदेश मूल्यांकन सुनिश्चित करता है। यह स्पष्ट रूप से C # 4.0 संकलक द्वारा लिया गया दृष्टिकोण नहीं है। मैं जो बता सकता हूं, उससे खंड 6.1.4 को इस विशेष कोड पथ में बहुत सख्त तरीके से संपर्क किया जाता है और अस्थायी मूल्यांकन को समाप्त नहीं किया जाता है जिसके परिणामस्वरूप दोहरा मूल्यांकन होता है।
user7116

16

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

ऐसा लगता है जैसे कि A ?? Bइसे लागू किया गया है A.HasValue ? A : B। इस मामले में, बहुत सी कास्टिंग भी है (टर्नरी ?:ऑपरेटर के लिए नियमित कास्टिंग के बाद )। लेकिन अगर आप उस सब को नजरअंदाज करते हैं, तो यह समझ में आता है कि यह कैसे लागू होता है:

  1. A ?? B तक फैलता है A.HasValue ? A : B
  2. Aहमारा है x ?? y। का विस्तार करेंx.HasValue : x ? y
  3. A -> की सभी घटनाओं को बदलें (x.HasValue : x ? y).HasValue ? (x.HasValue : x ? y) : B

यहां आप देख सकते हैं कि x.HasValueदो बार चेक किया गया है, और यदि x ?? yकास्टिंग की आवश्यकता है, तो xदो बार डाली जाएगी।

मैं इसे बस ??संकलक बग के बजाय कैसे लागू किया जाता है की एक कलाकृति के रूप में नीचे रखूँगा। टेक-अवे: साइड इफेक्ट के साथ निहित कास्टिंग ऑपरेटरों का निर्माण न करें।

यह कैसे ??लागू किया जाता है के आसपास एक संकलक बग घूमने लगता है। टेक-ऑफ़: साइड-इफ़ेक्ट के साथ कोलेसलेसिंग एक्सप्रेशन नहीं।


ओह, मैं निश्चित रूप से इस तरह के कोड का उपयोग नहीं करना चाहता हूं, लेकिन मुझे लगता है कि इसे अभी भी कंपाइलर बग के रूप में वर्गीकृत किया जा सकता है जिसमें आपके पहले विस्तार में "लेकिन केवल ए और बी का एक बार मूल्यांकन करना" शामिल होना चाहिए। (कल्पना करें कि क्या वे विधि कॉल थे।)
जॉन स्कीट

@ मैं सहमत हूँ कि यह भी हो सकता है - लेकिन मैं इसे स्पष्ट नहीं कहूँगा। खैर, वास्तव में, मैं देख सकता हूं कि A() ? A() : B()संभवतः A()दो बार मूल्यांकन करेंगे , लेकिन A() ?? B()इतना नहीं। और चूँकि यह केवल कास्टिंग पर होता है ... हम्म .. मैंने अभी खुद से यह सोचकर बात की है कि यह निश्चित रूप से सही व्यवहार नहीं कर रहा है।
फिलिप रीक

10

मैं एक सी # विशेषज्ञ नहीं हूं, जैसा कि आप मेरे प्रश्न इतिहास से देख सकते हैं, लेकिन, मैंने इसे आज़माया है और मुझे लगता है कि यह एक बग है .... लेकिन एक नौसिखिया के रूप में, मुझे कहना होगा कि मुझे सब कुछ समझ नहीं आ रहा है यहाँ पर मैं अपने जवाब को हटा दूंगा अगर मैं रास्ता बंद कर रहा हूँ।

मैं bugआपके कार्यक्रम का एक अलग संस्करण बनाकर इस निष्कर्ष पर पहुंचा हूं जो समान परिदृश्य से संबंधित है, लेकिन बहुत कम जटिल है।

मैं बैकिंग स्टोर के साथ तीन अशक्त पूर्णांक गुणों का उपयोग कर रहा हूं। मैं प्रत्येक को 4 पर सेट करता हूं और फिर चलाता हूंint? something2 = (A ?? B) ?? C;

( पूर्ण कोड यहाँ )

यह सिर्फ ए पढ़ता है और कुछ नहीं।

मेरे लिए यह कथन मेरे जैसा दिखता है:

  1. कोष्ठक में शुरू करें, ए को देखें, ए को वापस करें और यदि ए शून्य नहीं है तो समाप्त करें।
  2. यदि A शून्य था, B का मूल्यांकन करें, यदि B शून्य नहीं है, तो समाप्त करें
  3. यदि A और B शून्य थे, C का मूल्यांकन करें।

इसलिए, जैसा कि A शून्य नहीं है, यह केवल A को देखता है और खत्म करता है।

आपके उदाहरण में, First Case में एक ब्रेकपॉइंट लगाने से पता चलता है कि x, y और z सभी शून्य नहीं हैं और इसलिए, मैं उनसे यही अपेक्षा करूंगा कि वे मेरे कम जटिल उदाहरण के समान ही व्यवहार करें .... लेकिन मुझे डर है कि मैं बहुत अधिक हूं एक सी # नौसिखिया की और इस सवाल के बिंदु को पूरी तरह से याद किया है!


5
जॉन का उदाहरण कुछ अस्पष्ट कोने के मामले में है कि वह एक अशक्त संरचना (एक मूल्य-प्रकार जो "बिल्ट-इन प्रकारों के समान" int) का उपयोग कर रहा है। वह कई अस्पष्ट प्रकार के रूपांतरण प्रदान करके मामले को एक अस्पष्ट कोने में आगे बढ़ाता है। इसके लिए कंपाइलर के खिलाफ जाँच करते समय डेटा के प्रकार को बदलने की आवश्यकता होती है null। यह इन प्रकार के रूपांतरणों के कारण है कि उसका उदाहरण आपसे अलग है।
user7116
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.