c# - जिज्ञासु नल-कोलेसिंग ऑपरेटर कस्टम निहित रूपांतरण व्यवहार




null-coalescing-operator (4)

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

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

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

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

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

ऐसा लगता है कि कैसे एक कंपाइलर बग घूमती है ?? लागू किया गया है। दूर ले जाएं: दुष्प्रभावों के साथ अभिव्यक्तियों को घोंसला न करें।

नोट: ऐसा लगता है कि Roslyn में तय किया गया है

यह सवाल तब उठ गया जब मेरा जवाब लिख रहा था, जो नल-कोलेसिंग ऑपरेटर की सहयोगीता के बारे में बात करता है।

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

x ?? y

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

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

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

x ?? y के साधारण मामले के लिए x ?? y x ?? y , मैंने कोई अजीब व्यवहार नहीं देखा है। हालांकि, के साथ (x ?? y) ?? z (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() को यहां दो बार बुलाया जाता है, यह मेरे लिए बेहद आश्चर्यजनक है - अभिव्यक्ति के लिए मुझे दो बार मूल्यांकन करने का कोई कारण नहीं दिख रहा है।


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

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

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)) लेकिन यह वास्तव में अनुकूलित रूप नहीं है जिसे हम चाहते हैं; हम फू () - के साथ-साथ-अस्थायी-और-फिर-रूपांतरित के अनुकूलित रूप चाहते हैं।

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

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

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

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


यदि आप बाएं समूह वाले मामले के लिए जेनरेट किए गए कोड को देखते हैं तो यह वास्तव में ऐसा कुछ करता है ( 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 गैर-शून्य है तो यह वापस लौटने से पहले B को अंतर्निहित रूपांतरण के हिस्से के रूप में फिर से मूल्यांकन करता है, जिसमें से a या b गैर-शून्य है।

सी # 4.0 विशिष्टता से, §6.1.4:

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

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

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

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

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


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

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)

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

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

आउटपुट:

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

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







null-coalescing-operator