Подтвердить что ты не робот

Будет ли полезен метод расширения Task <T>.Convert <TResult> или он имеет скрытые опасности?

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

4b9b3361

Ответ 1

Преобразование результата ожидания заканчивается раздражающим с точки зрения приоритета

Обычно я предпочитаю вводить локальный var, но, как вы отметили, это предотвращает методы, основанные на выражении.

Мы иногда забываем ConfigureAwait(false) - это разрешимо с инструментами в некоторой степени

Поскольку вы работаете над библиотекой и должны использовать ConfigureAwait(false) везде, возможно, стоит использовать анализатор кода, который обеспечивает ConfigureAwait. Там плагин ReSharper и неформально Then. Стивен не говорит об этом, но я предполагаю, что имя Then является из JavaScript world, где promises эквивалент задачи (они оба Futures).

С другой стороны, сообщение в блоге Стивена воспринимает эту концепцию как интересную вывод. Convert/Then является bind для будущей монады, поэтому он может использоваться для реализации LINQ-over-futures. Стивен Туб также опубликованный код для этого (скорее устаревший на данный момент, но интересный).

Я несколько раз думал о добавлении Then в мою библиотеку AsyncEx, но каждый раз, когда он не делал разреза, потому что он почти такой же как только await. Его единственное преимущество - решить проблему приоритета, разрешив цепочку методов. Я предполагаю, что этого не существует в рамках та же причина.

Тем не менее, нет ничего плохого в реализации собственных Convert. Это позволит избежать скобок/дополнительных локальных переменную и разрешить методы с выражением.

Я знаю, что это изменяет время проверки

Это одна из причин, по которым я опасаюсь eliding async/await (мое сообщение в блоге объясняется многими причинами).

В этом случае я думаю, что это хорошо в любом случае, поскольку "краткая синхронная работа по настройке запроса" является проверкой предварительных условий, а IMO не имеет значения, где изгоняемые исключения выбрасываются (потому что они не должны быть пойманы в любом случае).

Если "краткая синхронная работа" была более сложной - если бы это было что-то, что могло бы бросить или могло бы разумно бросить после того, как кто-то реорганизовал ее через год, тогда я бы использовал async/await. Вы все равно можете использовать Convert, чтобы избежать проблемы с приоритетом:

public async Task<TranslationResult> TranslateTextAsync(string text, string targetLanguage) =>
  await TranslateTextAsync(SomthingThatCanThrow(text), targetLanguage)
  .Convert(results => results[0])
  .ConfigureAwait(false);