सी # और जेनेरिक - व्युत्पन्न कक्षा में नई विधि के बजाय बेस क्लास में विधि क्यों है?

यदि जेनेरिक टाइप तर्क (या तो एक कॉलिंग क्लास या कॉलिंग विधि) को जहां टी: बेस के साथ बाध्य किया जाता है तो टी == व्युत्पन्न में नई विधि को कॉल नहीं किया जाता है, इसके बजाय बेस में विधि कहा जाता है।

विधि टी के लिए टाइप टी को क्यों नजरअंदाज किया जाता है, भले ही इसे रन टाइम से पहले जाना जाना चाहिए?

Update: BUT, when the constraint is using an interface like where T : IBase the method in Base class is called (not the method in interface, which is also impossible).
So that means the system actually is able to detect the types that far and go beyond the type constraint! Then why doesn't it go beyond the type constraint in case of class-typed constraint?
Does that mean that the method in Base class that implements the interface has implicit override keyword for the method?

टेस्ट कोड:

public interface IBase
{
    void Method();
}

public class Base : IBase 
{
    public void Method()
    {

    }
}

public class Derived : Base
{
    public int i = 0;

    public new void Method()
    {
        i++;
    }
}

public class Generic
    where T : Base
{
    public void CallMethod(T obj)
    {
        obj.Method();  //calls Base.Method()
    }

    public void CallMethod2(T2 obj)
        where T2 : T
    {
        obj.Method();  //calls Base.Method()
    }
}

public class GenericWithInterfaceConstraint
    where T : IBase
{
    public void CallMethod(T obj)
    {
        obj.Method();  //calls Base.Method()
    }

    public void CallMethod2(T2 obj)
        where T2 : T
    {
        obj.Method();  //calls Base.Method()
    }
}

public class NonGeneric
{
    public void CallMethod(Derived obj)
    {
        obj.Method();  //calls Derived.Method()
    }

    public void CallMethod2(T obj)
        where T : Base
    {
        obj.Method();  //calls Base.Method()
    }

    public void CallMethod3(T obj)
        where T : IBase
    {
        obj.Method();  //calls Base.Method()
    }
}

public class NewMethod
{
    unsafe static void Main(string[] args)
    {
        Generic genericObj = new Generic();
        GenericWithInterfaceConstraint genericObj2 = new GenericWithInterfaceConstraint();
        NonGeneric nonGenericObj = new NonGeneric();
        Derived obj = new Derived();

        genericObj.CallMethod(obj);  //calls Base.Method()
        Console.WriteLine(obj.i);

        genericObj.CallMethod2(obj);  //calls Base.Method()
        Console.WriteLine(obj.i);

        genericObj2.CallMethod(obj);  //calls Base.Method()
        Console.WriteLine(obj.i);

        genericObj2.CallMethod2(obj);  //calls Base.Method()
        Console.WriteLine(obj.i);

        nonGenericObj.CallMethod(obj);  //calls Derived.Method()
        Console.WriteLine(obj.i);

        nonGenericObj.CallMethod2(obj);  //calls Base.Method()
        Console.WriteLine(obj.i);

        nonGenericObj.CallMethod3(obj);  //calls Base.Method()
        Console.WriteLine(obj.i);

        obj.Method();  //calls Derived.Method()
        Console.WriteLine(obj.i);
    }
}

आउटपुट:

0
0
0
0
1
1
1
2
0
जोड़ा संपादित
विचारों: 1
आपको क्या लगता है कि इंटरफ़ेस-बाधित वाला कोई IBase.Method नहीं बुला रहा है? Obj को IBase पर कास्टिंग करने और विधि को कॉल करने का प्रयास करें।
जोड़ा लेखक supercat, स्रोत
ध्यान दें कि यदि आप इसके बजाय लिखते हैं तो यह एक फर्क पड़ता है सार्वजनिक वर्ग व्युत्पन्न: बेस, आईबीएएस , यानी व्युत्पन्न वर्ग की घोषणा में इंटरफ़ेस दोहराएं। इसे इंटरफ़ेस पुन: कार्यान्वयन कहा जाता है। spec देखें
जोड़ा लेखक Jeppe Stig Nielsen, स्रोत

