2009-04-15 10 views
9

L'argomento di come il meccanismo C# virtuale e di override funziona internamente è stato discusso a morte tra i programmatori ... ma dopo mezz'ora su google, non riesco a trovare una risposta alla seguente domanda (vedi sotto):Internal Workings of C# Virtual and Override

utilizzando un semplice codice:

public class BaseClass 
{ 
    public virtual SayNo() { return "NO!!!"; } 
} 

public class SecondClass: BaseClass 
{ 
    public override SayNo() { return "No."; } 
} 

public class ThirdClass: SecondClass 
{ 
    public override SayNo() { return "No..."; } 
} 

class Program 
{ 
    static void Main() 
    { 
    ThirdClass thirdclass = new ThirdClass(); 
    string a = thirdclass.SayNo(); // this would return "No..." 

    // Question: 
    // Is there a way, not using the "new" keyword and/or the "hide" 
    // mechansim (i.e. not modifying the 3 classes above), can we somehow return 
    // a string from the SecondClass or even the BaseClass only using the 
    // variable "third"? 

    // I know the lines below won't get me to "NO!!!" 
    BaseClass bc = (BaseClass)thirdclass; 
    string b = bc.SayNo(); // this gives me "No..." but how to I get to "NO!!!"? 
    } 
} 

credo non posso ottenere i metodi della classe base o classe derivata intermedia semplicemente utilizzando l'esempio più derivata (senza modifica delle firme dei metodi delle 3 classi). Ma vorrei confermare e cementare la mia comprensione ...

Grazie.

+0

fatto che si intende per una delle classi di ereditare da BasClass (dire secondClass)? –

+0

Non del tutto; non più classi da aggiungere o modificare ... – henry000

risposta

1

Non è possibile accedere ai metodi di base di un override. Indipendentemente dal modo in cui si esegue il cast dell'oggetto, viene sempre utilizzato l'ultimo override dell'istanza.

+0

Questo non è completamente vero. Puoi sempre usare "base" come in risposta Jareds per chiamare un metodo sottoposto a override nella classe base, in questo caso SecondClass. – Pete

+1

No, non puoi farlo se sei fuori dall'oggetto e questo è ciò che l'autore della domanda ha chiesto. Ovviamente è possibile aggirarlo usando la chiamata IL invece di callvirt, ma C# è speciale che non emette mai chiamate, tranne che per i metodi statici. – grover

7

Senza alcuna modifica al campione e alla riflessione di sconto, non è possibile. L'intento del sistema virtuale è quello di far valere la chiamata del derivato più non importa cosa e il CLR è bravo nel suo lavoro.

Ci sono un paio di modi per aggirare questo problema.

Opzione 1: Si potrebbe aggiungere il seguente metodo per thirdClass

public void SayNoBase() { 
    base.SayNo(); 
} 

Questo costringerebbe l'invocazione di SecondClass.SayNo

Opzione 2: Il problema principale è che si vuole invocare un virtuale metodo non virtualmente. C# fornisce solo un modo per farlo tramite il modificatore di base. Ciò rende impossibile chiamare un metodo all'interno della tua classe in modo non virtuale. Puoi risolvere questo problema definendolo in un secondo metodo e in proxy.

public overrides void SayNo() { 
    SayNoHelper(); 
} 

public void SayNoHelper() { 
    Console.WriteLine("No"); 
} 
+0

Inoltre, se si dispone della classe pubblica ThirdClass: BaseClass { base.SayNo(); } Quello restituirebbe NO !!! – Pete

2

Certo ...

BaseClass bc = new BaseClass(); 
    string b = bc.SayNo(); 

"virtuale" significa che l'attuazione che sarà eseguito è basato sul tipo effettivo dell'oggetto sottostante, non il tipo di variabile è farcito in ... Quindi se l'oggetto reale è una terza classe, questa è l'implementazione che otterrai, indipendentemente da ciò che hai lanciato. Se vuoi il comportamento che descrivi sopra, non rendere i metodi virtuali ...

Se ti stai chiedendo "qual è il punto?" è per "polimorfismo"; in modo che tu possa dichiarare una collezione, o un parametro di metodo, come un tipo di base, e includerlo/passarlo un mix di tipi derivati, e comunque quando, all'interno del codice, anche se ogni oggetto è assegnato a una variabile ref dichiarata come tipo di base, per ognuno, l'effettiva implementazione che verrà eseguita per qualsiasi chiamata di metodo virtuale sarà quella implementata definita nella definizione di classe per il momento effettivo di ogni oggetto ...

0

Se supportato con un campo si potrebbe estrai il campo usando la riflessione.

Anche se si tira fuori il MethodInfo utilizzando la riflessione da typeof (BaseClass) sarà ancora finire per eseguire il vostro metodo sovrascritto

14

C# non può fare questo, ma è effettivamente possibile in IL utilizzando call invece di callvirt. È quindi possibile aggirare la limitazione di C# utilizzando Reflection.Emit in combinazione con uno DynamicMethod.

Ecco un esempio molto semplice per illustrare come funziona. Se vuoi davvero usarlo, avvolgilo in una bella funzione e cerca di farlo funzionare con diversi tipi di delegati.

delegate string SayNoDelegate(BaseClass instance); 

static void Main() { 
    BaseClass target = new SecondClass(); 

    var method_args = new Type[] { typeof(BaseClass) }; 
    var pull = new DynamicMethod("pull", typeof(string), method_args); 
    var method = typeof(BaseClass).GetMethod("SayNo", new Type[] {}); 
    var ilgen = pull.GetILGenerator(); 
    ilgen.Emit(OpCodes.Ldarg_0); 
    ilgen.EmitCall(OpCodes.Call, method, null); 
    ilgen.Emit(OpCodes.Ret); 

    var call = (SayNoDelegate)pull.CreateDelegate(typeof(SayNoDelegate)); 
    Console.WriteLine("callvirt, in C#: {0}", target.SayNo()); 
    Console.WriteLine("call, in IL: {0}", call(target)); 
} 

