Хороше рішення для await у try / catch / нарешті?


93

Мені потрібно викликати asyncметод у catchблоці, перш ніж знову викидати виняток (з його трасуванням стека), як це:

try
{
    // Do something
}
catch
{
    // <- Clean things here with async methods
    throw;
}

Але, на жаль, ви не можете використовувати awaitа catchчи finallyблок. Я дізнався, що це тому, що компілятор не має способу повернутися назад у catchблок, щоб виконати те, що є після вашої awaitінструкції або щось подібне ...

Я намагався використати Task.Wait()для заміни, awaitі я отримав глухий кут. Я шукав в Інтернеті, як мені цього уникнути, і знайшов цей сайт .

Оскільки я не можу змінити asyncметоди, і я не знаю, чи використовують вони ConfigureAwait(false), я створив ці методи, які беруть Func<Task>метод, який запускає асинхронний метод, коли ми потрапляємо в інший потік (щоб уникнути тупикової ситуації), і чекає його завершення:

public static void AwaitTaskSync(Func<Task> action)
{
    Task.Run(async () => await action().ConfigureAwait(false)).Wait();
}

public static TResult AwaitTaskSync<TResult>(Func<Task<TResult>> action)
{
    return Task.Run(async () => await action().ConfigureAwait(false)).Result;
}

public static void AwaitSync(Func<IAsyncAction> action)
{
    AwaitTaskSync(() => action().AsTask());
}

public static TResult AwaitSync<TResult>(Func<IAsyncOperation<TResult>> action)
{
    return AwaitTaskSync(() => action().AsTask());
}

Тож мої запитання: Чи вважаєте ви, що цей код нормальний?

Звичайно, якщо у вас є якісь вдосконалення або ви знаєте кращий підхід, я слухаю! :)


2
Використання awaitв блоці catch фактично дозволено з C # 6.0 (див. Мою відповідь нижче)
Adi Lester

3
Пов’язані повідомлення про помилки C # 5.0 : CS1985 : Не можна чекати в тілі пропозиції catch. CS1984 : Не можу дочекатися в тілі остаточної статті.
DavidRR

Відповіді:


173

Ви можете перемістити логіку за межі catchблоку та відновити виняток після, якщо потрібно, за допомогою ExceptionDispatchInfo.

static async Task f()
{
    ExceptionDispatchInfo capturedException = null;
    try
    {
        await TaskThatFails();
    }
    catch (MyException ex)
    {
        capturedException = ExceptionDispatchInfo.Capture(ex);
    }

    if (capturedException != null)
    {
        await ExceptionHandler();

        capturedException.Throw();
    }
}

Таким чином, коли абонент перевіряє StackTraceвластивість винятку , він все одно фіксує, куди TaskThatFailsвоно було кинуте.


1
У чому перевага збереження ExceptionDispatchInfoзамість Exception(як у відповіді Стівена Клірі)?
Варвара Калініна

2
Я можу здогадатися, що якщо ви вирішите повернути Exception, ви втратите все попереднє StackTrace?
Варвара Калініна

1
@VarvaraKalinina Рівно.

54

Ви повинні знати, що, починаючи з C # 6.0, його можна використовувати awaitв catchта finallyблоках, тому ви можете насправді зробити це:

try
{
    // Do something
}
catch (Exception ex)
{
    await DoCleanupAsync();
    throw;
}

Нова C # 6.0 особливості, в тому числі той , який я тільки що згадав , перераховані тут , або як відео тут .


Підтримка блоків await у catch / нарешті в C # 6.0 також вказана у Вікіпедії.
DavidRR

4
@DavidRR. Вікіпедія не є авторитетною. Це просто ще один веб-сайт серед мільйонів, що стосується цього.
user34660

Хоча це справедливо для C # 6, питання було позначене як C # 5 з самого початку. Це змушує мене задуматись, чи не має ця відповідь заплутаність, чи нам просто не видалити тег конкретної версії у цих випадках.
julealgon

16

Якщо вам потрібно скористатися asyncобробниками помилок, я рекомендую щось подібне:

Exception exception = null;
try
{
  ...
}
catch (Exception ex)
{
  exception = ex;
}

if (exception != null)
{
  ...
}

Проблема синхронної блокування asyncкоду (незалежно від того, на якому потоці він запущений) полягає в тому, що ви синхронно блокуєте. У більшості сценаріїв краще використовувати await.

Оновлення: Оскільки вам потрібно переробити, ви можете використовувати ExceptionDispatchInfo.


1
Дякую, але, на жаль, я вже знаю цей метод. Це те, що я роблю зазвичай, але я не можу цього робити тут. Якщо я просто використовую throw exception;в ifзаяві, трасування стека буде втрачено.
user2397050

3

Ми витягли чудову відповідь hvd на наступний клас корисних програм багаторазового використання у нашому проекті:

public static class TryWithAwaitInCatch
{
    public static async Task ExecuteAndHandleErrorAsync(Func<Task> actionAsync,
        Func<Exception, Task<bool>> errorHandlerAsync)
    {
        ExceptionDispatchInfo capturedException = null;
        try
        {
            await actionAsync().ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            capturedException = ExceptionDispatchInfo.Capture(ex);
        }

        if (capturedException != null)
        {
            bool needsThrow = await errorHandlerAsync(capturedException.SourceException).ConfigureAwait(false);
            if (needsThrow)
            {
                capturedException.Throw();
            }
        }
    }
}

Можна було б використовувати його наступним чином:

    public async Task OnDoSomething()
    {
        await TryWithAwaitInCatch.ExecuteAndHandleErrorAsync(
            async () => await DoSomethingAsync(),
            async (ex) => { await ShowMessageAsync("Error: " + ex.Message); return false; }
        );
    }

Не соромтеся вдосконалювати імена, ми дотримувались навмисно багатослівно. Зауважте, що немає необхідності фіксувати контекст всередині обгортки, оскільки він уже захоплений на сайті виклику, отже ConfigureAwait(false).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.