4 उत्तर

गतिशील ऑब्जेक्ट्स का उपयोग करते समय, सी # हमेशा संकलन समय पर तरीकों को बांधता है - यहां तक ​​कि जेनेरिक का उपयोग करते समय भी। वर्चुअल विधि कॉल कार्यान्वयन विधियों के बजाए आभासी विधि स्लॉट से बंधे हैं, ताकि जब वे व्युत्पन्न-वर्ग वस्तुओं पर प्रदर्शन किए जाते हैं तो उन्हें व्युत्पन्न-वर्ग कार्यान्वयन के लिए निर्देशित किया जाएगा; हालांकि जिस तरीके से स्लॉट पॉइंट रन टाइम पर निर्धारित किया जाएगा, स्लॉट के लिए बाध्यकारी संकलन समय पर होता है। यदि व्युत्पन्न-वर्ग विधि को नया घोषित किया गया है, तो ओवरराइड की बजाय, व्युत्पन्न कक्षा का उपयोग करके बाध्य कोड कोड व्युत्पन्न-वर्ग विधि का उपयोग करेगा, लेकिन कोड जो उपयोग करके बाध्य है बेस क्लास बेस-क्लास विधि का उपयोग करेगा।

यह समझने के लिए कि यह मामला क्यों होना चाहिए, कल्पना करें कि यह नहीं था। क्या होना चाहिए यदि वर्ग <�कोड> आधार एक विधि int Foo() घोषित करता है, और एक वर्ग <�कोड> व्युत्पन्न: आधार एक नई स्ट्रिंग Foo घोषित करता है ( ) । यदि बाधा वाला एक सामान्य वर्ग टी: बेस F प्रकार की ऑब्जेक्ट पर Foo विधि को कॉल करने का प्रयास करता है, तो उस विधि का रिटर्न प्रकार क्या होना चाहिए हो सकता है?

