Хорошее решение для ожидания в try / catch / finally?


94

Мне нужно вызвать 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 (см. Мой ответ ниже)
Ади Лестер,

3
Связанные сообщения об ошибках C # 5.0 : CS1985 : невозможно ожидать в теле предложения catch. CS1984 : не может ждать в теле предложения finally.
DavidRR

Ответы:


174

Вы можете переместить логику за пределы 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 / finally в 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.