Stampe:

callvirt, in C#: No. 
call, in IL: NO!!! 
+1

Ho letto solo le prime pagine di CLR tramite C#, ma risposte come questa mi fanno venire voglia di prendermi il giorno libero e finirlo! – overslacked

+0

@overslacked, anch'io. voglio così tanto tempo per finire il libro: CLR via C#. – Attilah

2

Uso base in C# funziona solo per la base immediata. Non è possibile accedere a un membro base-base.

Sembra che qualcun altro mi abbia picchiato al punzone con la risposta riguardo a ciò che è possibile fare in IL.

Tuttavia, penso che il modo in cui ho creato il codice gen abbia alcuni vantaggi, quindi lo posterò comunque.

La cosa che ho fatto in modo diverso è utilizzare gli alberi di espressione, che consentono di utilizzare il compilatore C# per eseguire la risoluzione di sovraccarico e la sostituzione di argomenti generici.

Quella roba è complicata, e non si vuole dover replicare se stessi se si può aiutare. Nel tuo caso, il codice dovrebbe funzionare in questo modo:

var del = 
    CreateNonVirtualCall<Program, BaseClass, Action<ThirdClass>> 
    (
     x=>x.SayNo() 
    ); 

si sarebbe probabilmente desidera memorizzare il delegato in un campo statico di sola lettura, in modo da avere solo compilare una volta.

è necessario specificare 3 argomenti generici:

  1. Il tipo proprietario - Questa è la classe che avrebbe invocato il codice se non stava utilizzando "CreateNonVirtualCall".

  2. La classe di base - Questa è la classe che si desidera effettuare la chiamata non virtuale da

  3. Un tipo delegato. Questo dovrebbe rappresentare la firma del metodo che viene chiamato con un parametro extra per l'argomento "this". È possibile eliminarlo, ma richiede più lavoro nel metodo del codice gen.

Il metodo accetta un singolo argomento, un lambda che rappresenta la chiamata. Deve essere una chiamata e solo una chiamata. Se vuoi estendere il codice gen puoi supportare cose più complesse.

Per semplicità, il corpo lambda è limitato alla sola possibilità di accedere ai parametri lambda e può solo passarli direttamente alla funzione. È possibile rimuovere questa restrizione se si estende il codice gen nel corpo del metodo per supportare tutti i tipi di espressioni. Ci vorrebbe un po 'di lavoro però. Puoi fare tutto ciò che vuoi con il delegato che ritorna, quindi la restrizione non è un grosso problema.

È importante notare che questo codice non è perfetto. Potrebbe usare molto più validazione, e non funziona con i parametri "ref" o "out" a causa delle limitazioni dell'albero dell'espressione.

L'ho testato in casi di esempio con metodi void, metodi che restituiscono valori e metodi generici e ha funzionato. Sono sicuro, tuttavia, puoi trovare alcuni casi limite che non funzionano.

In ogni caso, ecco il codice IL Gen:

public static TDelegate CreateNonVirtCall<TOwner, TBase, TDelegate>(Expression<TDelegate> call) where TDelegate : class 
{ 
    if (! typeof(Delegate).IsAssignableFrom(typeof(TDelegate))) 
    { 
     throw new InvalidOperationException("TDelegate must be a delegate type."); 
    } 

    var body = call.Body as MethodCallExpression; 

    if (body.NodeType != ExpressionType.Call || body == null) 
    { 
     throw new ArgumentException("Expected a call expression", "call"); 
    } 

    foreach (var arg in body.Arguments) 
    { 
     if (arg.NodeType != ExpressionType.Parameter) 
     { 
      //to support non lambda parameter arguments, you need to add support for compiling all expression types. 
      throw new ArgumentException("Expected a constant or parameter argument", "call"); 
     } 
    } 

    if (body.Object != null && body.Object.NodeType != ExpressionType.Parameter) 
    { 
     //to support a non constant base, you have to implement support for compiling all expression types. 
     throw new ArgumentException("Expected a constant base expression", "call"); 
    } 

    var paramMap = new Dictionary<string, int>(); 
    int index = 0; 

    foreach (var item in call.Parameters) 
    { 
     paramMap.Add(item.Name, index++); 
    } 

    Type[] parameterTypes; 


    parameterTypes = call.Parameters.Select(p => p.Type).ToArray(); 

    var m = 
     new DynamicMethod 
     (
      "$something_unique", 
      body.Type, 
      parameterTypes, 
      typeof(TOwner) 
     ); 

    var builder = m.GetILGenerator(); 
    var callTarget = body.Method; 

    if (body.Object != null) 
    { 
     var paramIndex = paramMap[((ParameterExpression)body.Object).Name]; 
     builder.Emit(OpCodes.Ldarg, paramIndex); 
    } 

    foreach (var item in body.Arguments) 
    { 
     var param = (ParameterExpression)item; 

     builder.Emit(OpCodes.Ldarg, paramMap[param.Name]); 
    } 

    builder.EmitCall(OpCodes.Call, FindBaseMethod(typeof(TBase), callTarget), null); 

    if (body.Type != typeof(void)) 
    { 
     builder.Emit(OpCodes.Ret); 
    } 

    var obj = (object) m.CreateDelegate(typeof (TDelegate)); 
    return obj as TDelegate; 
} 
+1

Fantastico lavoro. Sicuramente vale la pena pubblicarlo! –

+0

Grazie. (Argh! lunghezza minima del commento) –