0
जोड़ा
रिटर्न प्रकार के साथ अच्छा बिंदु। (आप विधि बाध्यकारी और आभासी कॉल और स्लॉट के बारे में जा सकते हैं, लेकिन हम में से कुछ को इसे समझने के लिए एक स्पष्ट तोड़ने की आवश्यकता है।)
जोड़ा लेखक Rawling, स्रोत
@ राउलिंग: बाइंडिंग पर चर्चा करने का कारण यह स्पष्ट करना है कि कंपाइलर के परिप्रेक्ष्य से, एक सामान्य प्रकार T Foo पर बाध्य होता है, कई तरीकों से Foo वास्तविक प्रकार की तरह है जिसे रन-टाइम पर T के लिए प्रतिस्थापित किया जा सकता है। .NET में जेनिक्स सी ++ टेम्पलेट्स की तरह दिखते हैं, लेकिन वे मूल रूप से अलग हैं।
जोड़ा लेखक supercat, स्रोत
@ रोलैंड पिहलाकास: मुझे संदेह है कि वास्तव में यह एक कारण है। यहां तक ​​कि यदि कोई नियम तैयार करना था जो संकलक को इस तरह के मुद्दों से उत्पन्न सभी संभावित अस्पष्टताओं को हल करने की अनुमति देगा, जेनेरिक व्यवहार करते हैं जैसे कि वे रन-टाइम बाध्य हैं, इसके लिए आवश्यक होगा कि प्रत्येक जेनेरिक विधि को उस सामान्य प्रकार के प्रत्येक संयोजन के लिए पुन: संकलित किया जाए । अपेक्षाकृत कम लाभ की पेशकश करते हुए, यह प्रदर्शन को गंभीर रूप से नुकसान पहुंचाएगा।
जोड़ा लेखक supercat, स्रोत
@ रोलैंड पिहलाकास: जेनिक्स का उपयोग करना रन-टाइम की बजाय संकलन समय पर कई प्रकार के प्रकार के चेक करना संभव बनाता है। उदाहरण के लिए, यदि कोड किसी आइटम को IEnumerable से पढ़ता है, तो यह आइटम को Animal के रूप में उपयोग-समय पर जांचने के बिना कर सकता है चाहे वह है या नहीं । यह दोनों कोड को टाइप-टाइम पर टाइप करने की आवश्यकता को समाप्त करता है (जिसमें समय लगेगा) और यह भी संभावना समाप्त हो जाती है कि रन-टाइम प्रकार की जांच विफल हो सकती है।
जोड़ा लेखक supercat, स्रोत
@ रोलैंड पिहलाकास: यह भी ध्यान रखना महत्वपूर्ण है कि एक सी ++ प्रोग्राम संकलित करने के लिए, कंपाइलर प्रोग्राम के निष्पादन के दौरान बनाए गए सभी टेम्पलेट प्रकारों की पहचान करने में सक्षम होना चाहिए, और संकलित प्रोग्राम का आकार आनुपातिक हो जाएगा एक विधि का उपयोग कर सकते हैं प्रकार के विभिन्न संयोजनों की संख्या के लिए। सी # में, प्रतिबिंब का उपयोग किए बिना, कोई भी एक प्रोग्राम लिख सकता है जो अंक की मनमानी-लंबाई स्ट्रिंग (जैसे 24601 ) लेता है और एक नेस्टेड जेनेरिक प्रकार (जैसे X2) के साथ सामान्य दिनचर्या को कॉल करता है >>>> )। नेस्टेड जेनेरिक प्रकार
जोड़ा लेखक supercat, स्रोत
... इस तरह की एक नियमित जेनरिक दिनचर्या को पारित कर सकती है ब्रह्मांड में परमाणुओं की संख्या से काफी अधिक हो जाएगी, वहां कोई भी तरीका नहीं होगा कि संकलक सभी के लिए कोड पूर्व-उत्पन्न कर सके। जेआईटीटर जेनरेट के लिए उत्पन्न होने वाले प्रत्येक प्रकार के लिए जेनेरिक विधि को संकलित करना संभव होगा, लेकिन प्रदर्शन लागत लाभ से कहीं अधिक होगी।
जोड़ा लेखक supercat, स्रोत
@ रोलैंड पिहलाकास: माइक्रोसॉफ्ट ने जेआईटी कंपाइलर को इस तरह से डिजाइन किया होगा, लेकिन इसमें एक महत्वपूर्ण रन-टाइम प्रदर्शन लागत होगी, और वास्तव में ऐसी कई स्थितियां नहीं हैं जहां यह कुछ भी उपयोगी करेगी जो संयोजन का उपयोग करके बेहतर नहीं किया जा सका आभासी कार्यों और प्रतिबिंब के संकलन समय बाध्यकारी। यद्यपि .NET सामान्य प्रकार के वर्ग प्रकारों के प्रत्येक संयोजन के लिए सामान्य कोड को पुन: संकलित नहीं करता है, जेनेरिक वर्ग सामान्य प्रकार पैरामीटर के प्रत्येक संयोजन के लिए स्थिर चर का एक अलग सेट धारण करते हैं।
जोड़ा लेखक supercat, स्रोत
@supercat: धन्यवाद, मुझे लगता है कि अगर नए तरीकों के रिटर्न प्रकारों का मुद्दा नहीं था तो डिजाइन अलग हो सकता था। यह मुझे इस डिजाइन निर्णय के एकल कारण लगता है। बाइंडिंग और ओवरलोडिंग आदि के बारे में सब कुछ सिर्फ परिणाम बता रहा है। मुझे इस प्रश्न में और अधिक स्पष्ट रूप से कहा जाना चाहिए था कि मुझे ओवरलोडिंग के अस्तित्व के बारे में पता है।
जोड़ा लेखक Roland Pihlakas, स्रोत
@supercat: हाँ, यह अनिवार्य होगा कि विभिन्न सामान्य प्रकारों के लिए कई विधियों को संकलित किया जा रहा है। लेकिन सी ++ वह भी करता है, है ना? मैं उम्मीद कर रहा था कि जेनेरिक का उपयोग करके मुझे वर्चुअल कॉल से छुटकारा मिल जाता है। ऐसा लगता है कि यह मामला नहीं है और जेनिक्स के एकमात्र प्रदर्शन से संबंधित उपयोग मुक्केबाजी मूल्य प्रकारों से बचने के लिए है। कक्षाओं के लिए, इंटरफेस (या वर्चुअल विधियों के साथ बेस क्लास) के बजाय सामान्य तर्क प्रकारों का उपयोग करके प्रदर्शन लाभ प्रदान नहीं कर सकते हैं।
जोड़ा लेखक Roland Pihlakas, स्रोत
@supercat: हाँ, मैं उम्मीद कर रहा था कि जेआईटी जेनेरिक तरीकों/प्रकारों को जेनरेट करता है क्योंकि उन्हें पहले शुरू किया गया था। इसी तरह एक बार स्थिर प्रकार की शुरुआत के लिए।
जोड़ा लेखक Roland Pihlakas, स्रोत

