P.S. : J'ai posté un nouvelle réponse (contenant un ensemble de règles simples pour savoir qui doit appeler Dispose
et comment concevoir une interface de programmation (API) qui traite des IDisposable
objets). Bien que la présente réponse contienne des idées intéressantes, j'en suis venu à penser que sa principale suggestion ne fonctionne souvent pas dans la pratique : Cacher IDisposable
dans des objets à grain plus grossier signifie souvent que ces derniers doivent devenir des objets à grain plus grossier. IDisposable
On se retrouve donc au point de départ, et le problème reste entier.
Existe-t-il des conseils ou des bonnes pratiques concernant les personnes à appeler ? Dispose()
sur les objets jetables lorsqu'ils ont été passés dans les méthodes ou le constucteur d'un autre objet ?
Réponse courte :
Oui, il existe de nombreux conseils sur ce sujet, et le meilleur que je connaisse est le suivant Eric Evans Le concept de Agrégats en Conception pilotée par domaine . (En d'autres termes, l'idée centrale, telle qu'elle est appliquée aux IDisposable
est la suivante : Encapsuler le IDisposable
dans un composant à grain plus grossier, de sorte qu'il n'est pas vu de l'extérieur et n'est jamais transmis au consommateur du composant).
De plus, l'idée que le créateur d'un IDisposable
L'objet doit également être en charge de l'élimination des déchets est trop restrictif et ne fonctionne souvent pas dans la pratique.
Le reste de ma réponse aborde plus en détail ces deux points, dans le même ordre. Je terminerai ma réponse par quelques renvois à d'autres documents en rapport avec le même sujet.
Réponse plus longue - Ce dont il est question dans cette question en termes plus généraux :
Les conseils sur ce sujet ne sont généralement pas spécifiques aux IDisposable
. Chaque fois que l'on parle de durée de vie et de propriété des objets, on se réfère au même problème (mais en termes plus généraux).
Pourquoi ce sujet ne se pose-t-il pratiquement jamais dans l'écosystème .NET ? Parce que l'environnement d'exécution de .NET (le CLR) effectue un ramassage automatique des ordures, qui fait tout le travail à votre place : Si vous n'avez plus besoin d'un objet, vous pouvez simplement l'oublier et le ramasse-miettes s'en chargera. éventuellement se réapproprier sa mémoire.
Pourquoi, alors, la question se pose-t-elle avec IDisposable
objets ? Parce que IDisposable
concerne le contrôle explicite et déterministe de la durée de vie d'une ressource (souvent peu abondante ou coûteuse) : IDisposable
Les objets sont censés être libérés dès qu'ils ne sont plus nécessaires - et la garantie indéterminée du ramasse-miettes ("Je vais éventuellement se réapproprier le mémoire utilisé par vous !") n'est tout simplement pas suffisant.
Votre question, reformulée en termes plus généraux de durée de vie et de propriété des objets :
Quel objet O
devrait être responsable de la fin de la durée de vie d'un objet (jetable) D
qui est également transmis aux objets X,Y,Z
?
Posons quelques hypothèses :
-
Appel D.Dispose()
pour un IDisposable
objet D
est en fait la fin de sa durée de vie.
-
Logiquement, la durée de vie d'un objet ne peut être interrompue qu'une seule fois. (Peu importe pour l'instant que cela s'oppose à la règle du IDisposable
qui autorise explicitement les appels multiples à Dispose
.)
-
Par conséquent, pour des raisons de simplicité, un seul objet O
doit être responsable de l'élimination D
. Appelons-le O
le propriétaire.
Nous arrivons maintenant au cœur du problème : Ni le langage C#, ni VB.NET ne fournissent de mécanisme permettant de renforcer les relations de propriété entre les objets. Il s'agit donc d'un problème de conception : Tous les objets O,X,Y,Z
qui reçoivent une référence à un autre objet D
doivent suivre et adhérer à une convention qui régit exactement qui est propriétaire de D
.
Simplifiez le problème avec les agrégats !
Le meilleur conseil que j'ai trouvé sur ce sujet est celui de Eric Evans Livre de 2004, Conception pilotée par domaine . Permettez-moi de citer un extrait du livre :
Supposons que vous supprimiez un objet Personne d'une base de données. La personne est accompagnée d'un nom, d'une date de naissance et d'une description de poste. Mais qu'en est-il de l'adresse ? Il peut y avoir d'autres personnes à la même adresse. Si vous supprimez l'adresse, ces objets Personne feront référence à un objet supprimé. Si vous la laissez, vous accumulez des adresses inutiles dans la base de données. Le ramassage automatique des ordures pourrait éliminer les adresses inutiles, mais cette solution technique, même si elle est disponible dans votre système de base de données, ne tient pas compte d'un problème de modélisation fondamental. (p. 125)
Vous voyez le lien avec votre problème ? Les adresses de cet exemple sont l'équivalent de vos objets jetables, et les questions sont les mêmes : qui doit les supprimer ? Qui les "possède" ?
Evans poursuit en suggérant Agrégats comme solution à ce problème de conception. Encore un extrait du livre :
Un agrégat est un groupe d'objets associés que nous traitons comme une unité pour les modifications de données. Chaque agrégat a une racine et une limite. La limite définit ce qui se trouve à l'intérieur de l'agrégat. La racine est une entité unique et spécifique contenue dans l'agrégat. La racine est le seul membre de l'agrégat auquel les objets extérieurs sont autorisés à faire référence, bien que les objets situés à l'intérieur du périmètre puissent faire référence les uns aux autres. (pp. 126-127)
Le message essentiel est qu'il faut limiter la transmission de vos IDisposable
à un ensemble strictement limité ("agrégat") d'autres objets. Les objets situés en dehors de cet ensemble ne doivent jamais obtenir de référence directe à votre objet IDisposable
. Cela simplifie grandement les choses, car il n'est plus nécessaire de se demander si la plus grande partie de tous les objets, à savoir ceux qui se trouvent en dehors de l'agrégat, pourrait Dispose
votre objet. Il suffit de s'assurer que les objets à l'intérieur la frontière, tous savent qui est responsable de son élimination. Ce problème devrait être assez facile à résoudre, étant donné que vous les mettez généralement en œuvre ensemble et que vous veillez à ce que les limites de l'agrégat soient raisonnablement "étroites".
Qu'en est-il de la suggestion selon laquelle le créateur d'un IDisposable
doit également s'en débarrasser ?
Cette ligne directrice semble raisonnable et présente une symétrie attrayante, mais en soi, elle ne fonctionne souvent pas dans la pratique. On peut soutenir qu'elle a la même signification que de dire : "Ne passez jamais une référence à un fichier IDisposable
à un autre objet", car dès que vous faites cela, vous risquez que l'objet récepteur suppose sa propriété et en dispose à votre insu.
Examinons deux types d'interface importants de la bibliothèque de classes de base (BCL) .NET qui violent clairement cette règle de base : IEnumerable<T>
y IObservable<T>
. Les deux sont essentiellement des usines qui renvoient IDisposable
objets :
Dans les deux cas, le appelant est censé disposer de l'objet retourné. On peut soutenir que notre ligne directrice n'a tout simplement pas de sens dans le cas des fabriques d'objets... à moins, peut-être, d'exiger que l'élément demandeur (et non pas son créateur ) de l IDisposable
le libère.
Par ailleurs, cet exemple montre également les limites de la solution globale décrite ci-dessus : Les deux IEnumerable<T>
y IObservable<T>
sont beaucoup trop générales pour faire partie d'un agrégat. Les agrégats sont généralement très spécifiques à un domaine.
Autres ressources et idées :
-
En UML, les relations "a" entre les objets peuvent être modélisées de deux manières : En tant qu'agrégation (losange vide) ou en tant que composition (losange rempli). La composition diffère de l'agrégation en ce sens que la durée de vie de l'objet contenu/référencé prend fin avec celle du conteneur/référent. Votre question initiale impliquait l'agrégation ("propriété transférable"), tandis que je me suis principalement orienté vers des solutions qui utilisent la composition ("propriété fixe"). Voir l'article Article de Wikipédia sur "Composition d'objets" .
-
Autofac (un logiciel .NET IoC ) résout ce problème de deux manières : soit en communiquant, à l'aide d'un "conteneur", ce que l'on appelle le type de relation , Owned<T>
qui acquiert la propriété d'un IDisposable
ou par le biais du concept d'unités de travail, appelées "lifetime scopes" dans Autofac.
-
À ce sujet, Nicholas Blumhardt, le créateur d'Autofac, a écrit "L'abc de la durée de vie d'Autofac" qui comprend une section intitulée "IDisposable and ownership". L'article entier est un excellent traité sur les questions de propriété et de durée de vie dans .NET. Je recommande sa lecture, même à ceux qui ne sont pas intéressés par Autofac.
-
En C++, la fonction L'acquisition des ressources est l'initialisation (RAII) idiome (en général) et types de pointeurs intelligents (en particulier) aident le programmeur à gérer correctement les questions de durée de vie et de propriété des objets. Malheureusement, elles ne sont pas transférables à .NET, car ce dernier ne dispose pas du support élégant de C++ pour la destruction déterministe des objets.
-
Voir aussi cette réponse à la question sur Stack Overflow, "Comment tenir compte des besoins disparates en matière de mise en œuvre ? qui (si je comprends bien) suit un raisonnement similaire à celui de ma réponse basée sur l'agrégat : Construire un composant à gros grain autour de l'agrégat IDisposable
de manière à ce qu'il soit complètement contenu (et caché au consommateur du composant) à l'intérieur.