
Estamos acostumbrados a usar el operador await
de C# para esperar la finalización de tareas asíncronas representadas por objetos de tipo Task
, Task<T>
, ValueTask
o ValueTask<T>
, pero, ¿sabíais que en realidad podemos usarlo con cualquier tipo de objeto?.
En este artículo vamos a ver que es bastante sencillo, y lo ilustraremos con un ejemplo muy simple: cómo esperar un segundo usando la expresión await 1000
o await TimeSpan.FromSeconds(1)
en lugar del clásico await Task.Delay(1000)
.
¿Podemos awaitear cualquier objeto en .NET?
Pues sí, podemos utilizar await
con cualquier tipo de datos, siempre que éste implemente el método GetAwaiter()
de forna directa o mediante extensores. El objeto retornado por este método es donde realmente se lleva a cabo la magia de la ejecución asíncrona, y debe cumplir los siguientes requisitos:
- Implementar la interfaz
INotifyCompletion
(o alguna interfaz descendiente, comoICriticalNotifyCompletion
). Esto obliga a que, como mínimo, el objeto tenga un métodoOnCompleted()
que reciba unAction
como parámetro, que será el que se ejecute cuando la operación asíncrona haya terminado. - Tener un método
GetResult()
que devuelva el resultado de la operación asíncrona (ovoid
si se trata de una tarea sin valor de retorno). - Tener una propiedad
IsCompleted
que valgatrue
si la operación asíncrona ha finalizado.
Fijaos que se trata de cumplir una interfaz implícita. No es necesario que el objeto retornado implemente la interfaz IAwaitable
o algo similar, simplemente que disponga de un método GetAwaiter()
que devuelva un objeto que cumpla con los requisitos. Si no es así, se generará un error en tiempo de compilación.
La implementación de objetos awaiters personalizados puede llegar a ser compleja, pero el framework ya nos proporciona clases que cumplen estos requisitos, como TaskAwaiter
o ValueTaskAwaiter
, que son los usados en los tipos que habitualmente utilizamos para operaciones asíncronas, como Task
o ValueTask
.
Por tanto, podemos aprovecharnos de esto para crear fácilmente clases cuyos objetos serán awaitables, como en el siguiente ejemplo:
public class Delayer
{
private readonly int _milliseconds;
public Delayer(int milliseconds)
{
_milliseconds = milliseconds;
}
public TaskAwaiter GetAwaiter()
{
return Task.Delay(_milliseconds).GetAwaiter();
}
}
Como podéis ver, no nos estamos complicando más de la cuenta: la clase implementa el método GetAwaiter()
, pero en él retornamos simplemente el resultado de invocar al método GetAwaiter()
de un objeto Task
que se crea con Task.Delay(_milliseconds)
.
En la práctica, esto es suficiente para poder utilizar await
con sus instancias:
var delayer = new Delayer(1000);
Console.WriteLine("Hello, world 1!");
await delayer; // Espera 1 segundo
Console.WriteLine("Hello, world 2!");
await new Delayer(2000); // Espera otros 2 segundos
Console.WriteLine("Hello, world 3!");
Pero ahora viene lo mejor: como adelantamos algo más arriba, GetAwaiter()
puede ser también implementado como método extensor, por lo que no es necesario modificar la clase original. Esto nos permite virtualmente hacer un await
sobre cualquier tipo de objeto, como vemos en el siguiente ejemplo, donde implementamos la extensión para el tipo int
:
public static class IntExtensions
{
public static TaskAwaiter GetAwaiter(this int value)
{
return Task.Delay(value).GetAwaiter();
}
}
Hecho esto, ya seremos capaces de utilizar esta forma tan concisa para realizar esperas asíncronas:
await 1000; // Espera 1 segundo
var x = 2000;
await x; // Espera 2 segundos
¿Y si queremos esperar un tiempo determinado en lugar de un número de milisegundos? Pues también es posible, simplemente creando una extensión para el tipo TimeSpan
:
public static class TimeSpanExtensions
{
public static TaskAwaiter GetAwaiter(this TimeSpan value)
{
return Task.Delay(value).GetAwaiter();
}
}
Así, ya podemos esperar el tiempo deseado usando directamente un TimeSpan
:
await TimeSpan.FromHours(1); // Espera 1 hora
¡Misión cumplida! Ojo, no es que me parezca especialmente apropiado hacer algo así por aquello del Principio de la Mínima Sorpresa. Sin embargo, siempre es interesante saber que se puede hacer, más que nada porque nos llevará a entender mejor cómo funcionan algunos aspectos de C# y .NET que normalmente no manejamos en nuestro día a día.
Publicado en Variable not found.
Aún no hay comentarios, ¡sé el primero!
Enviar un nuevo comentario