Accueil > Tutoriels > Le Masquage en C#

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.

 

  1. Héritage et substitution
  2. Masquage
  3. Masquage et interfaces
  4. Masquage, modificateurs d’accès et méthodes virtuelles
  5. Conclusion

 

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 :

public override new 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 :
protected public 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 new string int 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 :

 

Categories: Tutoriels Tags: , ,