55 votes

Devriez-vous implémenter IDisposable.Dispose() pour qu'il ne se lance jamais ?

Pour le mécanisme équivalent en C++ (le destructeur), le conseil est le suivant il ne devrait généralement pas lever d'exceptions . Ceci est principalement dû au fait qu'en faisant cela, vous risquez d'interrompre votre processus, ce qui n'est que très rarement une bonne stratégie.

Dans le scénario équivalent en .NET ...

  1. Une première exception est levée
  2. Un bloc final est exécuté à la suite de la première exception.
  3. Le bloc finally appelle une méthode Dispose()
  4. La méthode Dispose() lève une deuxième exception

... votre processus ne s'arrête pas immédiatement. Cependant, vous perdez des informations car .NET remplace sans hésiter la première exception par la seconde. Un bloc catch situé quelque part en haut de la pile d'appels ne verra donc jamais la première exception. Cependant, on est généralement plus intéressé par la première exception car elle donne de meilleurs indices sur la raison pour laquelle les choses ont commencé à mal tourner.

Étant donné que .NET ne dispose pas d'un mécanisme permettant de détecter si du code est exécuté alors qu'une exception est en suspens, il semble qu'il n'y ait que deux possibilités d'implémentation d'IDisposable :

  • Toujours avaler toutes les exceptions qui se produisent à l'intérieur de Dispose(). Ce n'est pas une bonne chose car vous pourriez aussi finir par avaler OutOfMemoryException, ExecutionEngineException, etc. que je préfère généralement laisser détruire le processus lorsqu'elles se produisent sans qu'une autre exception soit déjà en attente.
  • Laissez toutes les exceptions se propager hors de Dispose(). Ce n'est pas une bonne chose car vous risquez de perdre des informations sur la cause première du problème, voir ci-dessus.

Alors, quel est le moindre des deux maux ? Y a-t-il une meilleure solution ?

EDIT : Pour clarifier, je ne parle pas de lancer activement les exceptions de Dispose() ou non, je parle de laisser les exceptions lancées par les méthodes appelées par Dispose() se propager hors de Dispose() ou non, par exemple :

using System;
using System.Net.Sockets;

public sealed class NntpClient : IDisposable
{
    private TcpClient tcpClient;

    public NntpClient(string hostname, int port)
    {
        this.tcpClient = new TcpClient(hostname, port);
    }

    public void Dispose()
    {
        // Should we implement like this or leave away the try-catch?
        try
        {
            this.tcpClient.Close(); // Let's assume that this might throw
        }
        catch
        {
        }
    }
}

36voto

Richard Points 54016

El Directives de conception du cadre (2 et ed) le présente comme (§9.4.1) :

ÉVITER lancer une exception à l'intérieur de Dispose(bool), sauf dans les situations critiques où le processus contenant a été corrompu (fuites, partages incohérents, etc.). état partagé incohérent, etc.)

Commentaire [Edit] :

  • Il y a des lignes directrices, pas des règles strictes. Et il s'agit d'une ligne directrice "À ÉVITER" et non "À NE PAS FAIRE". Comme indiqué (dans les commentaires), le cadre de travail enfreint cette directive (et d'autres) à certains endroits. L'astuce consiste à savoir quand enfreindre une directive. C'est, à bien des égards, la différence entre un compagnon et un maître.
  • Si une partie du nettoyage risque d'échouer, il faut fournir une méthode Close qui lèvera des exceptions afin que l'appelant puisse les gérer.
  • Si vous suivez le modèle de disposition (et vous devriez le faire si le type contient directement une ressource non gérée), alors la balise Dispose(bool) peut être appelé depuis le finisseur, lancer depuis un finisseur est une mauvaise idée et bloquera d'autres objets d'être finalisés.

Mon point de vue : les exceptions échappant à Dispose ne devraient être que celles, comme dans la directive, qui sont suffisamment catastrophiques pour qu'aucune autre fonction fiable ne soit possible à partir du processus actuel.

17voto

Marc Gravell Points 482669

Je dirais que la déglutition est le moindre des deux maux dans ce scénario, car il est préférable de soulever la original Exception - Attention : sauf si peut-être que le fait de ne pas se débarrasser proprement des déchets est en soi assez critique (peut-être si une TransactionScope ne pouvait pas disposer, car cela pourrait indiquer un échec du rollback).

Ver aquí pour d'autres réflexions à ce sujet, y compris l'idée d'une méthode d'enveloppement/extension :

using(var foo = GetDodgyDisposableObject().Wrap()) {
   foo.BaseObject.SomeMethod();
   foo.BaseObject.SomeOtherMethod(); // etc
} // now exits properly even if Dispose() throws

Bien sûr, vous pouvez aussi faire une bizarrerie en relançant une exception composite avec l'exception originale et la seconde ( Dispose() ) exception - mais réfléchissez : vous pourriez avoir plusieurs using blocs... cela deviendrait rapidement ingérable. En réalité, c'est l'exception originale qui est intéressante.

6voto

Mehrdad Afshari Points 204872

Dispose doit être conçu pour remplir son rôle, à savoir disposer de l'objet. Cette tâche est sûr et ne lève pas d'exceptions la plupart du temps. . Si vous vous voyez lancer des exceptions à partir de Dispose vous devriez probablement y réfléchir à deux fois pour voir si vous ne faites pas trop de choses dedans. A part ça, je pense Dispose doit être traitée comme toutes les autres méthodes : manipulez-la si vous pouvez en faire quelque chose, laissez-la bouillonner si vous ne pouvez pas.

EDIT : Pour l'exemple spécifié, j'écrirais le code de façon à ce que mon code ne provoque pas d'exception, mais le fait d'effacer le TcpClient pourrait provoquer une exception, ce qui devrait être valide pour propager à mon avis (ou à gérer et relancer comme une exception plus générique, comme toute méthode) :

public void Dispose() { 
   if (tcpClient != null)
     tcpClient.Close();
}

Cependant, comme toute autre méthode, si vous savez tcpClient.Close() peut lever une exception qui doit être ignorée (sans importance) ou qui doit être représentée par un autre objet d'exception, vous pouvez vouloir l'attraper.

2voto

Nir Points 18250

La libération des ressources devrait être une opération "sûre" - après tout, comment puis-je me remettre de l'impossibilité de libérer une ressource ? Ainsi, lancer une exception depuis Dispose n'a aucun sens.

Cependant, si je découvre à l'intérieur de Dispose que l'état du programme est corrompu, il vaut mieux lancer l'exception que de l'avaler, il vaut mieux écraser maintenant que continuer à fonctionner et produire des résultats incorrects.

2voto

supercat Points 25534

Il est dommage que Microsoft n'ait pas fourni un paramètre Exception à Dispose, afin qu'il soit enveloppé comme une InnerException au cas où Dispose lui-même lève une exception. Pour être sûr, l'utilisation efficace d'un tel paramètre nécessiterait l'utilisation d'un bloc de filtre d'exception, que C# ne prend pas en charge, mais peut-être que l'existence d'un tel paramètre aurait pu motiver les concepteurs de C# à fournir une telle fonctionnalité ? Une variation intéressante que j'aimerais voir serait l'ajout d'un "paramètre" Exception à un bloc Finally, par exemple

  finally Exception ex: // In C#
  Finally Ex as Exception  ' In VB

qui se comporterait comme un bloc Finally normal, à l'exception de 'ex' qui serait null/Nothing si le 'Try' s'exécute jusqu'au bout, ou qui contiendrait l'exception levée dans le cas contraire. Dommage qu'il n'y ait aucun moyen de faire en sorte que le code existant utilise une telle fonctionnalité.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X