Voir le flux RSS

François DORIN

IDisposable... une interface contradictoire ?

Noter ce billet
par , 29/07/2016 à 11h39 (1893 Affichages)
L'interface IDisposable n'est pas une interface comme les autres. Non pas à cause de l'importance qu'elle revêt dans la gestion des ressources, mais parce que cette interface n'est pas un contrat mais reflète un aspect lié à l'implémentation.

Définition

Pour le retour aux sources, commençons par redonner la définition d'une interface telle que définie par la MSDN :
Une interface contient uniquement les signatures des méthodes, des propriétés, des événements ou des indexeurs. Une classe ou struct qui implémente l'interface doit implémenter les membres de l'interface spécifiés dans la définition d'interface.
C'est pour cela qu'on parle souvent de contrat lorsqu'on fait référence à une interface. Une classe implémentant une interface propose, par contrat, un service. Par exemple, si nous disposons d'une collection implémentant l'interface IEnumerable ou IEnumerable<T>, alors nous pouvons :
  • Récupérer un énumérateur via la méthode GetEnumerator() ;
  • Parcourir la liste via l'instruction foreach ;
  • Utiliser notre collection dans toutes méthodes nécessisant en paramètre une instance implémentant IEnumerable ;
  • etc...


Mais pourquoi tout cela ?
L'objectif de ce billet n'est pas de faire un cours sur la gestion des interfaces, mais sur l'interface IDisposable et ses spécificités. Et justement, c'est en regardant les interfaces IEnumerable et IEnumerable<T> que je suis tombé sur l'une d'elle. Plus particulièrement en regardant les interfaces suivantes :


Les deux interfaces sont les mêmes, au détail près que l'une est générique, l'autre non.

Mais êtes-vous sûr(e) de cela ? N'y a-t-il pas un autre détail qui viendrait faire de ces interfaces deux interfaces finalement très différente ? Un petit indice, regardez bien au niveau de l'héritage...

Et oui ! IEnumerator<T> dérive de IDisposable, tandis que IEnumerator non. Nous voilà enfin au coeur du sujet de ce billet.

Détail d'implémentation
Nous avons donc ici deux interfaces, une générique, l'autre non, mais qui permettent de spécifier le même contrat, mais dont l'usage est différent. En effet, IDisposable n'apporte pas d'indication sur les fonctionnalités offertes par les instances d'une classe, mais sur la manière de les utiliser.

Cette interface apporte un élément sur l'implémentation d'une classe. Elle nous informe qu'il est nécessaire de gérer les ressources associées.

