Implémenter une méthode DisposeAsync
L’interface System.IAsyncDisposable a été introduite dans le cadre de C# 8.0. Vous implémentez la méthode IAsyncDisposable.DisposeAsync() quand vous devez effectuer le nettoyage des ressources, comme vous le feriez lors de l’implémentation d’une méthode de suppression. Une des principales différences est cependant que cette implémentation permet des opérations de nettoyage asynchrones. Le DisposeAsync() renvoie un ValueTask qui représente l’opération de suppression asynchrone.
Lors de l’implémentation de l’interface IAsyncDisposable, il est courant que les classes implémentent également l’interface IDisposable. Un bon modèle d’implémentation de l’interface IAsyncDisposable doit être prêt pour la suppression synchrone ou asynchrone ; mais cela n’est pas une obligation. Si aucune suppression synchrone de votre classe n’est possible, avoir seulement IAsyncDisposable est acceptable. Toutes les instructions relatives à l’implémentation du modèle de suppression s’appliquent également à l’implémentation asynchrone. Cet article suppose que vous êtes déjà familiarisé avec l’implémentation d’une méthode de suppression.
Attention
Si vous implémentez l’interface IAsyncDisposable, mais pas l’interface IDisposable, votre application peut potentiellement laisser fuiter des ressources. Si une classe implémente IAsyncDisposable mais pas IDisposable, et qu’un consommateur appelle seulement Dispose
, votre implémentation n’appelle jamais DisposeAsync
. Ceci entraînerait une fuite de ressources.
Conseil
En ce qui concerne l’injection de dépendances, lors de l’inscription de services dans un IServiceCollection, la durée de vie du service est gérée implicitement en votre nom. Le IServiceProvider et le IHost correspondant orchestrent le nettoyage des ressources. Plus précisément, les implémentations de IDisposable et IAsyncDisposable sont correctement supprimées à la fin de leur durée de vie spécifiée.
Pour plus d’informations, consultez Injection de dépendances dans .NET.
Explorer les méthodes DisposeAsync
et DisposeAsyncCore
L’interface IAsyncDisposable déclare une seule méthode sans paramètre, DisposeAsync(). Toute classe non scellée doit définir une méthode DisposeAsyncCore()
qui renvoie également un ValueTask.
Une implémentation IAsyncDisposable.DisposeAsync()
public
qui n’a pas de paramètres.Une méthode
protected virtual ValueTask DisposeAsyncCore()
dont la signature est :protected virtual ValueTask DisposeAsyncCore() { }
Méthode DisposeAsync
La méthode DisposeAsync()
sans paramètre public
est appelée implicitement dans une instruction await using
, et son objectif est de libérer des ressources non managées, d’effectuer un nettoyage général et d’indiquer que le finaliseur, le cas échéant, n’a pas besoin d’être exécuté. La libération de la mémoire associée à un objet managé est toujours du domaine du récupérateur de mémoire. De ce fait, son implémentation standard est la suivante :
public async ValueTask DisposeAsync()
{
// Perform async cleanup.
await DisposeAsyncCore();
// Dispose of unmanaged resources.
Dispose(false);
// Suppress finalization.
GC.SuppressFinalize(this);
}
Notes
Une différence principale dans le modèle de suppression asynchrone par rapport au modèle de suppression, est que l’appel depuis DisposeAsync() à la méthode surchargée Dispose(bool)
reçoit false
comme argument. Cependant, lors de l’implémentation de la méthode IDisposable.Dispose(), c’est true
qui est passé à la place. Ceci permet de garantir l’équivalence fonctionnelle avec le modèle de suppression synchrone et de garantir que les chemins de code du finaliseur sont néanmoins toujours appelés. En d’autres termes, la méthode DisposeAsyncCore()
va supprimer les ressources managées de façon asynchrone : vous ne voulez donc pas les supprimer aussi de façon synchrone. Par conséquent, appelez Dispose(false)
au lieu de Dispose(true)
.
Méthode DisposeAsyncCore
La méthode DisposeAsyncCore()
est destinée à effectuer le nettoyage asynchrone des ressources managées ou pour les appels en cascade à DisposeAsync()
. Elle encapsule les opérations de nettoyage asynchrones courantes quand une sous-classe hérite d’une classe de base qui est une implémentation de IAsyncDisposable. La méthode DisposeAsyncCore()
est virtual
: les classes dérivées peuvent donc définir un nettoyage personnalisé dans leurs surcharges.
Conseil
Si une implémentation de IAsyncDisposable est sealed
, la méthode DisposeAsyncCore()
n’est pas nécessaire, et le nettoyage asynchrone peut être effectué directement dans la méthode IAsyncDisposable.DisposeAsync().
Implémenter le modèle de supplémentaires asynchrone
Toutes les classes non scellées doivent être considérées comme une classe de base potentielle, car elles peuvent être héritées. Si vous implémentez le modèle de suppression asynchrone pour toute classe de base potentielle, vous devez fournir la méthode protected virtual ValueTask DisposeAsyncCore()
. Certains des exemples suivants utilisent une classe NoopAsyncDisposable
définie comme suit :
public sealed class NoopAsyncDisposable : IAsyncDisposable
{
ValueTask IAsyncDisposable.DisposeAsync() => ValueTask.CompletedTask;
}
Voici un exemple d’implémentation du modèle de suppression asynchrone qui utilise le type NoopAsyncDisposable
. Le type implémente en DisposeAsync
renvoyant ValueTask.CompletedTask.
public class ExampleAsyncDisposable : IAsyncDisposable
{
private IAsyncDisposable? _example;
public ExampleAsyncDisposable() =>
_example = new NoopAsyncDisposable();
public async ValueTask DisposeAsync()
{
await DisposeAsyncCore().ConfigureAwait(false);
GC.SuppressFinalize(this);
}
protected virtual async ValueTask DisposeAsyncCore()
{
if (_example is not null)
{
await _example.DisposeAsync().ConfigureAwait(false);
}
_example = null;
}
}
Dans l’exemple précédent :
- Le
ExampleAsyncDisposable
est une classe non scellée qui implémente l’interface IAsyncDisposable. - Il contient un champ privé
IAsyncDisposable
,_example
, qui est initialisé dans le constructeur. - La méthode
DisposeAsync
délègue à la méthodeDisposeAsyncCore
et appelle GC.SuppressFinalize pour avertir le récupérateur de mémoire que le finaliseur ne doit pas s’exécuter. - Elle contient une méthode
DisposeAsyncCore()
qui appelle la méthode_example.DisposeAsync()
et définit le champ surnull
. - La méthode
DisposeAsyncCore()
estvirtual
, ce qui permet aux sous-classes de la remplacer par un comportement personnalisé.
Autre modèle de suppression asynchrone scellé
Si votre classe d’implémentation peut être sealed
, vous pouvez implémenter le modèle de suppression asynchrone en surchargeant la méthode IAsyncDisposable.DisposeAsync(). L’exemple suivant montre comment implémenter le modèle de suppression asynchrone pour une classe scellée :
public sealed class SealedExampleAsyncDisposable : IAsyncDisposable
{
private readonly IAsyncDisposable _example;
public SealedExampleAsyncDisposable() =>
_example = new NoopAsyncDisposable();
public ValueTask DisposeAsync() => _example.DisposeAsync();
}
Dans l’exemple précédent :
SealedExampleAsyncDisposable
est une classe scellée qui implémente l’interface IAsyncDisposable.- Le champ
_example
conteneur estreadonly
et est initialisé dans le constructeur. - La méthode
DisposeAsync
appelle la méthode_example.DisposeAsync()
, implémentant le modèle via le champ conteneur (suppression en cascade).
Implémenter à la fois des modèles de suppression et de suppression asynchrone
Il peut être nécessaire d’implémenter à la fois les interfaces IDisposable et IAsyncDisposable, en particulier quand l’étendue de votre classe contient des instances de ces implémentations. Ceci garantit que vous pouvez correctement effectuer des appels de nettoyage en cascade. Voici un exemple de classe qui implémente les deux interfaces et illustre les instructions appropriées pour le nettoyage.
class ExampleConjunctiveDisposableusing : IDisposable, IAsyncDisposable
{
IDisposable? _disposableResource = new MemoryStream();
IAsyncDisposable? _asyncDisposableResource = new MemoryStream();
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
public async ValueTask DisposeAsync()
{
await DisposeAsyncCore().ConfigureAwait(false);
Dispose(disposing: false);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_disposableResource?.Dispose();
_disposableResource = null;
if (_asyncDisposableResource is IDisposable disposable)
{
disposable.Dispose();
_asyncDisposableResource = null;
}
}
}
protected virtual async ValueTask DisposeAsyncCore()
{
if (_asyncDisposableResource is not null)
{
await _asyncDisposableResource.DisposeAsync().ConfigureAwait(false);
}
if (_disposableResource is IAsyncDisposable disposable)
{
await disposable.DisposeAsync().ConfigureAwait(false);
}
else
{
_disposableResource?.Dispose();
}
_asyncDisposableResource = null;
_disposableResource = null;
}
}
Les implémentations de IDisposable.Dispose() et de IAsyncDisposable.DisposeAsync() sont toutes deux du code réutilisable simple.
Dans la méthode de surcharge Dispose(bool)
, l’instance IDisposable est supprimée de façon conditionnelle si elle n’est pas null
. L’instance IAsyncDisposable est castée en tant que IDisposable, et si elle n’est pas non plus null
, elle est également supprimée. Les deux instances sont ensuite affectées de la valeur null
.
Avec la méthode DisposeAsyncCore()
, la même approche logique est suivie. Si l’instance IAsyncDisposable n’est pas null
, son appel à DisposeAsync().ConfigureAwait(false)
est attendu. Si l’instance IDisposable est également une implémentation de IAsyncDisposable, elle est également supprimée de façon asynchrone. Les deux instances sont ensuite affectées de la valeur null
.
Chaque implémentation s’efforce de supprimer tous les objets jetables possibles. Cela garantit que le nettoyage est correctement mis en cascade .
Utilisation d’un supprimable asynchrone
Pour consommer correctement un objet qui implémente l’interface IAsyncDisposable, vous utilisez les mots clés await et using ensemble. Prenons l’exemple suivant, où la classe ExampleAsyncDisposable
est instanciée, puis encapsulée dans une instruction await using
.
class ExampleConfigureAwaitProgram
{
static async Task Main()
{
var exampleAsyncDisposable = new ExampleAsyncDisposable();
await using (exampleAsyncDisposable.ConfigureAwait(false))
{
// Interact with the exampleAsyncDisposable instance.
}
Console.ReadLine();
}
}
Important
Utilisez la méthode d’extension ConfigureAwait(IAsyncDisposable, Boolean) de l’interface IAsyncDisposable pour configurer la façon dont la continuation de la tâche est marshalée sur son contexte ou son planificateur d’origine. Pour plus d’informations sur ConfigureAwait
, consultez Questions fréquentes (FAQ) sur ConfigureAwait.
Pour les situations où l’utilisation de ConfigureAwait
n’est pas nécessaire, l’instruction await using
peut être simplifiée comme suit :
class ExampleUsingStatementProgram
{
static async Task Main()
{
await using (var exampleAsyncDisposable = new ExampleAsyncDisposable())
{
// Interact with the exampleAsyncDisposable instance.
}
Console.ReadLine();
}
}
En outre, elle peut être écrite de façon à utiliser l’étendue implicite d’une déclaration using.
class ExampleUsingDeclarationProgram
{
static async Task Main()
{
await using var exampleAsyncDisposable = new ExampleAsyncDisposable();
// Interact with the exampleAsyncDisposable instance.
Console.ReadLine();
}
}
Plusieurs mots clés await dans une même ligne
Parfois, le mot clé await
peut apparaître plusieurs fois dans une même ligne. Considérons par exemple le code suivant :
await using var transaction = await context.Database.BeginTransactionAsync(token);
Dans l’exemple précédent :
- La méthode BeginTransactionAsync est attendue.
- Le type de retour est DbTransaction, qui implémente
IAsyncDisposable
. transaction
est utilisée de façon asynchrone et également attendue.
Using empilés
Dans les situations où vous créez et où vous utilisez plusieurs objets qui implémentent IAsyncDisposable, il est possible que l’empilement d’instructions await using
avec ConfigureAwait puisse empêcher les appels à DisposeAsync() dans des conditions irrégulières. Pour garantir que DisposeAsync() est toujours appelé, vous devez éviter l’empilement. Les trois exemples de code suivants montrent des modèles acceptables à utiliser à la place.
Modèle acceptable 1
class ExampleOneProgram
{
static async Task Main()
{
var objOne = new ExampleAsyncDisposable();
await using (objOne.ConfigureAwait(false))
{
// Interact with the objOne instance.
var objTwo = new ExampleAsyncDisposable();
await using (objTwo.ConfigureAwait(false))
{
// Interact with the objOne and/or objTwo instance(s).
}
}
Console.ReadLine();
}
}
Dans l’exemple précédent, chaque opération de nettoyage asynchrone est explicitement délimitée sous le bloc await using
. L’étendue externe suit la façon dont objOne
définit ses accolades, englobant objTwo
, de sorte que objTwo
est supprimé en premier, suivi de objOne
. La méthode DisposeAsync() des deux instances IAsyncDisposable
étant attendue, chaque instance effectue son opération de nettoyage asynchrone. Les appels sont imbriqués et non pas empilés.
Modèle acceptable 2
class ExampleTwoProgram
{
static async Task Main()
{
var objOne = new ExampleAsyncDisposable();
await using (objOne.ConfigureAwait(false))
{
// Interact with the objOne instance.
}
var objTwo = new ExampleAsyncDisposable();
await using (objTwo.ConfigureAwait(false))
{
// Interact with the objTwo instance.
}
Console.ReadLine();
}
}
Dans l’exemple précédent, chaque opération de nettoyage asynchrone est explicitement délimitée sous le bloc await using
. À la fin de chaque bloc, la méthode DisposeAsync() de l’instance IAsyncDisposable
correspondante est attendue, effectuant ainsi son opération de nettoyage asynchrone. Les appels sont séquentiels et non pas empilés. Dans ce scénario, objOne
est supprimé en premier, puis objTwo
est supprimé.
Modèle acceptable 3
class ExampleThreeProgram
{
static async Task Main()
{
var objOne = new ExampleAsyncDisposable();
await using var ignored1 = objOne.ConfigureAwait(false);
var objTwo = new ExampleAsyncDisposable();
await using var ignored2 = objTwo.ConfigureAwait(false);
// Interact with objOne and/or objTwo instance(s).
Console.ReadLine();
}
}
Dans l’exemple précédent, chaque opération de nettoyage asynchrone est implicitement délimitée par le corps de méthode qu’elle contient. À la fin du bloc englobant, les instances IAsyncDisposable
effectuent leurs opérations de nettoyage asynchrones. Cet exemple s’exécute dans l’ordre inverse de celui dont ils ont été déclarés, ce qui signifie que objTwo
est supprimé avant objOne
.
Modèle non acceptable
Les lignes mises en surbrillance dans le code suivant montrent ce que cela signifie d’avoir des « usings empilés ». Si une exception est levée depuis le constructeur AnotherAsyncDisposable
, aucun objet n’est correctement supprimé. La variable objTwo
n’est jamais affectée, car le constructeur ne s’est pas terminé correctement. Par conséquent, le constructeur pour AnotherAsyncDisposable
est responsable de l’élimination des ressources allouées avant de lever une exception. Si le type ExampleAsyncDisposable
a un finaliseur, il est éligible pour la finalisation.
class DoNotDoThisProgram
{
static async Task Main()
{
var objOne = new ExampleAsyncDisposable();
// Exception thrown on .ctor
var objTwo = new AnotherAsyncDisposable();
await using (objOne.ConfigureAwait(false))
await using (objTwo.ConfigureAwait(false))
{
// Neither object has its DisposeAsync called.
}
Console.ReadLine();
}
}
Conseil
Évitez ce modèle, car il pourrait entraîner un comportement inattendu. Si vous utilisez un des modèles acceptables, le problème des objets non supprimés n’existe pas. Les opérations de nettoyage sont effectuées correctement quand les instructions using
ne sont pas empilées.
Voir aussi
Pour obtenir un exemple d’implémentation double de IDisposable
et de IAsyncDisposable
, consultez le code source de Utf8JsonWritersur GitHub.