2009-04-15 7 views
9

Le sujet du fonctionnement interne du mécanisme virtuel et override de C# a été discuté à fond parmi les programmeurs ... mais après une demi-heure sur google, je ne trouve pas de réponse à la question suivante (voir ci-dessous):Fonctionnement interne de C# Virtual et Override

en utilisant un code simple:

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!!!"? 
    } 
} 

Je pense que je ne peux pas les méthodes de la classe de base ou la classe dérivée intermédiaire en utilisant simplement l'instance la plus dérivée (sans modifier les signatures de méthodes des 3 classes). Mais je voudrais confirmer et cimenter ma compréhension ...

Merci.

+0

avez-vous l'intention d'un de vos classes pour hériter de BasClass (disons deuxième classe)? –

+0

Pas tout à fait; plus de classes à ajouter ou à modifier ... – henry000

Répondre

1

Vous ne pouvez pas accéder aux méthodes de base d'un remplacement. Peu importe la manière dont vous avez casté l'objet, le dernier remplacement de l'instance est toujours utilisé.

+0

Ce n'est pas tout à fait vrai. Vous pouvez toujours utiliser "base" comme dans la réponse Jareds pour appeler une méthode surchargée dans la classe de base, dans ce cas SecondClass. – Pete

+1

Non, vous ne pouvez pas si vous êtes en dehors de l'objet et c'est ce que l'auteur de la question a demandé. Bien sûr, vous pouvez le contourner en utilisant l'appel IL au lieu de callvirt, mais C# est spécial qu'il n'émet jamais d'appel sauf pour les méthodes statiques. – grover

7

Sans modification de votre échantillon et réflexion réfléchie, non, il n'y a aucun moyen. L'intention du système virtuel est d'imposer l'appel le plus dérivé n'importe quoi et le CLR est bon à son travail.

Il y a plusieurs façons de contourner ce problème.

Option 1: Vous pouvez ajouter la méthode suivante pour troisième classe

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

Cela forcerait l'invocation de SecondClass.SayNo

Option 2: Le principal problème ici est que vous voulez invoquer un virtuel méthode non-virtuelle. C# fournit seulement une façon de le faire via le modificateur de base. Cela rend impossible d'appeler une méthode dans votre propre classe de manière non virtuelle. Vous pouvez résoudre ce problème en l'intégrant dans une seconde méthode et en l'utilisant comme proxy.

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

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

En outre, si vous aviez classe publique ThirdClass: BaseClass { base.SayNo(); } Cela reviendrait NO !!! – Pete

2

... Bien sûr

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

« virtuel » signifie que la mise en œuvre qui sera exécutée est basée sur le type RÉEL de l'objet sous-jacent, et non le type de la variable est bourré dedans ... Donc si l'objet réel est une ThirdClass, c'est l'implémentation que vous obtiendrez, peu importe ce que vous avez lancé. Si vous voulez le comportement que vous décrivez ci-dessus, ne faites pas les méthodes virtuelles ...

Si vous vous demandez "quel est le point?" c'est pour le 'polymorphisme'; de sorte que vous pouvez déclarer une collection, ou un paramètre de méthode, comme un type de base, et l'inclure/passer un mélange de types dérivés, et pourtant, dans le code, même si chaque objet est assigné à une variable ref déclarée comme type de base, pour chacun, l'implémentation réelle qui sera exécutée pour tout appel de méthode virtuelle sera cette implémentation définie dans la définition de classe pour le tyoe ACTUAL de chaque objet ...

0

Si son support avec un champ vous pouvez tirez le champ en utilisant la réflexion.

Même si vous tirez au large de la MethodInfo utilisant la réflexion de typeof (BaseClass) vous encore jusqu'à la fin l'exécution de votre méthode surchargée

14

C# ne peut pas le faire, mais il est réellement possible en IL en utilisant call au lieu de callvirt. Vous pouvez donc contourner la limitation de C# en utilisant Reflection.Emit en combinaison avec un DynamicMethod.

Voici un exemple très simple pour illustrer comment cela fonctionne. Si vous avez vraiment l'intention d'utiliser ceci, enveloppez-le dans une fonction agréable, efforcez-vous de le faire fonctionner avec différents types de délégués.

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

Prints:

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

J'ai seulement lu les premières pages de CLR via C#, mais des réponses comme celles-ci me donnent envie de prendre congé et de finir! – overslacked

+0

@overslacked, moi aussi. Je veux tellement avoir le temps de finir le livre: CLR via C#. – Attilah

2

Utilisation base en C# ne fonctionne que pour la base immédiate. Vous ne pouvez pas accéder à un membre de base.

Il semble que quelqu'un d'autre m'a battu au punch avec la réponse à propos de ce qu'il est possible de faire en IL.

Cependant, je pense que la façon dont j'ai fait le code gen a quelques avantages, donc je vais le poster de toute façon. La chose que j'ai faite différemment est d'utiliser des arbres d'expression, ce qui vous permet d'utiliser le compilateur C# pour effectuer une résolution de surcharge et une substitution d'arguments génériques.

Ce truc est compliqué, et vous ne voulez pas avoir à le reproduire vous-même si vous pouvez l'aider. Dans votre cas, le code fonctionnerait comme ceci:

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

Vous auriez probablement eu envie de stocker le délégué dans un champ statique en lecture seule, de sorte que vous suffit de compiler une fois.

Vous devez spécifier 3 arguments génériques:

  1. Le type de propriétaire - C'est la classe que vous auriez invoqué le code de si vous n'utilisez pas « CreateNonVirtualCall ».

  2. La classe de base - C'est la classe que vous voulez faire l'appel non virtuel à partir

  3. Un type délégué. Cela devrait représenter la signature de la méthode appelée avec un paramètre supplémentaire pour l'argument "this". Il est possible d'éliminer cela, mais cela nécessite plus de travail dans la méthode de génération de code.

La méthode prend un seul argument, un lambda représentant l'appel. Ce doit être un appel, et seulement un appel. Si vous voulez étendre le code gen, vous pouvez supporter des choses plus complexes. Pour simplifier, le corps lambda est limité à la seule possibilité d'accéder aux paramètres lambda, et ne peut les transmettre directement à la fonction. Vous pouvez supprimer cette restriction si vous étendez le code gen dans le corps de la méthode pour prendre en charge tous les types d'expression. Cela prendrait du travail cependant. Vous pouvez faire ce que vous voulez avec le délégué qui revient, donc la restriction n'est pas trop importante.

Il est important de noter que ce code n'est pas parfait. Il pourrait utiliser beaucoup plus de validation, et cela ne fonctionne pas avec les paramètres "ref" ou "out" à cause des limitations de l'arbre d'expression.

Je l'ai testé dans des exemples de cas avec des méthodes void, des méthodes renvoyant des valeurs et des méthodes génériques, et cela a fonctionné. Je suis sûr, cependant, vous pouvez trouver quelques cas de bord qui ne fonctionnent pas.

Dans tous les cas, voici le code 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

Cool pièce de travail. Vaut vraiment la peine de le poster! –

+0

Merci. (argh! longueur minimale du commentaire) –