Si nous en revenons à nos deux interfaces IEnumerator et IEnumerator<T>, qu'apprenons nous ? Que c'est un système simple en apparence, mais qui soulève de nombreuses questions dès lors qu'on s'y intéresse de près, nécessitant parfois de revoir notre position (ce qu'à fait Microsoft dans le cas présent). Historiquement, avant l'introduction de la généricité, il n'y avait que l'interface IEnumerator. Le choix a été fait ici de ne pas dériver de IDisposable.

En effet, l'objectif de l'interface IEnumerator est de permettre l'accès aux éléments d'une collection. Savoir si un objet a besoin d'une gestion particulière pour gérer la libération de ses ressources ne relève pas de l'interface, mais de l'implémentation.

Dans un langage sans ramasse miette, ce problème de la gestion de la libération des ressources ne se pose pas, puisque c'est au programmeur d'appeler plus ou moins explicitement un destructeur sur l'objet. Mais dans un langage tel que le C#, disposant d'un ramasse miette, cette problématique est bien présente. On sait que l'objet sera détruit lorsqu'il sera devenu inutile. Mais c'est un "lorsque", pas un "dès que". Nous n'avons aucune information sur le "quand". On sait que l'objet sera détruit automatiquement, dans un futur plus ou moins proche . Lorsque la seule ressource en jeu est la mémoire, ce n'est pas grave, car c'est justement le rôle du ramasse miette de s'en occuper. Mais s'il y a d'autres ressources (accès à des fichiers, des connexions à une base de données, connexion à un réseau, etc...) alors ne pas savoir ce "quand" et ne pas pouvoir interagir dessus peut vite devenir problématique.

Et c'est justement pour palier à cela que l'interface IDisposable a été introduite : permettre de décider du "quand" pour la libération des ressources (autre que la mémoire bien entendu).

Une limitation à ce système intervient assez vite dès lors qu'une interface à l'utilisation très générique est définie (ici IEnumerator). Si, dans la plupart des cas, il n'y a pas besoin de libération de ressources, comment s'assurer que cela ne sera jamais le cas ? Réponse : on ne le peut pas. Aussi, pour tenir compte des quelques cas où cela sera nécessaire, notre interface doit dériver de IDisposable. C'est sans doute pour cela qu'avec l'introduction de la généricité, Microsoft a légèrement modifié l'interface IEnumerator pour que sa version générique dérive de IDisposable. On se retrouve donc dans la situation suivante : pour tenir compte des cas où une libération des ressources explicite doit pouvoir se faire, on dérive de l'interface IDisposable. Même si, pour la plupart des implémentations, cela n'est pas nécessaire (et le corps de la méthode Dispose() sera tout simplement vide). On a fait remonté un aspect lié à l'implémentation au sein d'une interface.

Au final, on peut se demander si la gestion de la libération des ressources n'aurait pas pue être faite autrement. En effet, le développeur que nous sommes, lorsqu'il instancie un nouvel objet, doit vérifier s'il implémente l'interface IDisposable pour savoir si et comment il doit le gérer. Comme tout dérive de la classe System.Objet en .Net, on aurait pu imaginer que la méthode Dispose() ne soit pas définie via une interface, mais comme méthode virtuelle de la classe de base de tous les objets, au même titre que la méthode ToString() par exemple. Je ne doute pas que Microsoft ait envisagé cette voie. Si cela n'a pas été retenu, c'est très certainement pour de bonnes raisons. Pour ma part, j'en vois au moins 2 :
  • L'une d'entre elles pourrait être l'impact sur les performances (un appel à une méthode virtuelle sur tous les objets par le ramasse miette par exemple) ;
  • Une autre raison aurait pu être que finalement, les développeurs n'auraient pas porté d'attention plus particulière à la gestion des ressources si tous les objets possédaient cette méthode Dispose(). Il est inenvisageable de fournir une architecture basée sur un ramasse miette d'un côté, et de demander au développeur d'appeler la méthode Dispose() sur chaque objet devenu inutile. Quel serait l'intérêt dans ce cas du ramasse-miette ?


Implémentation de IDisposable
Histoire de finir ce billet, je vais vous parler de la manière recommandée d'implémenter l'interface IDisposable, connu sous le nom de IDisposable pattern. Microsoft a écrit un article sur la manière recommandée de le faire.

Voici le code. Les explications seront juste en dessous :
Code C# : Sélectionner tout - Visualiser dans une fenêtre à part
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
public class Resource : IDisposable 
{
    private IntPtr nativeResource = Marshal.AllocHGlobal(100);
    private AnotherResource managedResource = new AnotherResource();
 
    // Dispose() calls Dispose(true)
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
    // NOTE: Leave out the finalizer altogether if this class doesn't 
    // own unmanaged resources itself, but leave the other methods
    // exactly as they are. 
    ~Resource() 
    {
        // Finalizer calls Dispose(false)
        Dispose(false);
    }
    // The bulk of the clean-up code is implemented in Dispose(bool)
    protected virtual void Dispose(bool disposing)
    {
        if (disposing) 
        {
            // free managed resources
            if (managedResource != null)
            {
                managedResource.Dispose();
                managedResource = null;
            }
        }
        // free native resources if there are any.
        if (nativeResource != IntPtr.Zero) 
        {
            Marshal.FreeHGlobal(nativeResource);
            nativeResource = IntPtr.Zero;
        }
    }
}

L'exemple donné par Microsoft est intéressant, puisque cette classe gère à la fois des ressources managées (managedResource) et non managées (nativeResource).

Tout d'abord, on constate que la classe Resource implémente bien l'interface IDisposable. De ce fait, elle dispose d'une méthode Dispose() (jeu de mots totalement assumé ). Mais elle fournit également :
  • Un destructeur ~Resource ;
  • Une méthode virtuelle protected virtual void Dispose(bool disposing).


La méthode gérant la libération des ressources liées à l'instance de l'objet est la méthode virtuelle Dispose qui prend en argument un booléen. L'objectif de ce booléen est de permettre de déterminer le contexte de l'appel à la méthode de libération des ressources :
  • S'il est true, alors un appel explicite à la méthode Dispose() de l'interface IDisposable a été effectué.
  • S'il est false, alors l'objet est en cours de destruction par le Garbage Collector (GC).


Il est nécessaire de distinguer les deux, car lorsque le Garbage Collector ramasse les miettes et recycle un objet, c'est que cet objet n'est plus référencé (ou pour être plus précis que ses références ne sont plus atteignables depuis l'application). Dans ce cas, le GC appelle le destructeur de l'objet. Mais un objet en cours de recyclage peut référencer d'autres objets (dans notre cas, via l'attribut managedResource). Le soucis est que le GC ne fournit aucun ordre quant au recyclage des objets. Il est donc tout à fait possible que l'objet référencé par l'attribut managedResource ait déjà été recylé ! Il est donc très important de ne pas y accéder lorsque la libération des ressources intervient dans le cadre du GC.

Cette méthode est également marquée comme virtuelle afin de permettre aux classes qui viendrait à hériter de notre classe de surcharger la méthode afin de prendre en compte des libérations de ressources supplémentaires si cela est nécessaire.

Encore, un dernier petit détail. Dans le corps de l'implémentation de la méthode Dispose() de l'interface IDisposable, on peut remarquer un appel à une méthode du GC : GC.SuppressFinalize(this). Cet appel met juste initialise un drapeau au sein de l'objet pour signifier au GC que lorsque le prochain ramassage aura lieu, il n'est pas utile d'appeler le destructeur de cet objet.

Enfin, malgré que l'exemple soit donné par Microsoft lui-même, il manque un aspect très important. Une fois la méthode Dispose() appelée sur une instance, il peut encore exister des références à cette instance dans votre application (et donc cette instance peut encore être utilisée alors qu'elle ne le devrait pas). Normalement, tout appel à une méthode publique sur cette instance devrait lever l'exception ObjectDisposedException. Aussi, toute classe implémentant l'interface IDisposable devrait avoir un attribut booléen "isDisposed" indiquant si un appel à la méthode Dispose a été effectué ou non. Lors de l'appel d'une méthode publique (ou l'accès à une propriété publique), il est recommandé de vérifier la valeur de ce booléen, et le cas échéant, de lever une exception.

Envoyer le billet « IDisposable... une interface contradictoire ? » dans le blog Viadeo Envoyer le billet « IDisposable... une interface contradictoire ? » dans le blog Twitter Envoyer le billet « IDisposable... une interface contradictoire ? » dans le blog Google Envoyer le billet « IDisposable... une interface contradictoire ? » dans le blog Facebook Envoyer le billet « IDisposable... une interface contradictoire ? » dans le blog Digg Envoyer le billet « IDisposable... une interface contradictoire ? » dans le blog Delicious Envoyer le billet « IDisposable... une interface contradictoire ? » dans le blog MySpace Envoyer le billet « IDisposable... une interface contradictoire ? » dans le blog Yahoo

Mis à jour 17/08/2017 à 15h30 par Malick (Ajout balises code)

Catégories
DotNET , C#

Commentaires

  1. Avatar de ebastien
    • |
    • permalink
    C'est ce que j'appelle un billet clair, efficace et complet sur l'interface IDisposable parfois négligée par les développeurs... On prend du plaisir à le lire. Beau travail !
  2. Avatar de François DORIN
    • |
    • permalink
    Citation Envoyé par ebastien
    C'est ce que j'appelle un billet clair, efficace et complet sur l'interface IDisposable parfois négligée par les développeurs... On prend du plaisir à le lire. Beau travail !
    Merci beaucoup pour les encouragements. Je ferai mon maximum pour que les prochains billets soient d'une qualité au moins équivalente à celui-ci.