Dot.Blog

C#, XAML, WinUI, WPF, Android, MAUI, IoT, IA, ChatGPT, Prompt Engineering

Après les Weak References, les Weak Events !

Tous faibles pour une code plus fort ! Tel pourrait être la devise des Weak References et des Weak Events ! Mais qu’est-ce que les Weak Events ? Et à quoi cela sert-il ? Et comment les mettre en œuvre ? Plein de questions auxquelles je vais répondre…

La notion “Weak”

imageWeak, prononcer “ouik”, veut dire “faible”. A quoi est du cette “faiblesse” ?

En matière de code vous l’avez compris le dogme qui s’est imposé, non sans bonnes raisons, est celui du “decoupled” (découplé). Quand on dit découplé, on s’oppose à couplé, logique.

Le couplage consiste à lier deux choses entre elles.

Si j’ai deux objets a et b et que dans la définition de la classe A dont est issue l’instance a j’ai un code du type “private B instanceDeB = new B();” cela signifie que les instances a de A se lient à une instance de B.

Ce type de couplage peut être trivial comme cet exemple ou être plus indirect, le résultat étant le même, l’instance a de type A possède une référence sur une instance b de B.

Ce type de couplage est dit “fort”, non pour qualifier sa valeur intrinsèque mais juste pour signifie que le lien est fort.

Les environnements managés et les liens forts

La base même du principe des environnements managés comme .NET repose sur le fait que le développeur n’a plus à se préoccuper de la libération des objets (ce n’est pas aussi simple mais on considèrera cela comme vrai en première approximation).

Pour ce faire le code qui est exécuté est “surveillé” par une couche, le CLR dans .NET. Et lorsque cela est nécessaire un objet particulier de .NET fait le ménage, c’est le Garbage Collector. Je schématise beaucoup mais c’est l’essentiel du processus.

Pour faire son travail le GC utilise un “graphe des objets”, c’est à dire qu’il se fait une représentation mémoire des liens entre chaque objet pour supprimer ceux qui n’ont plus de liens avec les autres car dans ce cas cela signifie qu’ils n’ont plus d’utilité. Le raisonnement est simple (et faillit quelques fois mais c’est une autre histoire dont je parlerai un jour certainement).

Tous les liens forts permettent au GC de relier des objets entre eux dans son graphe. Ces objets là seront conservés (poussés de la zone dite génération 0 à celle appelée génération 1 puis pour les objets à très longue durée de vie dans la génération 2 qui est scrutée tellement moins souvent que souvent les objets même libérés y vivent jusqu’à l’arrêt de l’application et sont détruits sans appel à leur finalseur).

Cette façon de procéder a démontré sa supériorité sur les langages non managés, les informaticiens ayant gardé un très mauvais souvenir des bogues que des langages tels que C facilitent. Les pointeurs fous, les zones mémoire partiellement libérées, les débordements de buffers, etc, sont même à la base de la plupart des malwares, des attaques de sites web, sans parler des écrans bleus de la mort et ce encore aujourd’hui puisque des acharnés restent scotchés à ces langages primitifs et dangereux.

Well, tout est beau et chouette dans le monde managé… Sauf un petit détail : tous les objets n’ont pas vocation à avoir une vie infinie et tous ne possèdent pas un point unique de responsabilité de gestion de leur cycle de vie. Leur destruction, via des patterns comme Dispose ou autres, ne peut donc pas être encadrée et perd son caractère déterministe. Tout repose ainsi sur l’intelligence du GC pour faire le ménage. Intelligence très limitée.

Or, même comme cela les choses ne sont pas si simples…

Dans le cas de l’exemple évoqué plus haut, pour que l’instance de B puisse être collectée par le GC il faut absolument que l’instance de A relâche la référence qu’elle possède sur cette dernière. Donc sans logique particulière l’instance de B vivra aussi longtemps que celle de A qui la possède.

Si le besoin de référencer b dans a est une obligation durant toute la vie de a, cela n’est pas gênant. Mais si l’utilisation que a fait de b n’a de sens que ponctuellement, b engorgera la mémoire pour rien l’essentiel du temps.

