I. Définition

Une référence faible est une référence à un objet qui bien qu'elle en autorise l'accès n'interdit pas la possible suppression de ce dernier par le Garbage Collector.

En clair, cela signifie qu'une référence faible, d'où sa « faiblesse », ne crée pas un lien fort avec l'instance référencée. C'est une référence qui, du point de vue du système de libération des instances n'existe pas, elle n'est pas prise en compte dans le graphe des chemins des objets « accessibles » par le GC.

C'est en fait comme cela que tout fonctionne en POO classique dans des environnements non managés comme Win32, rien n'interdit une variable de pointer un objet qui a déjà été libéré, mais tout accès par ce biais se soldera par une violation d'accès.
Sous environnement non managé, il n'existe pas de solution simple pour éviter de telles situations, d'où l'engouement croissant pour les environnements managés comme .NET ou Java sans qu'aucun retour en arrière ne semble désormais possible, n'en déplaise à ceux qui restent attachés à Win32.

En effet, sous .NET une telle situation ne peut tout simplement pas arriver puisqu'on ne libère pas la mémoire explicitement, c'est le CLR qui s'en charge lorsqu'il n'y a plus de référence à l'objet.

Il ne faut d'ailleurs pas confondre mémoire et ressources externes. .NET protège la mémoire, pas les ressources externes. Pour cela les classes doivent implémenter IDisposable. Et si un objet a été « disposé », il n'est pas « libéré » pour autant. On peut donc continuer à l'utiliser, mais cela créera le plus souvent une erreur d'exécution puisque les ressources externes auront été libérées entretemps… Prenez une instance de System.Drawing.Font, appelez sa méthode Dispose(). Vous pourrez toujours accéder à l'objet en tant qu'entité, mais si vous tentez d'appeler sa méthode ToHFont() qui retourne le handle de l'objet fonte sous-jacent, une exception sera levée… Il existe une nuance importante entre mémoire et ressources externes, entre libération d'un objet et libération de ses ressources externes. C'est là l'une des difficultés du modèle objet de .NET qui pose souvent des problèmes aux débutants, et parfois même à des développeurs plus confirmés.

II. Les mécanismes en jeu

Le Garbarge Collector du CLR libère la mémoire de tout objet qui ne peut plus être atteint. Un objet ne peut plus être atteint quand toutes les références qui le pointent deviennent non valides, par exemple en les forçant à null (nil sous Delphi). Lorsqu'il détruit les objets qui se trouvent dans cette situation le GC appelle leur méthode Finalize, à condition qu'une telle méthode soit définie et que le GC en ait été informé (le mécanisme réel est plus complexe et sort du cadre de cet article).

Lorsqu'un objet peut être directement ou indirectement atteint, il ne peut pas être supprimé par le GC. Une référence vers un objet qui peut être atteint est appelée une référence forte.

