C # & generice - de ce se numește metoda din clasa de bază în locul metodei noi în clasa derivată?

Dacă argumentul de tip generic (fie de o clasă de apeluri, fie de o metodă de apelare) este constrâns cu în cazul în care T: Base , noua metodă din T == Derived nu este apelată, în schimb se numește metoda din Base.

De ce este ignorat tipul T pentru apelul metodei, chiar dacă ar trebui să fie cunoscut înainte de timpul de execuție?

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?

Cod de încercare:

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);
    }
}

ieşire:

0
0
0
0
1
1
1
2
0
Ce te face să crezi că interfața constrânsă nu sună IBase.Method ? Încercați să introduceți Obj la IBase și apelați Method pe acesta.
adăugat autor supercat, sursa
Rețineți că este o diferență dacă scrieți, în schimb, public class Derived: Base, IBase , adică repetați interfața în declarația clasei derivate, de asemenea. Aceasta se numește re-implementarea interfeței . Consultați specificația .
adăugat autor Jeppe Stig Nielsen, sursa

4 răspunsuri

Cu excepția cazului în care se utilizează obiecte dinamic , C# se leagă mereu de metode la momentul compilării - chiar și atunci când utilizează generice. Apelurile metodelor virtuale sunt legate de sloturile de metode virtuale, mai degrabă decât de metodele de implementare, astfel încât atunci când sunt realizate pe obiecte de clasă derivată, acestea vor fi direcționate către implementările din clasa derivată; deși metodele la care punctul de sloturi vor fi determinate la timpul de execuție, legarea la sloturi are loc la timpul de compilare. Dacă o metodă de clasă derivată este declarată new mai degrabă decât override , codul care este legat folosind clasa derivată va utiliza metoda de clasă derivată, dar codul care este legat folosind clasa de bază va folosi metoda de clasă de bază.

Pentru a înțelege de ce trebuie să se întâmple acest lucru, imaginați-vă dacă nu ați fost. Ce ar trebui să se întâmple dacă clasa Base declară o metodă int Foo() și Derived: Base ) . Dacă o clasă generică cu constrângere T: Base încearcă să apeleze metoda Foo pe un obiect de tip T fi?

