Хорошее решение для ожидания в 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());
}

Итак, мои вопросы: как вы думаете, этот код в порядке?

Конечно, если у вас есть улучшения или вы знаете лучший подход, я слушаю! :)

user2397050
источник
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).

mrts
источник