Les applications managés ont ainsi une tendance fâcheuse à consommer de la RAM plus qu’il n’en faudrait. Sauf si le code est parfaitement développé, mais la perfection n’étant pas de ce monde il y a toujours un peu de perte. Ce qui n’est pas grave en soi car la plupart des développeurs aujourd’hui travaillent pour des machines gorgées de RAM.

Pour les applications utilisées sporadiquement dans une journée cela ne pose aucun problème en général. Pour des services Windows tournant en permanence les problèmes peuvent survenir comme pour de très grosses applications brassant beaucoup de données.

D’où l’importance même en managé d’apporter un soin particulier à l’écriture des classes et des relations qu’elles entretiennent, d’implémenter Dispose même s’il n’y a pas de référence extérieur pour permettre le nettoyage des références managées, ajouter une méthode de type Close() ou équivalent pour marquer la fin de l’utilisation d’une instance et nettoyer les références qu’elle possède, etc… 

Affaiblir les liens

Donc pour pallier ce problème il faut affaiblir les liens entre les instances.

Mais ce n’est qu’une phrase, en réalité cela veut dire quoi ? Comment rendre un lien “faible” ?

Il faut d’abord penser qu’il y a deux types de liens dans un environnement comme .NET : les références entre objets, comme l’exemple utilisé jusqu’ici, et les évènements. C’est un peu simplificateur mais du point de vue du développeur ce sont ces deux situations qui vont lui poser problème.

Pour ce qui est des liens de type référence je vous ai déjà parlé des Weak References et je vous incite à lire cet article si ce n’est pas déjà fait.

Concernant les évènements .. c’est le sujet du jour !

Les Weak Events

Comme nous l’avons vu les liens forts entre deux objets peuvent être “affaiblis” par l’utilisation des Weak References. Ce mécanisme permet à une instance a de A de référencer une instance b de type B d’une telle façon que b peut être collecté à tout moment. La classe A est écrite pour savoir se débrouiller dans ce cas là et l’utilisation des Weak References lui permet de savoir si l’instance b existe toujours ou non.

Pour les évènements l’affaire est beaucoup moins simple. Quand a référence l’instance b, c’est clair. L’instance a est responsable de ce lien qu’elle noue avec b.

Mais dans la gestion des évènements le lien est plus indirect.

Si on pose maintenant qu’il n’y a plus de lien direct entre A et B et que la classe B expose un évènement E, l’instance a de type A doit posséder une méthode M et l’inscrire auprès de b pour que b puisse l’appeler lorsque cela sera nécessaire. Cette fois-ci a n’est plus responsable de tout. Au contraire, c’est b qui pour invoquer M doit garder une référence sur a. Ce mécanisme est évident mais il n’est matérialisé par aucun morceau de code.

Quand a s’abonne à l’évènement E de b, il passe la référence à sa méthode M, mais cette référence inclut implicitement une référence à a

Ce qui créée un lien fort entre b et a.
b possède désormais une référence forte sur a par l’intermédiaire de celle qu’elle a enregistrée sur M.

Ce circuit plus complexe rend forcément le problème des références fortes plus difficile à gérer que dans le cas des références directes entre deux objets.

Les problèmes soulevés sont nombreux et sont la cause de fuites mémoires sous .NET.

On croyait avoir résolu le problème des fuites en passant au managé mais ce n’est pas totalement vrai… Il faut ici aussi développer un soin particulier pour éviter de perdre des bouts de mémoire, ce qui revient un peu au même que les pointeurs non libérés en C… Enfin pas tout à fait. Car ici nous connaissons le mécanisme responsable de ces pertes, toujours le même, celui lié aux références fortes alors qu’en C on ne peut pas dégager de solution globale puisque c’est inhérent au code lui-même et à sa logique (l’oubli de libération d’un pointeur ou libérer une quantité de mémoire différente de celle allouée ne peuvent être réglés en C par du C, il faut passer au managé justement).

Il existe des références de deux types et le problème est réglé pour les références directes avec les Weak References. Reste donc à régler le problème pour les évènements (et plus généralement pour tout ce qui fonctionne sur le mode d’un abonnement comme les Commandes sous MVVM, raison pour laquelle MVVM Light utilise des Weak Events dans son implémentation de RelayCommand).

Avant .NET 3.5

