2012-04-18 7 views
12

Diciamo che abbiamo seguente codice di esempio in C#:Perché compilatore C# metodo di produzione di chiamata per chiamare il metodo BaseClass in IL

class BaseClass 
    { 
    public virtual void HelloWorld() 
    { 
     Console.WriteLine("Hello Tarik"); 
    } 
    } 

    class DerivedClass : BaseClass 
    { 
    public override void HelloWorld() 
    { 
     base.HelloWorld(); 
    } 
    } 

    class Program 
    { 
    static void Main(string[] args) 
    { 
     DerivedClass derived = new DerivedClass(); 
     derived.HelloWorld(); 
    } 
    } 

Quando ho ildasmed il seguente codice:

.method private hidebysig static void Main(string[] args) cil managed 
{ 
    .entrypoint 
    // Code size  15 (0xf) 
    .maxstack 1 
    .locals init ([0] class EnumReflection.DerivedClass derived) 
    IL_0000: nop 
    IL_0001: newobj  instance void EnumReflection.DerivedClass::.ctor() 
    IL_0006: stloc.0 
    IL_0007: ldloc.0 
    IL_0008: callvirt instance void EnumReflection.BaseClass::HelloWorld() 
    IL_000d: nop 
    IL_000e: ret 
} // end of method Program::Main 

Tuttavia, csc .exe convertito derived.HelloWorld(); ->callvirt instance void EnumReflection.BaseClass::HelloWorld(). Perché? Non ho citato BaseClass ovunque nel metodo Main.

E anche se sta chiamando BaseClass::HelloWorld() quindi mi aspetterei call invece di callvirt dal momento che sembra chiamata diretta a BaseClass::HelloWorld() metodo.

+2

Lanciare questa idea, ma non lo so: potrebbe essere un'ottimizzazione del compilatore in quanto il metodo si chiama semplicemente l'implementazione di base. – payo

+0

@payo no; anche se l'override avesse una logica unica, il callvirt sarebbe lo stesso. – phoog

+0

@phoog questo non è lo stesso per tutte le lingue - posso vedere come questo è il caso per C# tho – payo

risposta

20

La chiamata va a BaseClass :: HelloWorld perché BaseClass è la classe che definisce il metodo. Il modo in cui la distribuzione virtuale funziona in C# è che il metodo viene chiamato sulla classe base e il sistema di invio virtuale ha la responsabilità di garantire che venga richiamata l'override più derivata del metodo.

Questa risposta di Eric Lippert di è molto istruttiva: https://stackoverflow.com/a/5308369/385844

Come è la sua serie di blog sul tema: http://blogs.msdn.com/b/ericlippert/archive/tags/virtual+dispatch/

Avete qualche idea del perché questo è implementato in questo modo? Cosa accadrebbe se stesse chiamando direttamente il metodo ToString della classe derivata? In questo modo, a prima vista, non aveva molto senso ...

È implementato in questo modo perché il compilatore non traccia il tipo di oggetti runtime, solo il tipo di tempo di compilazione dei loro riferimenti. Con il codice che hai pubblicato, è facile vedere che la chiamata andrà all'implementazione DerivedClass del metodo. Ma supponiamo che la variabile derived è stato inizializzato in questo modo:

Derived derived = GetDerived(); 

E 'possibile che GetDerived() restituisce un'istanza di StillMoreDerived. Se StillMoreDerived (o qualsiasi classe compresa tra Derived e StillMoreDerived nella catena di ereditarietà) sovrascrive il metodo, quindi sarebbe errato chiamare l'implementazione Derived del metodo.

Per trovare tutti i valori possibili che una variabile può contenere attraverso l'analisi statica è quello di risolvere il problema di interruzione. Con un assembly .NET, il problema è anche peggiore, perché un assembly potrebbe non essere un programma completo. Quindi, il numero di casi in cui il compilatore potrebbe ragionevolmente dimostrare che derived non contiene un riferimento a un oggetto più derivato (o un riferimento null) sarebbe piccolo.

Quanto costerebbe aggiungere questa logica in modo che possa emettere un'istruzione call anziché callvirt? Senza dubbio, il costo sarebbe molto più alto del piccolo vantaggio derivato.

+0

Ha senso per me :) Grazie per la risposta. – Tarik

+0

Ciò significa, in altre parole, che ciò che sta realmente vedendo è il polimorfismo che funziona? L'IL viene gestito in questo modo in modo che in fase di esecuzione venga chiamato il metodo corretto sottoposto a override? –

+0

@BradRem è esattamente ciò che significa. – phoog

9

Il modo di pensare a questo è che i metodi virtuali definiscono uno "spazio" in cui inserire un metodo in fase di esecuzione. Quando emettiamo un'istruzione callvirt diciamo "al momento dell'esecuzione, guarda per vedere cosa c'è in questo slot e invocalo".

Lo slot è identificato dalle informazioni metodo sul tipo che dichiarato il metodo virtuale, non il tipo che sostituzioni esso.

Sarebbe perfettamente legale emettere un callvirt al metodo derivato; il runtime si renderà conto che il metodo derivato è lo stesso slot del metodo base e il risultato sarebbe esattamente lo stesso. Ma non c'è mai uno motivo per farlo. È più chiaro se identifichiamo lo slot identificando il tipo che dichiara tale slot.

1

Si noti che questo accade anche se si dichiara DerivedClass come sealed.

C# utilizza l'operatore callvirt per chiamare qualsiasi metodo di istanza (virtual o meno) per ottenere automaticamente un controllo nullo sul riferimento all'oggetto - per sollevare un NullReferenceException al punto che un metodo viene chiamato. In caso contrario, il NullReferenceException verrà generato solo al primo utilizzo effettivo di qualsiasi membro di istanza della classe all'interno del metodo, il che può essere sorprendente. Se non si utilizza alcun membro dell'istanza, il metodo potrebbe effettivamente completare correttamente senza mai sollevare l'eccezione.

Si deve anche ricordare che IL non viene eseguito direttamente. Viene prima compilato con le istruzioni native del compilatore JIT e questo esegue una serie di ottimizzazioni a seconda che si stia eseguendo il debug del processo. Ho trovato che il JIT x86 per CLR 2.0 ha delineato un metodo non virtuale ma ha chiamato il metodo virtuale - è anche inlineato Console.WriteLine!

Problemi correlati