Я пишу клиентские библиотеки для облачных 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, но я подозреваю, что информация в ответах будет в целом полезной, кроме того, является ли это особенно хорошей идеей. Если другие не согласны, я рад ее перенести.)