Le Masquage en C#
Le terme « masquage » (hiding en anglais) désigne une technique assez peu connue en C# permettant de masquer ou d’occulter un membre d’une classe de base. Globalement, le principe est très semblable à celui de la substitution (overriding), mais son utilisation répond à des cas précis, et son comportement final réserve parfois quelques surprises que nous allons essayer d’aborder dans cet article.
Pour commencer, je n’ai pas pu résister à l’envie de citer l’ancienne définition du masquage donnée par Microsoft (VS 2003) sur MSDN :
La portée d’une entité englobe généralement plus de texte de programme que l’espace de déclaration de l’entité. Plus précisément, la portée d’une entité peut inclure des déclarations qui introduisent de nouveaux espaces de déclaration contenant des entités du même nom. La conséquence de ces déclarations est que l’entité originale devient masquée. À l’inverse, une entité est dite visible lorsqu’elle n’est pas masquée.
Le masque de nom se produit lorsque les portées se chevauchent par imbrication et lorsque les portées se chevauchent par héritage.
Hum… Cela me rappelle à quel point il était difficile, à l’époque de Visual Studio 2002-2003, d’appréhender la nouvelle architecture .NET. Il fallait souvent une grande concentration (et quelques aspirines) pour arriver à comprendre certains termes comme « code managé », « solution », etc. en se basant uniquement sur la documentation MSDN. Heureusement, les pages concernant les nouvelles versions sont bien plus faciles d’accès, mais ce ne sont pas forcément celles-ci que l’on trouve en premier.
Pour faire simple, le masquage permet de redéfinir une méthode de la classe de base, même lorsque celle-ci n’est pas déclarée comme étant virtuelle ( « substituable »). Cependant le comportement de cette nouvelle méthode ne sera pas tout à fait identique à celui obtenu à l’aide d’une substitution classique. Pour montrer ceci, on va donc commencer par quelques rappels sur le polymorphisme.
Héritage et substitution
Prenons une classe de base nommée WebSite
contenant deux fonctions : GetDefinition()
permettant d’obtenir une information générale, et GetURL()
retournant l’adresse locale par défaut. La méthode GetDefinition()
est déclarée avec le mot-clé virtual
(Overridable
en vb.net) afin d’offrir la possibilité aux classes dérivées de définir leur propre implémentation de la méthode :
public class WebSite { protected string name; // Constructor public WebSite(string webSiteName) { name = webSiteName; } // Returns the website's definition (overridable) public virtual string GetDefinition() { return "A WebSite"; } // Returns the website's URL (not overridable) public string GetURL() { return String.Format("http://www.{0}.com", name); } }
On crée ensuite une nouvelle classe Blog
dérivant de WebSite
afin d’y ajouter une information propre à ce type de site : le moteur de blog (WordPress par exemple). On substitue la méthode GetDefinition()
à l’aide du mot-clé override
afin d’y ajouter cette donnée supplémentaire :
public class Blog : WebSite { protected string engine; // Constructor public Blog(string webSiteName, string blogEngine) : base(webSiteName) { engine = blogEngine; } // Returns the website's definition public override string GetDefinition() { return "A blog powered by " + engine; } }
A noter que le constructeur de la classe Blog
appelle celui de la classe de base pour initialiser la variable webSiteName
(d’accès protected
ici pour éviter d’allonger le code de l’exemple avec des accesseurs).
On peut ensuite utiliser le code suivant pour tester cette classe :
List<WebSite> webSitesList = new List<WebSite>(); webSitesList.Add(new WebSite("google")); webSitesList.Add(new Blog("pausedotnet", "WordPress")); foreach (WebSite webSite in webSitesList) Console.WriteLine("{0} | {1} : URL = {2}", webSite.GetType().Name, webSite.GetDefinition(), webSite.GetURL());
On obtient alors la sortie suivante :
WebSite | A WebSite : URL = http://www.google.com Blog | A blog powered by WordPress : URL = http://www.pausedotnet.com
Malgré l’utilisation de l’objet WebSite
dans l’instruction foreach
du code de test, on voit bien que c’est la méthode substituée GetDefinition()
de la classe Blog
qui a été appelée ici, tandis que la méthode GetURL()
de la classe de base a été appelée dans les deux cas.
Masquage
La substitution nous a permis de personnaliser la méthode GetDefinition()
pour notre classe Blog
, dérivée de la classe WebSite
. Supposons maintenant que l’on souhaite obtenir une URL se terminant par « .fr » au lieu de « .com » et sans le « http:// ». Il faut donc substituer la méthode GetURL()
en ajoutant le code ci-dessous à la classe Blog
:
public override string GetURL() { return String.Format("www.{0}.fr", name); }
Cependant le compilateur nous refuse ce nouveau code, car la méthode GetURL()
n’a pas été déclarée avec le mot-clé virtual
dans la classe de base. Elle n’est donc pas substituable.
Pour contourner ce problème, nous allons donc masquer la méthode de base au lieu de la substituer. A la place du mot-clé override
, nous allons utiliser le mot-clé new
(Shadows
en vb.net) dans notre classe dérivée :
publicoverridenew string GetURL() { return String.Format("www.{0}.fr", name); }
Attention cependant : le mot-clé new
n’est pas obligatoire. Si la classe dérivée contient une méthode avec la même signature qu’une des méthodes de la classe de base, celle-ci masquera par défaut la méthode de base. Le compilateur générera alors une alerte conseillant de spécifier le mot-clé new
afin d’éviter de masquer par erreur une méthode existante.
On teste ensuite rapidement ce nouveau code à l’aide de quelques lignes :
Blog myBlog = new Blog("pausedotnet", "WordPress"); Console.WriteLine("Test avec masquage : " + myBlog.GetURL());
Ce qui nous donne :
Test avec masquage : www.pausedotnet.fr
Tout fonctionne correctement, notre nouvelle adresse en « .fr » a bien remplacé celle de la classe de base en « .com ». Voyons maintenant le résultat si l’on utilise le bloc foreach
du test précédent :
WebSite | A standard WebSite : URL = http://www.google.com Blog | A blog powered by WordPress : URL = http://www.pausedotnet.com
Ça ne fonctionne plus ! Cette fois notre URL est affichée d’après le formatage effectuée par la classe de base et ne tient absolument pas compte de celui de notre classe dérivée. Il ne faut pas réfléchir très longtemps pour constater que la différence par rapport à l’exemple précédent vient du fait que dans cet exemple on effectue un cast en WebSite
de nos objets de la liste pour pouvoir les utiliser dans une instruction foreach
. Notre objet Blog
sera donc traité comme un objet WebSite
, ce qui donne un résultat très différent selon la technique utilisée pour remplacer notre méthode de base, c’est-à-dire le masquage ou la substitution.
On voit bien ici la différence entre le masquage et la substitution : La substitution remplace la méthode de la classe de base, le nouveau code est donc correctement exécuté même si l’on cast notre classe Blog
en WebSite
. Le masquage quant à lui ne fait qu’occulter la méthode de base en interposant un masque entre la classe dérivée et la classe de base. Si l’on cast la classe Blog
en WebSite
, les méthodes ne se chevauchent plus puisque la classe dérivée n’est plus visible et le masque ne peut plus s’interposer. La méthode de base est donc utilisée dans ce cas.
Le masquage peut donc être utilisé pour résoudre certains problèmes, mais il faut faire particulièrement attention à la façon dont la classe dérivée peut être utilisée dans le reste du code.
Masquage et interfaces
Imaginons maintenant que la méthode GetDefinition()
ne soit plus proposée directement dans la classe de base WebSite
mais dans les spécifications d’une interface nommée IWebURL
:
public interface IWebURL { string GetURL(); }
On modifie alors la déclaration de notre classe WebSite
pour implémenter cette nouvelle interface :
public class WebSite : IWebURL { ...
La classe WebSite
implémente déjà la méthode GetURL()
, il n’est donc pas nécessaire de la rajouter. Jusqu’ici cela ne change rien de spécial, comme on peut le voir avec ce code de test :
Blog myBlog = new Blog("pausedotnet", "WordPress"); Console.WriteLine("Blog | {0} - URL = {1}", myBlog.GetDefinition(), myBlog.GetURL()); WebSite mySite = myBlog; Console.WriteLine("Blog as WebSite | {0} - URL = {1}", mySite.GetDefinition(), mySite.GetURL());
Résultat :
Blog | A blog powered by WordPress : URL = www.pausedotnet.fr Blog as WebSite | A blog powered by WordPress : URL = http://www.pausedotnet.com
Le masquage fonctionne toujours uniquement dans le cas où l’on utilise la classe dérivée, et non la classe de base. Cependant l’implémentation de notre interface IWebURL
par la classe de base nous permet d’écrire un troisième cas:
IWebURL webURL = myBlog; Console.WriteLine("Blog as IWebURL | URL = {0}", webURL.GetURL());
Résultat :
Blog as IWebURL | URL = http://www.pausedotnet.com
Toujours pas de différence malheureusement. Le masquage est encore sans effet. Il nous reste une dernière modification à tester : notre classe Blog
masquant la méthode GetURL()
avec sa propre interprétation, on peut donc considérer qu’elle implémente elle aussi l’interface IWebURL
. Dans ce cas, on va ajouter cette interface directement à la déclaration de notre classe dérivée, puis relancer notre dernier test :
public class Blog : WebSite, IWebURL { ...
Résultat :
Blog as IWebURL | URL = www.pausedotnet.fr
Victoire ! Le masquage fonctionne ici, même avec un cast de notre classe dérivée. Cependant il est probable que l’explication de ce comportement vienne du fait que le programme ne traite pas réellement un masquage ici mais simplement l’implémentation de l’interface IWebURL
par la classe Blog
. Le mot-clé new
demandé par le compilateur étant toutefois nécessaire en raison de la présence d’une méthode de même signature dans la classe de base.
Cet exemple peut paraître un peu extrême, mais il faut savoir qu’au sein du .NET Framework, de nombreuses classes implémentent une ou plusieurs interfaces. Il peut être intéressant dans ce cas d’exposer un membre masqué par l’intermédiaire d’une interface au lieu de fournir directement la classe dérivée, pour éviter les appels involontaires aux membres de la classe de base. Par exemple :
private Blog myBlog; public IWebURL BlogHiddenMember { get { return myBlog as IWebURL; } }
Masquage, modificateurs d’accès et méthodes virtuelles
Pour finir, voici quelques exemples un peu plus exotiques du masquage. Pour chacun des cas étudiés, on utilisera le code de test ci-dessous :
Blog myBlog = new Blog("pausedotnet", "WordPress"); WebSite mySite = myBlog; Console.WriteLine("Blog : " + myBlog.GetURL()); Console.WriteLine("WebSite : " + mySite.GetURL());
Dans notre classe Blog
, on va remplacer le modificateur d’accès public
de notre méthode GetURL()
par private
:
private new string GetURL() { return String.Format("www.{0}.fr", name); }
Résultats :
Blog : http://www.pausedotnet.com WebSite : http://www.pausedotnet.com
Contrairement à l’exemple présenté dans le chapitre précédent (qui utilisait un code de test similaire), le masquage n’a pas fonctionné ici, même en utilisant l’objet Blog
. L’explication vient du fait que le masque n’avait pas une portée suffisante pour être visible depuis le code de test. Si l’on change à nouveau le modificateur d’accès pour internal
par exemple, le masquage devient visible à l’intérieur de l’assembly et le résultat sera alors celui attendu (si le test est effectué dans la même assembly). Ce type de modification ne permet donc pas de rendre invisible une méthode de la classe de base. Par contre il est possible d’étendre (dans une certaine limite) la portée d’un membre d’un classe de base :
// Code dans la classe de base WebSite : protected string GetURL() { return String.Format("http://www.{0}.com", name); } // Code dans la classe dérivée Blog :protectedpublic new string GetURL() { return String.Format("www.{0}.fr", name); }
Visual Studio refusera ici de compiler le code de test car la méthode GetURL()
n’est pas accessible depuis un objet WebSite
. Il faudra donc commenter la ligne concernée pour vérifier les résultats. A noter que cela fonctionne uniquement avec les membres de la classe de base visibles depuis la classe dérivée. Impossible de rendre publique un membre privé par exemple, heureusement.
Il est aussi possible aussi de changer le type de retour, et de rendre la méthode virtuelle :
// Code dans la classe dérivée Blog : public virtual newstringint GetURL() { return name.Length; }
Résultats :
Blog : 11 WebSite : http://www.pausedotnet.com
Je vous laisse imaginer les conséquences de la substitution (ou encore de l’occulation) d’un membre « virtualisé par masquage ». Le résultat suivant les différents cast de l’objet sera encore plus difficile à anticiper.
Conclusion
Le masquage est une technique offrant de nombreuses possibilités. Cependant elle est difficile à manipuler correctement et peut donner des résultats inattendus. Elle doit donc être utilisée essentiellement pour corriger des problèmes qui ne peuvent se régler par une substitution classique ou une modification du code de la classe de base.
A lire aussi sur MSDN :
Un rappel général sur l’héritage et tous les termes associés :
Et sur le masquage en particulier :
- Versioning avec les mots clés override et new (Guide de programmation C#)
- Savoir quand utiliser les mots clés override et new (Guide de programmation C#)