puede - spreadsheetlight c#
¿Sería útil un método de extensión Task<T>.Convert<TResult> o tiene peligros ocultos? (1)
Transformar el resultado de esperar termina siendo molesto en términos de precedencia.
Por lo general, prefiero introducir una var local, pero como notó, eso evita los métodos con expresión.
Ocasionalmente, olvidamos
ConfigureAwait(false)
- esto es solucionable con herramientas hasta cierto punto.
Ya que está trabajando en una biblioteca y debería usar ConfigureAwait(false)
todas partes, puede valer la pena usar un analizador de código que imponga el uso de ConfigureAwait
. Hay un complemento ReSharper y un complemento VS que hacen esto. Aunque no los he probado yo mismo.
Task<TResult>.ContinueWith
suena como una buena idea, pero leí la publicación del blog de Stephen Cleary que la recomendaba, y los motivos parecen ser sólidos.
Si usó ContinueWith
, tendría que especificar explícitamente TaskScheduler.Default
(este es el equivalente de ContinueWith
ConfigureAwait(false)
), y también considerar agregar indicadores como DenyChildAttach
. En mi opinión, es más difícil recordar cómo usar ContinueWith
correctamente que recordar ConfigureAwait(false)
.
Por otro lado, si bien ContinueWith
es un método peligroso de bajo nivel, si lo usa correctamente, puede brindarle mejoras menores en el rendimiento. En particular, el uso del parámetro de state
puede ahorrarle una asignación de delegado. Este es el enfoque comúnmente adoptado por el TPL y otras bibliotecas de Microsoft, pero, en mi opinión, reduce la capacidad de mantenimiento demasiado para la mayoría de las bibliotecas.
Parece tan simple y útil que me sorprende un poco que no haya algo disponible.
El método de conversión que sugieres ha existido informalmente como Then
. Stephen no lo dice, pero supongo que el nombre Then
es del mundo de JavaScript , donde las promesas son equivalentes a la tarea (ambos son Futures ).
En una nota lateral, la publicación del blog de Stephen lleva este concepto a una conclusión interesante. Convert
/ Then
es el bind
para la mónada del futuro , por lo que puede utilizarse para implementar LINQ sobre futuros. Stephen Toub también ha publicado un código para esto (bastante fechado en este punto, pero interesante).
He pensado varias veces acerca de agregar Then
a mi biblioteca AsyncEx, pero cada vez no se realizó el corte porque es casi lo mismo que await
. Su único beneficio es resolver el problema de precedencia al permitir el encadenamiento de métodos. Supongo que no existe en el marco por la misma razón.
Dicho esto, ciertamente no hay nada de malo en implementar tu propio método Convert
. Si lo hace, evitará la variable local entre paréntesis / extra y permitirá métodos con cuerpo de expresión.
Soy consciente de que esto cambia el momento de la validación.
Esta es una de las razones por las que desconfío de dejar de lado async
/ await (mi publicación en el blog tiene más razones).
En este caso, creo que está bien de cualquier manera, ya que el "breve trabajo sincrónico para configurar una solicitud" es una verificación de las condiciones previas, y en mi opinión no importa dónde se lanzan las excepciones de cabeza de cabeza (porque de todos modos no deberían detectarse) .
Si el "trabajo sincrónico breve" fuera más complejo, si fuera algo que pudiera lanzar, o que pudiera lanzarse razonablemente después de que alguien lo haya refactado dentro de un año, entonces usaría async
/ await
. Aún podría usar Convert
para evitar el problema de precedencia:
public async Task<TranslationResult> TranslateTextAsync(string text, string targetLanguage) =>
await TranslateTextAsync(SomthingThatCanThrow(text), targetLanguage)
.Convert(results => results[0])
.ConfigureAwait(false);
Estoy escribiendo bibliotecas de clientes para las API de Google Cloud, que tienen un patrón bastante común para las sobrecargas de ayuda asíncronas:
- Haga un breve trabajo síncrono para configurar una solicitud
- Hacer una solicitud asíncrona.
- Transforma el resultado de forma sencilla.
Actualmente estamos usando métodos asíncronos para eso, pero:
- Transformar el resultado de aguardar termina siendo molesto en términos de precedencia: terminamos necesitando
(await foo.Bar().ConfigureAwait(false)).TransformToBaz()
y los paréntesis son molestos. El uso de dos afirmaciones mejora la legibilidad, pero significa que no podemos usar un método con expresión. - Ocasionalmente, olvidamos
ConfigureAwait(false)
: esto se puede solucionar con herramientas hasta cierto punto, pero todavía es un poco de olor.
Task<TResult>.ContinueWith
suena como una buena idea, pero leí la publicación del blog de Stephen Cleary que la recomendaba, y los motivos parecen ser sólidos. Estamos considerando agregar un método de extensión para la Task<T>
como este:
Método de extensión de potencial
public static async Task<TResult> Convert<TSource, TResult>(
this Task<TSource> task, Func<TSource, TResult> projection)
{
var result = await task.ConfigureAwait(false);
return projection(result);
}
Entonces podemos llamar esto desde un método síncrono de manera simple, por ejemplo,
public async Task<Bar> BarAsync()
{
var fooRequest = BuildFooRequest();
return FooAsync(fooRequest).Convert(foo => new Bar(foo));
}
o incluso:
public Task<Bar> BarAsync() =>
FooAsync(BuildFooRequest()).Convert(foo => new Bar(foo));
Parece tan simple y útil que me sorprende un poco que no haya algo disponible.
Como ejemplo de dónde usaría esto para hacer que un método con expresión expresada funcione, en el código Google.Cloud.Translation.V2
tengo dos métodos para traducir texto sin formato: uno toma una sola cadena y otra toma varias cadenas. Las tres opciones para la versión de una sola cadena son (simplificadas en términos de parámetros):
Método asíncrono regular
public async Task<TranslationResult> TranslateTextAsync(
string text, string targetLanguage)
{
GaxPreconditions.CheckNotNull(text, nameof(text));
var results = await TranslateTextAsync(new[] { text }, targetLanguage).ConfigureAwait(false);
return results[0];
}
Método asíncrono expresivo
public async Task<TranslationResult> TranslateTextAsync(
string text, string targetLanguage) =>
(await TranslateTextAsync(new[] { GaxPreconditions.CheckNotNull(text, nameof(text)) }, targetLanguage)
.ConfigureAwait(false))[0];
Método de sincronización con expresión utilizando Convert
public Task<TranslationResult> TranslateTextAsync(
string text, string targetLanguage) =>
TranslateTextAsync(new[] { GaxPreconditions.CheckNotNull(text, nameof(text)) }, targetLanguage)
.Convert(results => results[0]);
Personalmente prefiero el último de estos.
Soy consciente de que esto cambia el tiempo de la validación: en el ejemplo final, pasar un valor null
para el text
arrojará inmediatamente una ArgumentNullException
mientras que pasar un valor null
para targetLanguage
devolverá una tarea con errores (porque TranslateTextAsync
fallará de forma asíncrona). Esa es una diferencia que estoy dispuesto a aceptar.
¿Hay diferencias en la programación o el rendimiento que debo tener en cuenta? (Todavía estamos construyendo dos máquinas de estado, porque el método Convert
creará una. El uso de Task.ContineWith
lo evitaría, pero tiene todos los problemas mencionados en la publicación del blog. El método Convert
podría potencialmente cambiarse para usar ContinueWith
cuidado).
(Tengo la tentación de publicar esto en CodeReview, pero sospecho que la información en las respuestas será más útil en general más allá de si esto es específicamente una buena idea. Si otros no están de acuerdo, me complace moverlo).