ऐसा इसलिए है क्योंकि T को base के अर्थशास्त्र के लिए बाध्य किया गया है। मैं आपको बता नहीं सकता कि रनटाइम पर बाध्यकारी प्रकार के साथ क्या चल रहा है, लेकिन यह मेरा शिक्षित अनुमान है।

आप विधि को सही तरीके से ओवरराइड नहीं कर रहे हैं, बल्कि इसके बजाय "नया" के माध्यम से छिपे हुए हैं, यदि आप बेस क्लास के संदर्भ का उपयोग करते हैं तो आप किसी भी छिपाने को बाईपास करते हैं। यह वह जगह है जहां छुपा रहता है।

जो सदस्य अन्य सदस्यों को छिपाते हैं उन्हें केवल सम्मानित किया जाता है यदि आप उस प्रकार के संदर्भ का उपयोग कर रहे हैं जिसमें वे छिपे हुए हैं। आप बेस क्लास के संदर्भ का उपयोग करके हमेशा एक छिपे हुए सदस्य को बाईपास कर सकते हैं:

var derived = new Derived();
var baseRef = (Base)derived;
baseRef.Method();//calls Base.Method instead of Derived.Method.

विधि को सही तरीके से ओवरराइड करने के लिए और यह कोड काम करने के लिए, विधि को बेस क्लास में वर्चुअल के रूप में चिह्नित करें और व्युत्पन्न कक्षा में ओवरराइड को चिह्नित करें।

class Base
{
    public virtual void Method() {}
}

class Derived : Base
{
    public override void Method() {}
}

आप इसे साबित कर सकते हैं, अपनी जेनेरिक बाधा को जहां टी: व्युत्पन्न होने के लिए बदलें और इसे "नया" सदस्य हिट करना चाहिए।

0
जोड़ा

यह ऑपरेटर की प्रकृति के कारण नई है: ओवरराइड के विपरीत नया, आधार नाम के समान नाम वाला एक फ़ंक्शन बनाएं, जो आधार विधि को मुखौटा करता है लेकिन इसे ओवरराइड नहीं करता है।

इसके लिए, एक उचित कलाकार के बिना, मूल विधि को संदर्भित किया जाएगा यदि संदर्भ प्रकार बेस है।

0
जोड़ा

नया कीवर्ड बस ओवरलोड करने की बजाय विधि को छुपाता है। आपकी गैर-जेनेरिक CallMethod अपेक्षित रूप से काम करने का कारण है क्योंकि विधि हस्ताक्षर बेस के बजाय व्युत्पन्न की अपेक्षा करता है।

जेनेरिक वास्तव में यहां अपराधी नहीं हैं। यदि आप विधि हस्ताक्षर CallMethod (बेस obj) में बदलते हैं, तो आपको सामान्य कार्यान्वयन के रूप में एक ही "अप्रत्याशित" व्यवहार दिखाई देगा और निम्न आउटपुट प्राप्त होगा:

0
0
0
0
0
0
0
1

यदि आप Base.Method वर्चुअल बनाते हैं और Derived.Method के साथ इसे ओवरराइड करते हैं:

public class Base 
{
    public virtual void Method()
    {

    }
}

public class Derived : Base
{
    public int i = 0;

    public override void Method()
    {
        i++;
    }
}

आपको निम्न आउटपुट मिलेगा:

1
2
3
4
5
6
7
8

Edit: updated to match question's updated output.

0
जोड़ा