Méthode virtuelle dans le constructeur. L'erreur trop ignorée...

Une couche de rappel ... "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 ?

Ça me dit quelque chose ....

Peut-être en effet. C'est même fort possible. Car de temps en temps j'aime sortir du fin fond de l'oubli d'Internet et surtout de Dot.Blog qui comptent plus de 1300 articles certains papiers anciens mais dont le sujet est toujours d'actualité. Et c'est le cas pour le papier d'aujourd'hui ! Alors si ça semble vous dire quelque chose, c'est normal, c'est une re-publication. D'ailleurs il se peut que des détails aient changé mais l'essentiel, ce qui justifie l'article, reste vrai !

Une erreur qui passe Ă  travers les mailles

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... D'ailleurs c'est un sujet que j'ai dĂ©jĂ  abordĂ© et voyant que les choses n'ont pas changĂ© depuis des annĂ©es je me suis dit qu'en mettre une nouvelle couche ne serait pas un mal. Trop de gens ignorent encore cette "erreur" qui n'en ai pas une Ă  la compilation mais qui a des consĂ©quences graves.

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 aggraver 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 - Loi de 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 enfoncer le clou par quelques lignes de code (qu'il est mĂ©chant !)

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 trop ignoré, une spécificité de C# qu'on ne retrouve pas ailleurs notamment pas en C++. Mais dont les conséquences sont terribles...

Un sujet parfait pour cette fin de (premier ?) confinement pour vous forcer Ă  revoir votre code sous un autre angle et traquer cette erreur sournoise (surtout sans l'aide de Resharper). 

Le projet VS pour les fĂ©nĂ©ants : VirtualInit.rar (5,41 kb)

Stay Tuned !