Une référence faible permet elle aussi de pointer un objet qui peut être atteint qu'on appelle la cible (target en anglais). Mais cette référence n'interfère pas avec le GC qui, si aucune référence forte n'existe sur l'objet, peut détruire ce dernier en ignorant les éventuelles références faibles (elles ne sont pas totalement ignorées puisque, nous allons le voir, la référence faible sera avertie de la destruction de l'objet).

Les références faibles se définissent par des instances de la classe WeakReference. Elle expose une propriété Target qui permet justement de réacquérir une référence forte sur la cible. À condition que l'objet existe encore… C'est pour cela que cette classe offre aussi un moyen de le savoir par le biais de sa propriété IsAlive (« est-il encore vivant ? »).

Pour un système de cache, comme évoqué en introduction, cela est très intéressant puisqu'on peut libérer des objets (plus aucune référence valide ne le pointe) et malgré tout le récupérer dans de nombreux cas si le besoin s'en fait sentir. Cela est possible, car entre le moment où un objet devient éligible pour sa destruction par le GC et le moment où il est réellement collecté et finalisé il peut se passer un temps non négligeable !

Le GC utilise trois « générations », trois conteneurs logiques. Les objets sont créés dans la génération 0, lorsqu'elle est pleine le GC supprime tous les objets inutiles et déplace ceux encore en utilisation dans la génération 1. Si celle-ci vient à être saturée le même processus se déclenche (nettoyage de la génération 1 et déplacement des objets encore valides dans la génération 2 qui représente toute la RAM disponible).

De fait, un objet qui a été utilisé un certain temps se voit pousser en génération 2, un endroit qui est rarement visité par le GC. Parfois même, s'il y a beaucoup de mémoire installée sur le PC et si l'application n'est pas très gourmande, les objets de la génération 2, voire de la génération 1, ne seront jamais détruits jusqu'à la fermeture de l'application… À ce moment précis, le CLR videra d'un seul coup tout l'espace réservé sans même finaliser les objets ni appeler leur destructeur. C'est pourquoi sous .NET on ne programme généralement pas de destructeurs dans les classes : le mécanisme d'appel à cette méthode n'est pas déterministe.

Donc, durant toute la vie de l'application de nombreuses instances restent malgré tout en vie « quelque part » dans la RAM. Si une référence faible pointe l'un de ces objets, il pourra donc être « récupéré » en réacquérant une référence forte dessus par le biais de la propriété Target de l'objet WeakReference. Si l'objet en question réclame beaucoup de traitement pour sa création, il y a un énorme avantage à le récupérer s'il doit resservir au lieu d'avoir à le recréer, le tout sans pour autant engorger la mémoire puisque, si nécessaire, le GC l'aura totalement libéré, ce qu'on saura en interrogeant la propriété IsAlive de l'objet WeakReference qui aura alors la valeur false.

Si vous vous souvenez de ce que nous disions plus haut sur la nuance entre libération d'une instance et libération de ses ressources externes, vous comprenez que l'application ne doit utiliser des références faibles que sur des objets qui n'implémente pas IDisposable. En effet, pour reprendre l'exemple d'une instance de la classe Font, si avant de mettre la référence à null votre application a appelé sa méthode Dispose(), « récupérer » plus tard l'instance grâce à une référence faible sera très dangereux : les ressources externes sont déjà libérées et toute utilisation de l'instance se soldera par une exception. Il est donc important de se limiter à des classes non disposables.

III. L'intérêt

Les références faibles ne servent pas qu'à mettre en œuvre des systèmes de cache, elles servent aussi lorsqu'on doit pointer des objets qui peuvent et doivent éventuellement être détruits. Rappelons-nous : si nous utilisons une simple référence sur un tel objet, il ne sera jamais détruit puisque justement nous le référençons… Les références faibles permettent d'échapper à ce mécanisme par défaut qui, parfois, devient une gêne plus qu'un avantage.

Un exemple d'une telle situation : supposons une liste de personnes. Cette liste pointe donc des instances de la classe Personne. Imaginons maintenant que l'application autorise la création de « groupes de travail », c'est-à-dire des listes de personnes. Si les listes définissant les groupes de travail pointent directement les instances de Personne et si une personne est supprimée de cette liste, les groupes de travail continueront de « voir » cette personne puisque l'instance étant référencée (référence forte) elle ne sera pas détruite par sa simple suppression de la liste des personnes…

En fait, on souhaitera dans un tel cas que toute personne supprimée de la liste principale n'apparaisse plus dans les groupes de travail dans lesquels elle a pu être référencée.

Cela peut se régler par une gestion d'événement : toute suppression de la liste des personnes entraînera le balayage de tous les groupes de travail pour supprimer la personne. Cette solution n'est pas toujours utilisable. Les références faibles deviennent alors une alternative intéressante, notamment parce qu'il n'y a pas besoin d'avoir prévu un lien entre la liste principale et les listes secondaires qui peuvent être ajoutées après coup dans la conception de l'application et parce que la suppression d'une personne n'impose pas une attente en raison du balayage de toutes les listes secondaires.

IV. Mise en œuvre

Il est temps de voir comment implémenter les références faibles.

Finalement, vous allez le constater, c'est assez simple. Les explications qui précèdent permettent de comprendre pourquoi les références faibles sont utiles. Les utiliser réclame moins de mots…

La classe WeakReference

Cette classe appartient à l'espace de nom System du framework. Son constructeur prend en paramètre l'instance que l'on souhaite référencer (la cible).

Elle expose trois propriétés caractéristiques, les autres propriétés et méthodes étant celles héritées de la classe mère System.Object :

IsAlive

Indique si l'instance référencée est vivante ou non.

Target

Permet de réacquérir une référence forte sur la cible.

TrackResurrection

Pour dés/activer le pistage de résurrection de la cible.

Un mot sur cette dernière propriété : lorsque l'on crée une référence faible, on peut indiquer dans le constructeur, en plus de l'objet ciblé, un paramètre booléen qui fixera la valeur de TrackResurrection. Lorsque la valeur est « false » (par défaut) on parle de référence faible « courte », lorsque la valeur est « true » on parle de référence faible « longue ».

Si l'objet possède un finaliseur, c'est dans celui-ci qu'il sera possible d'indiquer ou non si l'instance doit rester en vie (être ressuscitée) ou pas, notamment par un appel à GC.ReRegisterForFinalize(this). L'intérêt se trouve surtout dans les gestions de cache, car un objet « finalisable » survivra à au moins un cycle du GC en étant promu de la génération 0 à la génération 1. En retardant encore sa finalisation il sera poussé en génération 2 où il restera certainement un bon moment, améliorant ainsi grandement les chances de pouvoir le récupérer plus tard, donc rendant la gestion du cache encore plus efficace.

Le code qui suit est autodocumenté par sa simple exécution. S'agissant de projets console il vous suffit de créer une nouvelle application de ce type (que ce soit sous C# ou Delphi.Net) et de faire un copier / coller du code proposé. Lancez l'exécution (F5 sous VS2003/2005, F9 sous BDS) et laissez-vous guider à l'exécution en jetant un œil sur le code…

IV-A. Version C#

Voici la version C#. Elle est développée en framework 1.1 afin que tous les lecteurs puissent la tester (le même code fonctionne en framework 2.0). De même, nous avons choisi le mode « application console » pour éviter de s'encombrer avec la mise en page.

 
TéléchargerCacher/Afficher le codeSélectionnez

IV-B. Version Delphi.NET

Voici la version Delphi.NET. Elle est développée en framework 1.1 (par force puisque Highlander n'est pas encore sorti). De même, nous avons choisi le mode « application console » pour éviter de s'encombrer avec la mise en page.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
program od.article.wr;

{$APPTYPE CONSOLE}


uses

  SysUtils,
  StrUtils,
  System.Collections;

  type

  Personne = class
  private
   _Nom : string;
  public

   procedure setNom(const Value: string);
   property Nom:string read _Nom write setNom;

   constructor Create(nom:string);
  end;

{ Personne }

procedure Personne.setNom(const Value: string);
begin

 _nom := IfThen(Value<>nil,Value.Trim(),System.&String.Empty);

end;

constructor Personne.Create(nom: string);

begin
  inherited Create();
  _nom := nom.Trim();

end;

{ fin Personne }

var Employés : ArrayList;
const _line = '----------------';

procedure Return();
begin
 Console.WriteLine('<return> pour continuer...');

 Console.ReadLine();
end;

procedure ListeEmployés();

var p:Personne;
begin
 Console.WriteLine(_line);

 for p in Employés do Console.WriteLine(p.Nom);

 Console.WriteLine(_line);
 Console.WriteLine();

 Return();
end;


var p:Personne;

    wr:WeakReference;
begin
 Employés := ArrayList.Create();

 Employés.Add(Personne.Create('Olivier'));
 Employés.Add(Personne.Create('Barbara'));

 Employés.Add(Personne.Create('Jacky'));
 Employés.Add(Personne.Create('Valérie'));

 Console.WriteLine('Liste originale');
 ListeEmployés();

 p := Personne(Employés[0]); // pointe "olivier"
 Employés.RemoveAt(0); // suppression "olivier" dans liste



 Console.WriteLine('p pointe : '+p.Nom);
 Console.WriteLine('l''élément 0 de la liste a été supprimé');

 Console.WriteLine();

 ListeEmployés();

 Console.WriteLine('Mais l''objet pointé par p existe toujours : '+p.nom);

 Return();

 wr := WeakReference.Create(Employés[0]); // pointe "barbara"


 Console.WriteLine('wr est une référence faible sur : '+Personne(wr.Target).Nom);

 Return();

 Console.WriteLine('La cible de wr est vivante ? : '+wr.IsAlive.ToString());

 Employés.RemoveAt(0); // suppression de la liste de "barbara"

 Return();

 Console.WriteLine('l''élément 0 (barbara) a été supprimé, la liste devient : ');
 ListeEmployés();

 Console.WriteLine('La cible de wr est vivante ? : '+wr.IsAlive.ToString());

 Console.WriteLine('On peut réacquérir la cible : '+Personne(wr.Target).Nom);

 Return();

 Console.WriteLine('Mais si le GC passe par là...');

 GC.Collect(GC.MaxGeneration);

 Console.WriteLine('La cible de wr est vivante ? : '+wr.IsAlive.ToString());

 Console.WriteLine('La référence faible n''a pas interdit la suppression de la cible.');

 Return();

end.

V. Conclusion

Cet article est un peu « austère », pas de jolis diagrammes UML qui font branché, pas de capture d'écran pour enjoliver, du texte et du code… L'auteur a bien conscience que ce côté aride aura peut-être rebuté quelques lecteurs, alors, à vous qui en êtes arrivé à cette conclusion il tient à vous dire merci… et …

Bon développement !

Fichiers complémentaires à télécharger

Télécharger la version PDF : version PDF 26 Ko

Télécharger les sources : zip 35 Ko

Olivier Dahan.

Développement et Formation Win32/.NET, Delphi/C#

e-naxos