Implementación de un método DisposeAsync
La interfaz System.IAsyncDisposable se presentó como parte de C# 8.0. El método IAsyncDisposable.DisposeAsync() se implementa cuando se necesita realizar una limpieza de recursos, tal como se haría a la hora de implementar un método Dispose. Sin embargo, una de las principales diferencias es que esta implementación permite operaciones de limpieza asincrónicas. El elemento DisposeAsync() devuelve un elemento ValueTask que representa la operación de eliminación asincrónica.
Es habitual que, al implementar la interfaz IAsyncDisposable, las clases también implementen la interfaz IDisposable. Se debe preparar un patrón de implementación correcto de la interfaz IAsyncDisposable para la eliminación sincrónica o asincrónica. Todas las instrucciones para implementar el patrón de eliminación se aplican también a la implementación asincrónica. En este artículo se supone que ya se ha familiarizado con el modo de implementar un método Dispose.
DisposeAsync() y DisposeAsyncCore()
La interfaz IAsyncDisposable declara un único método sin parámetros: DisposeAsync(). Cualquier clase no sellada debe tener un método DisposeAsyncCore() adicional que también devuelva un elemento ValueTask.
Una implementación de IAsyncDisposable.DisposeAsync()
publicque no tenga parámetros.Un método
protected virtual ValueTask DisposeAsyncCore()cuya signatura sea:protected virtual ValueTask DisposeAsyncCore() { }
El método DisposeAsync()
Se llama de forma implícita al método DisposeAsync() sin parámetros public en una instrucción await using, y su propósito consiste en liberar los recursos no administrados, realizar una limpieza general e indicar que el finalizador, si existe, no necesita ejecutarse. La liberación de la memoria asociada a un objeto administrado siempre corresponde al recolector de elementos no utilizados. Debido a esto, se realiza una implementación estándar:
public async ValueTask DisposeAsync()
{
// Perform async cleanup.
await DisposeAsyncCore();
// Dispose of unmanaged resources.
Dispose(false);
// Suppress finalization.
GC.SuppressFinalize(this);
}
Nota
Una diferencia principal en el patrón de eliminación asincrónica en comparación con el patrón de eliminación es que la llamada desde DisposeAsync() al método de sobrecarga Dispose(bool) recibe el valor false como argumento. Pero al implementar el método IDisposable.Dispose(), en su lugar se pasa el valor true. Esto ayuda a garantizar la equivalencia funcional con el patrón de eliminación sincrónico y garantiza aún más que se invoquen las rutas de acceso al código finalizador. En otras palabras, el método DisposeAsyncCore() eliminará los recursos administrados de forma asincrónica, por lo que no querrá eliminarlos también de forma sincrónica. Por tanto, llame a Dispose(false) en lugar de a Dispose(true).
Método DisposeAsyncCore()
El método DisposeAsyncCore() está diseñado para realizar la limpieza asincrónica de los recursos administrados o para hacer llamadas en cascada a DisposeAsync(). Encapsula las operaciones de limpieza asincrónica comunes cuando una subclase hereda una clase base que es una implementación de IAsyncDisposable. El método DisposeAsyncCore() es virtual para que las clases derivadas puedan definir la limpieza adicional en sus invalidaciones.
Sugerencia
Si una implementación de IAsyncDisposable es sealed, el método DisposeAsyncCore() no es necesario y la limpieza asincrónica se puede realizar directamente en el método IAsyncDisposable.DisposeAsync().
Implementación del patrón de eliminación asincrónica
Todas las clases no selladas deben considerarse una clase base potencial, ya que se podrían heredar. Cuando se implementa el patrón de eliminación asincrónica para cualquier clase base potencial, se debe proporcionar el método protected virtual ValueTask DisposeAsyncCore(). Este es un ejemplo de implementación del patrón de eliminación asincrónica que usa un elemento System.Text.Json.Utf8JsonWriter.
using System;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
public class ExampleAsyncDisposable : IAsyncDisposable, IDisposable
{
private Utf8JsonWriter _jsonWriter = new Utf8JsonWriter(new MemoryStream());
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
public async ValueTask DisposeAsync()
{
await DisposeAsyncCore();
Dispose(disposing: false);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_jsonWriter?.Dispose();
}
_jsonWriter = null;
}
protected virtual async ValueTask DisposeAsyncCore()
{
if (_jsonWriter is not null)
{
await _jsonWriter.DisposeAsync().ConfigureAwait(false);
}
_jsonWriter = null;
}
}
En el ejemplo anterior se usa Utf8JsonWriter. Para obtener más información sobre System.Text.Json, vea Migración desde Newtonsoft.json a System.Text.Json.
Implementación de patrones de eliminación y eliminación asincrónica
Es posible que tenga que implementar las interfaces IDisposable y IAsyncDisposable, especialmente si el ámbito de clase contiene instancias de estas implementaciones. De este modo se asegura de poder limpiar correctamente las llamadas en cascada. A continuación se incluye una clase de ejemplo que implementa ambas interfaces y muestra las instrucciones adecuadas para la limpieza.
using System;
using System.IO;
using System.Threading.Tasks;
namespace Samples
{
public class CustomDisposable : 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();
Dispose(disposing: false);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_disposableResource?.Dispose();
(_asyncDisposableResource as IDisposable)?.Dispose();
}
_disposableResource = null;
_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;
}
}
}
Las implementaciones de IDisposable.Dispose() y IAsyncDisposable.DisposeAsync() se llevan a cabo mediante código reutilizable sencillo.
En el método de sobrecarga Dispose(bool), la instancia de IDisposable se elimina condicionalmente si no es null. La instancia de IAsyncDisposable se convierte a IDisposable y, si tampoco es null, también se elimina. Después, se asignan ambas instancias a null.
Con el método DisposeAsyncCore(), se sigue el mismo enfoque lógico. Si la instancia de IAsyncDisposable no es null, se espera a la llamada a DisposeAsync().ConfigureAwait(false). Si la instancia de IDisposable también es una implementación de IAsyncDisposable, entonces, también se elimina de forma asincrónica. Después, se asignan ambas instancias a null.
Uso de la eliminación asincrónica
Para usar correctamente un objeto que implementa la interfaz IAsyncDisposable, utilice las palabras clave await y using juntas. Tenga en cuenta el ejemplo siguiente, donde se crea una instancia de la clase ExampleAsyncDisposable y después se encapsula en una instrucción await using.
using System;
using System.Threading.Tasks;
class ExampleConfigureAwaitProgram
{
static async Task Main()
{
var exampleAsyncDisposable = new ExampleAsyncDisposable();
await using (exampleAsyncDisposable.ConfigureAwait(false))
{
// Interact with the exampleAsyncDisposable instance.
}
Console.ReadLine();
}
}
Importante
Use el método de extensión ConfigureAwait(IAsyncDisposable, Boolean) de la interfaz IAsyncDisposable para configurar el modo en que se serializa la continuación de la tarea en su contexto o programador original. Para obtener más información sobre ConfigureAwait, consulte las preguntas más frecuentes sobre ConfigureAwait.
En situaciones en las que no se necesita el uso de ConfigureAwait, la instrucción await using se podría simplificar de la siguiente manera:
using System;
using System.Threading.Tasks;
class ExampleProgram
{
static async Task Main()
{
await using (var exampleAsyncDisposable = new ExampleAsyncDisposable())
{
// Interact with the exampleAsyncDisposable instance.
}
Console.ReadLine();
}
}
Además, se podría escribir para utilizar el ámbito implícito de una declaración "using".
using System;
using System.Threading.Tasks;
class ExampleUsingDeclarationProgram
{
static async Task Main()
{
await using var exampleAsyncDisposable = new ExampleAsyncDisposable();
// Interact with the exampleAsyncDisposable instance.
Console.ReadLine();
}
}
Declaraciones "using" apiladas
En situaciones en las que se crean y se usan varios objetos que implementan IAsyncDisposable, es posible que el apilamiento de instrucciones await using con ConfigureAwait en condiciones errantes pueda impedir las llamadas a DisposeAsync(). Para asegurarse de que siempre se llama a DisposeAsync(), debe evitar el apilamiento. En los tres ejemplos de código siguientes se muestran patrones aceptables para usar en su lugar.
Patrón aceptable 1
using System;
using System.Threading.Tasks;
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();
}
}
En el ejemplo anterior, cada operación de limpieza asincrónica tiene un ámbito explícito en el bloque await using. El ámbito externo se define en la forma en la que objOne establece sus llaves y se encierra objTwo; por tanto, primero se elimina objTwo, seguido de objOne. Las dos instancias de IAsyncDisposable tienen métodos DisposeAsync() en espera, con lo que se realiza su operación de limpieza asincrónica. Las llamadas están anidadas, no apiladas.
Patrón aceptable 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();
}
}
En el ejemplo anterior, cada operación de limpieza asincrónica tiene un ámbito explícito en el bloque await using. Al final de cada bloque, la instancia de IAsyncDisposable correspondiente tiene su método DisposeAsync() en espera, con lo que se realiza su operación de limpieza asincrónica. Las llamadas son secuenciales, no apiladas. En este escenario, primero se elimina objOne y, luego, objTwo.
Patrón aceptable 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();
}
}
En el ejemplo anterior, cada operación de limpieza asincrónica tiene un ámbito implícito en el cuerpo del método contenedor. Al final del bloque contenedor, las instancias de IAsyncDisposable realizan sus operaciones de limpieza asincrónicas. Esto se ejecuta en orden inverso en el que se han declarado, lo que significa que objTwo se elimina antes que objOne.
Patrón no aceptable
Si se inicia una excepción desde el constructor AnotherAsyncDisposable, objOne no se elimina correctamente:
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();
}
}
Sugerencia
Evite este patrón, ya que podría provocar un comportamiento inesperado.
Consulte también
Para obtener un ejemplo de implementación dual de IDisposable y IAsyncDisposable, vea el código fuente de Utf8JsonWriter en GitHub.