Ce n’était pas la préhistoire mais concernant les Weak Events il fallait le faire à la main. Le Framework ne prévoyait pas de solution particulière. Il faut comprendre qu’à cette époque les évènements sont principalement utilisés pour liés par exemple le Click d’un bouton à du code-behind majoritairement sous Windows Forms. Le code behind existe aussi longtemps que la Form existe, ce lien fort entre ces deux objets ne posait donc pas de souci en soi.

Les problèmes sont apparus dès lors qu’on a mis l’accent sur la programmation découplée et des architectures basées sur ce concept du couplage faible, comme MVVM avec Silverlight ou WPF.

A Partir de .NET 3.5

Le problème devient assez voyant et le Framework propose alors sa solution.

Il s’agit d’un couple :

  • WeakEventManager
  • IWeakEventListener

C’est donc une solution en deux étapes un peu compliqué à comprendre.

En gros il faut créer un évènement personnalisé en dérivant de WeakEventManager puis dans la casse qui écoute supporter l’interface IWeakEventListener.

C’est assez lourd et c’est heureusement dépassé.

A partir de .NET 4.5

Le besoin étant toujours aussi pressant mais la version en kit de .NET 3.5 n’ayant pas convaincu, il fallait que le Framework nous propose un peu mieux. C’est avec la version 4.5 que la bonne nouvelle arrivera sous la forme d’un WeakEventManager, le même oui, mais en générique ce qui change tout !

Avec WeakEventManager<TEventSource,TEventArgs> l’écriture devient autrement plus légère puisqu’il n’y a plus besoin d’écrire un descendant de WeakEventManager pour chaque type d’évènement. Mieux encore, la source de l’évènement n’a plus besoin d’implémenter l’interface IWeakEventListener !

On retrouve ainsi une gestion d’évènement presque aussi légère que la version non protégée contre les fuites mémoires.

Ce qui peut se résumer par le petit exemple ci-dessous :

void Main()
{

	var source = new SourceDEvenement();
	Ecouteur listener = new Ecouteur(source);

	source.Raise();

	Console.WriteLine("Ecouteur à null:");
	listener = null;

	ViderMemoire();

	source.Raise();

	Console.WriteLine("Source à null :");
	source = null;

	ViderMemoire();
}

static void ViderMemoire()
{
	Console.WriteLine("**Vidage en cours**");

	GC.Collect();
	GC.WaitForPendingFinalizers();
	GC.Collect();

	Console.WriteLine("**Nettoyé!**");
}


public class SourceDEvenement
{
	public event EventHandler<EventArgs> Event = delegate { };

	public void Raise()
	{
		Event(this, EventArgs.Empty);
	}
}

public class Ecouteur
{
	private void OnEvent(object source, EventArgs args)
	{
		Console.WriteLine("L'écouteur à reçu un évènement de la source.");
	}

	public Ecouteur(SourceDEvenement source)
	{
		WeakEventManager<SourceDEvenement, EventArgs>.AddHandler(source, "Event", OnEvent);
	}

	~Ecouteur()
	{
		Console.WriteLine("Finaliseur de l'écouteur exécuté.");
	}
}

 

Code qui va produire la sortie suivante (testé sous LinqPad) :

L'écouteur à reçu un évènement de la source.
Ecouteur à null:
**Vidage en cours**
Finaliseur de l'écouteur exécuté.
**Nettoyé!**
Source à null :
**Vidage en cours**
**Nettoyé!**

Comme on le voit le code est très simple puisqu’une seule ligne permet à l’écouteur de s’abonner à la source d’évènement.

En procédant de cette façon plus aucun risque de fuite mémoire, si l’écouteur disparait (cela est forcé dans l’exemple ci-dessus) la référence n’est plus maintenue par la source et l’appel à Raise sur cette dernière ne provoque pas non plus d’erreur d’exécution. L’abonné a disparu, la source n’en a que faire et poursuit son existence paisiblement…

Conclusion

Les Weak Events de .NET 4.5 sont une grande simplification de ce qui était proposé avant.

Les utiliser est tellement simple qu’il n’y a plus d’excuse aux memory leaks d’antan, et pourtant les Weak Events sont encore très peu utilisés.

Mais après cette saine lecture vous serez plus vigilants j’en suis certain !

Stay Tuned !

Faites des heureux, PARTAGEZ l'article !