0
adăugat
Punct bun cu tipul de returnare. (Puteți continua legarea cu metode și apelurile virtuale și sloturile, dar unii dintre noi au nevoie de un exemplu evident de rupere pentru a le da seama.)
adăugat autor Rawling, sursa
@Rawling: Motivul discutării legării este acela de a clarifica faptul că, din perspectiva compilatorului, un tip generic T constrâns la Foo se va comporta mai mult ca un Foo decât ca tipul real care poate fi înlocuit cu T în timpul run-time. Genericitățile în .net arată ca șabloane C ++, dar ele sunt fundamental diferite.
adăugat autor supercat, sursa
@RolandPihlakas: Mă îndoiesc că este singura cauză, de fapt. Chiar dacă ar trebui să se formuleze reguli care să permită compilatorului să rezolve toate ambiguitățile posibile generate de astfel de probleme, genericii se comportă ca și cum ar fi legați de timpul de execuție ar necesita recompilarea fiecărei metode generice pentru fiecare combinație de tipuri generice folosite . Acest lucru ar dăuna serios performanței, oferind în același timp beneficii relativ reduse.
adăugat autor supercat, sursa
@RolandPihlakas: Utilizarea genericelor face posibilă efectuarea mai multor tipuri de verificări de tip la timpul de compilare, nu în timpul run-time. De exemplu, dacă un cod citește un element dintr-un IEnumerabil , poate folosi elementul ca Animale fără a fi nevoie să verifice la run-time dacă este sau nu . Acest lucru elimină atât nevoia de a verifica tipul de cod la timpul de execuție (ceea ce ar necesita timp), cât și eliminarea posibilității ca o verificare de tipul run-time să nu reușească.
adăugat autor supercat, sursa
@RolandPihlakas: Este, de asemenea, important să rețineți că pentru un program C ++ care trebuie compilat, compilatorul trebuie să poată identifica toate tipurile șablonate care ar putea fi create în timpul execuției programului, iar mărimea programului compilat va crește proporțional la numărul de combinații diferite de tipuri pe care le poate utiliza o metodă. În C#, chiar fără a folosi Reflection, se poate scrie un program care să ia un șir de cifre arbitrare (de exemplu 24601 ) și să apeleze o rutină generică cu un tip generic imbricat (de exemplu X2
adăugat autor supercat, sursa
... o astfel de rutină ar putea trece la rutina generică ar depăși cu mult numărul atomilor din univers, nu ar exista nici un fel în care compilatorul ar putea să genereze coduri pentru toți. Ar fi posibil ca JITter să compileze o metodă generică o dată pentru fiecare tip care este generat, dar costul de performanță ar depăși cu mult avantajul.
adăugat autor supercat, sursa
@RolandPihlakas: Microsoft ar fi putut proiecta compilatorul JIT în acest fel, dar ar fi avut un cost semnificativ de performanță în timpul run-time și nu există într-adevăr multe situații în care să realizeze ceva util, care nu ar putea fi făcut mai bine folosind o combinație de legare compilativă a funcțiilor virtuale și de reflecție. Chiar dacă .net nu recompila codul generic pentru fiecare combinație de tipuri de clase pe care le folosește, clasele generice dețin un set separat de variabile statice pentru fiecare combinație de parametri de tip generic.
adăugat autor supercat, sursa
@supercat: Mulțumesc, cred că dacă nu ar exista problema de returnare tipuri de metode noi, atunci designul ar fi putut fi diferit. Acest lucru îmi pare a fi cauza unică a acestei decizii de proiectare. Totul altceva despre legarea și nu supraîncărcarea etc doar afirmă consecința. De asemenea, ar fi trebuit să spun mai clar în întrebarea pe care o cunosc despre existența supraîncărcării.
adăugat autor Roland Pihlakas, sursa
@supercat: Da, ar fi inevitabil ca mai multe metode să fie compilate pentru diferite tipuri generice. Dar C ++ face și asta, nu-i așa? Speram că utilizarea genericelor îmi permite să scap de apelurile virtuale. Se pare că acest lucru nu este cazul și singura utilizare a genericilor legate de performanță este de a evita tipurile de valori de box. Pentru clase, folosirea tipurilor generice de argument în loc de interfețe (sau clase de bază cu metode virtuale) nu poate oferi beneficii de performanță.
adăugat autor Roland Pihlakas, sursa
@supercat: Da, speram ca JIT genereaza metode/tipuri generice pe masura ce sunt initial initializate. Similar cu inițializarea inițială de tip statică.
adăugat autor Roland Pihlakas, sursa
var derived = new Derived();
var baseRef = (Base)derived;
baseRef.Method();//calls Base.Method instead of Derived.Method.

Pentru a suprascrie o metodă și a avea acest cod, marcați metoda ca virtual din clasa de bază și suprascrie în clasa derivată.

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

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

Puteți demonstra acest lucru, schimbați constrângerea dvs. generică pentru a fi unde T: Derived și ar trebui să apară pe noul membru.

0
adăugat

Acest lucru se datorează naturii operatorului nou: Noi, spre deosebire de suprascriere, creați o funcție cu același nume ca cea de bază, care maschează metoda de bază, dar nu o suprascrie.

Prin urmare, fără o distribuție corespunzătoare, metoda inițială va fi apelată dacă referința este de tip Base.

0
adăugat

Cuvântul new ascunde metoda în loc să o supraîncarce. Motivul pentru care codul CallMethod non-generic pare să funcționeze conform așteptărilor se datorează faptului că semnătura metodei se așteaptă ca Derived în loc de Base .

Genericii nu sunt chiar vinovați aici. Dacă schimbați semnătura metodei la CallMethod (Base obj) , veți vedea același comportament "neașteptat" ca și implementarea generică și obțineți următoarea ieșire:

0
0
0
0
0
0
0
1

Dacă faceți Base.Method virtuale și o înlocuiți cu Derived.Method după cum urmează:

public class Base 
{
    public virtual void Method()
    {

    }
}

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

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

Veți obține următoarele rezultate:

1
2
3
4
5
6
7
8

Edit: updated to match question's updated output.

0
adăugat