Dot.Blog

C#, XAML, Xamarin, UWP/Android/iOS

Appel d'un membre virtuel dans le constructeur ou "quand C# devient vicieux". A lire absolument...

En maintenant un code C# d'un client mon ami Resharper me dit d'un appel à une méthode dans le constructeur d'une classe "virtual member call in constructor". J'ai tellement pris le pli avec ce problème que je ne m'en souci plus guère dans mon propre code, j'évite soigneusement la situation... Mais vous ? Avez-vous conscience de la gravité de ce problème ?

Sans Resharper il faut passer volontairement une analyse du code pour voir apparaître le message CA2214 "xxx contient une chaîne d'appel aboutissant à un appel vers une méthode virtuelle définie par la classe.". D'une part je doute fort que tout le monde comprenne du premier coup ce message ésotérique mais le pire c'est que je sais par expérience que la grande majorité des développeurs n'utilisent, hélas, que très rarement cette fonction... Et à la compilation du projet, aucune erreur, aucun avertissement ne sont indiqués !

Vous allez me dire "ça ne doit pas être bien grave si le compilateur ne dit rien et que seul un FxCop relève un simple avertissement". Je m'attendais à ce que vous me disiez cela... Et je vais vous prouver dans quelques lignes que cette remarque candide est la porte ouverte à de gros ennuis...

Le grave problème des appels aux méthodes virtuelles dans les constructeurs

Ce problème est "grave" à plus d'un titre. Tout d'abord techniquement, comme le code qui suit va vous le montrer, votre programme aura un comportement que vous n'avez pas prévu et qui mène à des bogues sournois. Cela est en soi suffisant pour qualifier le problème de "grave".
Ensuite, moins on a conscience d'un problème potentiel et plus il est grave, par nature. Comme très peu de développeurs ont conscience du fait que ce comportement bien particulier de C# est une source potentielle d'énormes problèmes, sa gravité augmente d'autant.
Pour terminer et agraver la situation, le compilateur ne dit rien et seule une analyse du code (ou l'utilisation d'un outil comme Resharper qui l'indique visuellement dans l'éditeur de code) peut permettre de prendre connaissance du problème.
La chaîne ne s'arrête pas là (tout ce qui peut aller mal ira encore pire - Murphy ), puisque même en passant l'analyseur de code le message sera noyé dans des dizaines, voire centaines d'avertissements et que, cerise sur le gateau, même si on prend la peine de lire l'avertissement, son intitulé est totalement nébuleux !

La preuve par le code

Maintenant que je vous ai bien alarmé, je vais enfoncé le clou par quelques lignes de code (qu'il est méchant Laughing) !

class Program
{
     static void Main(string[] args)
     {
        var derivé = new Derived();
     }
}

public class Base
{
   public Base()
   { Init(); }

   public virtual void Init()
  { Console.WriteLine("Base.Init"); }
}

public class Derived : Base
{
   private string s = "Non initialisée!";
   public Derived()
  { s = "variable initialisée"; }

  public override void Init()
 { Console.WriteLine("Derived.Init. var s = "+s); }
}

La question à deux eurocents est la suivante : Au lancement de la classe Program et de son Main, qu'est-ce qui va s'afficher à la console ?

La réponse est "Derived.Init. var s = Non initiliasée!".

L'action au ralenti avec panoramique 3D façon Matrix : Dans Main nous instancions la classe Derived. Cette classe est une spécialisation de la classe Base. Dans cette dernière il y a un constructeur qui appelle la méthode Init. Cette méthode est virtuelle et elle est surchargée dans la classe Derived.
Lorsque nous instancions Derived, de façon automatique le constructeur de Base se déclenche, ce qui provoque l'appel à Init. Donc à la version surchargée de Derived puisque C# appelle toujours la méthode dérivée la plus proche du type en cours.

D'où vient le problème ? ... Il vient du fait que le constructeur de Base, d'où provient l'appel à Init, n'est pas terminé (il le sera au retour de Init et une fois sa parenthèse de fin atteinte), du coup le constructeur de Derived n'a pas encore été appelé !

Si le code de Init ne repose sur aucune initialisation effectuée dans le constructeur de cette classe, tout va bien. Vous remarquerez d'ailleurs que le message affiché prend en compte la valeur de la variable s qui est initialisée dans sa déclaration et non pas une chaîne nulle. Ce qui prouve que les déclarations de variables initialisées sont, elles, bien exécutées, et avant le constructeur. Mais si le code de Init dépend de certaines initialisations effectuées dans le constructeur (initialisations simples comme dans l'exemple ci-dessus ou indirectes avec des appels de méthodes), alors là c'est la catastrophe : le constructeur de Derived n'a pas encore été appelé alors même que la version surchargée de Init dans Derived est exécutée par le constructeur de la classe mère !

La règle

Elle est simple : ne jamais appeler de méthodes virtuelles dans le constructeur d'une classe !

La règle CA2214 de l'analyseur de code :

"When a virtual method is called, the actual type that executes the method is not selected until run time. When a constructor calls a virtual method, it is possible that the constructor for the instance that invokes the method has not executed. "

"Quand une méthode virtuelle est appelée, le type actuel qui exécute la méthode n'est pas sélectionné jusqu'au runtime [ndt: c'est le principe des méthodes virtuelles, le "late binding"]. Quand un constructeur appelle une méthode virtuelle, il est possible que le constructeur de l'instance qui est invoquée n'ait pas encore été exécuté".

C'est "possible", c'est même pas sûr, donc il ne faut surtout pas écrire de code qui repose sur ce mécanisme...

L'aide de l'analyseur de code m'amuse beaucoup car dans sa section "How to fix violations" ("comment résoudre le problème"), il est dit tout simplement de ne jamais appeler de méthodes virtuelles dans les constructeurs... Avec ça débrouillez-vous !

La solution

Comme le dit laconiquement l'aide de l'analyseur : "faut pas le faire". Voilà la solution... En gros, si le cas se produit, comme dans notre exemple, la seule solution viable consiste à prendre le code de la méthode Init et à le déplacer dans le constructeur, il est fait pour ça... La méthode Init n'existe plus bien entendu, et elle est n'est donc plus surchargée dans la classe fille.

Conclusion

J'espère que ce petit billet vous aura aidé à prendre conscience d'un problème généralement méconnu, une spécificité de C# qu'on ne retrouve ni sous C++ ni sous Delphi.

Le projet VS2008 pour les fénéants : VirtualInit.rar (5,41 kb)

blog comments powered by Disqus