Certains objets détiennent des ressources que le ramasse-miettes ne gère pas : fichiers ouverts, sockets réseau, connexions à une base de données, descripteurs du système d'exploitation. Ils implémentent IDisposable, et vous libérez la ressource en appelant Dispose(). L'instruction using l'appelle pour vous à un moment déterminé, même si une exception est levée.
Cette page porte sur l'instruction using. Les lignes using System; en haut d'un fichier sont une autre fonctionnalité, la directive using, qui importe un namespace.
Un bloc using
using (declaration) { body } crée l'objet, exécute le corps, puis appelle Dispose. Une petite classe qui affiche un message dans Dispose montre exactement quand cela se produit :
Sortie :
open db
db sends SELECT 1
close db
open api
api sends GET /orders
close api
error handled
Le second bloc lève une exception, et « close api » s'affiche quand même, avant que le catch ne traite l'exception. Cette garantie est la raison d'utiliser using au lieu d'appeler vous-même Dispose() sur la dernière ligne : un appel manuel est sauté par toute exception levée avant lui.
En quoi using se compile
Le compilateur développe un bloc using en un try/finally. Ces deux formes sont équivalentes :
using (var writer = new StreamWriter("log.txt"))
{
writer.WriteLine("started");
}
// is compiled as:
{
var writer = new StreamWriter("log.txt");
try
{
writer.WriteLine("started");
}
finally
{
if (writer != null) ((IDisposable)writer).Dispose();
}
}
Trois détails découlent de ce développement. La variable est limitée au bloc et en lecture seule à l'intérieur (vous ne pouvez pas réaffecter writer). L'objet doit implémenter IDisposable, sinon le code ne compile pas. Et une valeur null est autorisée : Dispose est simplement sauté, ce qui est pratique quand une fabrique peut ne rien renvoyer.
Fichiers et flux
Les classes de fichiers et de flux sont le cas quotidien. Un StreamWriter met le texte en mémoire tampon, et Dispose vide le tampon sur le disque puis ferme le descripteur de fichier :
Sortie :
id,total
1001,59.90
1002,12.50
Sans Dispose, les dernières lignes du writer peuvent rester dans son tampon, et sous Windows le descripteur ouvert empêche les autres programmes (et votre propre File.Delete suivant) de toucher au fichier. Des fonctions comme File.WriteAllText et File.ReadAllLines ouvrent et libèrent en interne, elles n'ont donc pas besoin de using. Voir fichiers pour plus de détails.
Plusieurs ressources : l'ordre de libération
Empilez des instructions using sans accolades entre elles pour ouvrir plusieurs ressources dans un seul bloc. Elles sont libérées dans l'ordre inverse de leur création :
Sortie :
acquire file
acquire buffer
acquire writer
working
release writer
release buffer
release file
acquire a
acquire b
both open
release b
release a
L'ordre inverse est ce dont ont besoin les ressources en couches : un writer enveloppe un tampon qui enveloppe un fichier, donc le writer doit vider son contenu dans le tampon avant que le tampon n'écrive dans le fichier, avant que le fichier ne se ferme.
La déclaration using (C# 8)
C# 8 a ajouté une forme sans accolades. La variable est libérée à la fin de la portée englobante, en général la fin de la méthode :
static void ExportReport(string path, IEnumerable<string> rows)
{
using var writer = new StreamWriter(path);
writer.WriteLine("Report");
foreach (string row in rows)
{
writer.WriteLine(row);
}
} // writer.Dispose() runs here, at the end of the method
Elle supprime un niveau d'indentation, ce qui compte quand une méthode utilise deux ou trois ressources. La règle d'ordre de libération est la même : les déclarations d'une même portée sont libérées dans l'ordre inverse. La contrepartie est que la ressource reste ouverte jusqu'à la fin de la portée ; quand un fichier doit être fermé avant que la méthode ne fasse d'autre travail, gardez la forme en bloc ou placez la déclaration dans son propre bloc { }.
Pour les objets dont le nettoyage est asynchrone (IAsyncDisposable, comme beaucoup de connexions à des bases de données et de flux), C# 8 propose aussi await using, qui attend DisposeAsync() au même endroit.
Implémenter IDisposable dans votre propre classe
Implémentez IDisposable quand votre classe possède quelque chose de libérable (elle a créé un flux, un timer, une connexion) et doit donc le libérer à son tour. Pour le cas courant, cela tient en une courte méthode :
Sortie :
1. user ana logged in
2. order 1001 created
AuditLog disposed
Caught ObjectDisposedException
Les règles suivies ici : Dispose libère tout ce que l'objet possède ; l'appeler plus d'une fois ne fait rien la seconde fois ; et utiliser l'objet après sa libération lève ObjectDisposedException. using (log) montre aussi que le bloc peut prendre une variable existante au lieu d'une déclaration.
Le « dispose pattern » plus long, avec une méthode protected virtual void Dispose(bool disposing) et un finaliseur, n'est nécessaire que lorsqu'une classe détient directement un descripteur non managé (un pointeur brut venant de code natif). Dans le .NET moderne, enveloppez plutôt ces descripteurs dans une sous-classe de SafeHandle, et la version simple ci-dessus est tout ce dont votre classe a besoin.
Erreurs courantes
- Renvoyer un objet créé dans un bloc
using. Il est libéré à la sortie du bloc, donc l'appelant reçoit un flux fermé. Renvoyez les données, ou laissez l'appelant posséder leusing. - Oublier
usingsur les flux et les writers. Des données en tampon peuvent être perdues et les fichiers restent verrouillés. - Libérer un objet partagé. Ne libérez que ce qui vous appartient. Un
HttpClientà longue durée de vie, par exemple, est fait pour être partagé et réutilisé, pas créé et libéré à chaque requête. - Utiliser un objet après le bloc. Les méthodes d'un objet libéré lèvent
ObjectDisposedException. - Compter sur le ramasse-miettes. Il libère la mémoire, pas les fichiers ni les sockets, et n'appelle jamais
Dispose.
Questions fréquentes
Que fait l'instruction using en C# ?
using (var x = ...) { ... } appelle x.Dispose() à la fin du bloc, que celui-ci se termine normalement, par un return ou à cause d'une exception. Le compilateur la transforme en try/finally avec l'appel à Dispose dans le finally, si bien que la ressource est libérée à un moment connu plutôt qu'au passage du ramasse-miettes.
Qu'est-ce qu'IDisposable en C# ?
IDisposable est une interface à une seule méthode, void Dispose(). Une classe l'implémente quand elle détient quelque chose qui doit être libéré explicitement : un descripteur de fichier, une socket réseau, une connexion à une base de données, un timer. Tout objet dont le type implémente IDisposable doit être libéré quand vous n'en avez plus besoin, en général avec using.
Quelle est la différence entre using et using var ?
using (var x = ...) { } libère l'objet à la fin de son bloc. La déclaration using using var x = ...; (C# 8 et plus) n'a pas de bloc propre : elle libère l'objet quand la portée englobante se termine, en général à la fin de la méthode. Elle évite une imbrication quand une ressource doit vivre jusqu'à la fin de la méthode.
Dans quel ordre plusieurs objets using sont-ils libérés ?
Dans l'ordre inverse de leur création. Avec using (var a = ...) using (var b = ...) { }, b est libéré en premier, puis a. C'est l'ordre voulu quand b dépend de a, comme un StreamWriter qui enveloppe un FileStream.
Le ramasse-miettes appelle-t-il Dispose ?
Non. Le ramasse-miettes libère la mémoire, et il peut exécuter un finaliseur à un moment ultérieur imprévisible, mais il n'appelle jamais Dispose. Un fichier non libéré peut rester verrouillé, et ses données en mémoire tampon peuvent ne jamais être écrites. C'est pourquoi using existe.