Fonctionnement interne de C # Virtual and Override

Le sujet du fonctionnement interne des mécanismes virtuels et d’annulation C # a été discuté à mort entre les programmeurs … mais après une demi-heure sur Google, je ne trouve pas de réponse à la question suivante (voir ci-dessous):

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(); ssortingng a = thirdclass.SayNo(); // this would return "No..." // Question: // Is there a way, not using the "new" keyword and/or the "hide" // mechansim (ie not modifying the 3 classes above), can we somehow return // a ssortingng 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; ssortingng b = bc.SayNo(); // this gives me "No..." but how to I get to "NO!!!"? } } 

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

Merci.

C # ne peut pas faire cela, mais il est en fait possible dans IL d’utiliser 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 de l’utiliser, enveloppez-le dans une fonction conviviale et essayez de le faire fonctionner avec différents types de delegates.

 delegate ssortingng SayNoDelegate(BaseClass instance); static void Main() { BaseClass target = new SecondClass(); var method_args = new Type[] { typeof(BaseClass) }; var pull = new DynamicMethod("pull", typeof(ssortingng), 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)); } 

Impressions:

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

Sans modification de votre échantillon et réduction de la reflection, non, il n’y a pas moyen. L’intention du système virtuel est d’appliquer le plus souvent l’appel dérivé, et le CLR est performant.

Vous pouvez toutefois contourner ce problème de plusieurs manières.

Option 1: vous pouvez append la méthode suivante à ThirdClass

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

Cela forcerait l’invocation de SecondClass.SayNo

Option 2: Le problème principal ici est que vous souhaitez invoquer une méthode virtuelle de manière non virtuelle. C # ne fournit qu’un moyen de le faire via le modificateur de base. Cela rend impossible l’appel d’une méthode au sein de votre propre classe d’une manière non virtuelle. Vous pouvez résoudre ce problème en l’intégrant dans une seconde méthode et en utilisant un proxy.

 public overrides void SayNo() { SayNoHelper(); } public void SayNoHelper() { Console.WriteLine("No"); } 

Sûr…

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

“Virtuel” signifie que l’implémentation à exécuter est basée sur le type ACTUAL de l’object sous-jacent, et non sur le type de la variable dans laquelle il est bourré … Donc, si l’object réel est une ThirdClass, c’est l’implémentation que vous obtiendrez. , peu importe ce que vous lancez. Si vous voulez le comportement que vous décrivez ci-dessus, ne rendez pas les méthodes virtuelles …

Si vous vous demandez “à quoi ça sert?” c’est pour le ‘polymorphism’; de sorte que vous puissiez déclarer une collection ou un paramètre de méthode en tant que type de base et l’inclure / le transmettre à une combinaison de types dérivés, et pourtant, dans le code, même si chaque object est affecté à 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 celle définie dans la définition de classe pour le type ACTUAL de chaque object …

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

Il semble que quelqu’un d’autre m’a battu au poing avec la réponse sur le fait qu’il est possible de le faire en IL.

Cependant, je pense que la façon dont j’ai utilisé le code gen présente certains avantages, alors je le posterai quand même.

Ce que j’ai fait différemment est d’utiliser des arbres d’expression, qui vous permettent d’utiliser le compilateur C # pour résoudre les surcharges et remplacer les arguments génériques.

C’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> ( x=>x.SayNo() ); 

Vous voudrez probablement stocker le délégué dans un champ statique en lecture seule, afin de ne le comstackr qu’une seule fois.

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

  1. Le type de propriétaire – Il s’agit de la classe à partir de laquelle vous auriez invoqué le code si vous n’utilisiez pas “CreateNonVirtualCall”.

  2. La classe de base – Il s’agit de la classe à partir de laquelle vous souhaitez effectuer l’appel non virtuel.

  3. Un type de 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 code gen.

La méthode prend un seul argument, un lambda représentant l’appel. Ce doit être un appel, et seulement un appel. Si vous souhaitez étendre le code, vous pouvez prendre en charge des tâches plus complexes.

Pour simplifier, le corps lambda est limité à l’access aux parameters lambda et ne peut les transmettre que directement à la fonction. Vous pouvez supprimer cette ressortingction si vous étendez le code gen dans le corps de la méthode pour prendre en charge tous les types d’expression. Cela prendrait cependant du travail. Vous pouvez faire tout ce que vous voulez avec le délégué qui revient, de sorte que la ressortingction ne soit pas un gros problème.

Il est important de noter que ce code n’est pas parfait. Cela pourrait utiliser beaucoup plus de validation, et cela ne fonctionne pas avec les parameters “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, que vous pouvez trouver des cas extrêmes qui ne fonctionnent pas.

Dans tous les cas, voici le code IL Gen:

 public static TDelegate CreateNonVirtCall(Expression 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(); 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; } 

Vous ne pouvez pas accéder aux méthodes de base d’un remplacement. Quelle que soit la manière dont vous convertissez l’object, le dernier remplacement de l’instance est toujours utilisé.

Si elle est adossée à un champ, vous pouvez extraire le champ par reflection.

Même si vous extrayez la methodinfo en utilisant la reflection de typeof (BaseClass), vous finirez toujours par exécuter votre méthode surchargée.