Я пишу клиентские библиотеки для облачных API Google, которые имеют довольно распространенный шаблон для асинхронных перегрузок:
- Сделайте короткую синхронную работу по настройке запроса
- Сделать асинхронный запрос
- Преобразование результата простым способом
В настоящее время мы используем для этого методы async, но:
- Преобразование результата ожидания заканчивается раздражающим с точки зрения приоритета - мы нуждаемся в
(await foo.Bar().ConfigureAwait(false)).TransformToBaz(), и скобки раздражают. Использование двух операторов улучшает читаемость, но означает, что мы не можем использовать метод с выражением. - Мы иногда забываем
ConfigureAwait(false)- это в какой-то мере разрешимо с инструментами, но это все еще немного запах.
Task<TResult>.ContinueWith звучит неплохо, но я прочитал Сообщение в блоге Стивена Клири, рекомендующее против него, и причины кажутся звуковыми. Мы рассматриваем возможность добавления метода расширения для Task<T> следующим образом:
Метод потенциального расширения
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);
}
Мы можем называть это по синхронному методу действительно просто, например.
public async Task<Bar> BarAsync()
{
var fooRequest = BuildFooRequest();
return FooAsync(fooRequest).Convert(foo => new Bar(foo));
}
или даже:
public Task<Bar> BarAsync() =>
FooAsync(BuildFooRequest()).Convert(foo => new Bar(foo));
Кажется, это так просто и полезно, что я немного удивлен, что нет уже доступного.
В качестве примера, где я использовал бы это, чтобы сделать работу с выражением тела, в коде Google.Cloud.Translation.V2 у меня есть два метода для перевода простого текста: один берет одну строку и один берет несколько строк. Три варианта однострочной версии (несколько упрощены с точки зрения параметров):
Обычный асинхронный метод
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];
}
Асинхронный метод с выражением
public async Task<TranslationResult> TranslateTextAsync(
string text, string targetLanguage) =>
(await TranslateTextAsync(new[] { GaxPreconditions.CheckNotNull(text, nameof(text) }, targetLanguage)
.ConfigureAwait(false))[0];
Экспрессионный метод синхронизации с использованием Convert
public Task<TranslationResult> TranslateTextAsync(
string text, string targetLanguage) =>
TranslateTextAsync(new[] { GaxPreconditions.CheckNotNull(text, nameof(text) }, targetLanguage)
.Convert(results => results[0]);
Я лично предпочитаю последнее из них.
Я знаю, что это изменяет время проверки - в последнем примере передача значения null для text будет немедленно ArgumentNullException, тогда как передача значения null для targetLanguage вернет (поскольку TranslateTextAsync будет асинхронно работать). Это различие, которое я готов принять.
Существуют ли различия в планировании или производительности, о которых я должен знать? (Мы все еще строим две машины состояний, потому что метод Convert будет создавать один. Использование Task.ContineWith позволит избежать этого, но имеет все проблемы, упомянутые в сообщении в блоге. Метод Convert потенциально может быть изменен для использования ContinueWith осторожно.)
(Я несколько соблазн опубликовать это на CodeReview, но я подозреваю, что информация в ответах будет в целом полезной, кроме того, является ли это особенно хорошей идеей. Если другие не согласны, я рад ее перенести.)