Solusi yang baik untuk menunggu di coba / tangkap / akhirnya?

94

Saya perlu memanggil asyncmetode dalam satu catchblok sebelum melemparkan lagi pengecualian (dengan jejak tumpukannya) seperti ini:

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

Tapi sayangnya Anda tidak dapat menggunakan awaitdalam catchatau finallyblok. Saya mempelajarinya karena kompiler tidak memiliki cara untuk kembali catchke blok untuk mengeksekusi apa yang ada setelah awaitinstruksi Anda atau sesuatu seperti itu ...

Saya mencoba menggunakan Task.Wait()untuk mengganti awaitdan saya menemui jalan buntu. Saya mencari di Web bagaimana saya dapat menghindari ini dan menemukan situs ini .

Karena saya tidak dapat mengubah asyncmetode dan juga tidak tahu apakah mereka menggunakannya ConfigureAwait(false), saya membuat metode ini yang mengambil Func<Task>yang memulai metode asinkron setelah kita berada di utas yang berbeda (untuk menghindari kebuntuan) dan menunggu penyelesaiannya:

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());
}

Jadi pertanyaan saya adalah: Apakah menurut Anda kode ini baik-baik saja?

Tentu saja, jika Anda memiliki beberapa penyempurnaan atau mengetahui pendekatan yang lebih baik, saya mendengarkan! :)

pengguna2397050
sumber
2
Menggunakan awaitblok tangkap sebenarnya diperbolehkan sejak C # 6.0 (lihat jawaban saya di bawah)
Adi Lester
3
Terkait pesan kesalahan C # 5.0 : CS1985 : Tidak bisa menunggu di badan klausa catch. CS1984 : Tidak bisa menunggu di badan klausa akhirnya.
DavidRR

Jawaban:

174

Anda bisa memindahkan logika ke luar catchblok dan memunculkan kembali pengecualian setelah, jika perlu, dengan menggunakan 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();
    }
}

Dengan cara ini, saat pemanggil memeriksa StackTraceproperti pengecualian , ia masih mencatat di mana di TaskThatFailsdalamnya itu dilemparkan.

Sam Harwell
sumber
1
Apa keuntungan menyimpan ExceptionDispatchInfodaripada Exception(seperti dalam jawaban Stephen Cleary)?
Varvara Kalinina
2
Saya dapat menebak bahwa jika Anda memutuskan untuk melempar kembali Exception, Anda kehilangan semua sebelumnya StackTrace?
Varvara Kalinina
1
@Varamalina Persis.
54

Anda harus tahu bahwa sejak C # 6.0, itu mungkin untuk digunakan awaitdalam catchdan finallyblok, jadi Anda sebenarnya bisa melakukan ini:

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

Fitur C # 6.0 baru, termasuk yang baru saja saya sebutkan , tercantum di sini atau sebagai video di sini .

Adi Lester
sumber
Dukungan untuk menunggu di blok tangkapan / akhirnya di C # 6.0 juga ditunjukkan di Wikipedia.
DavidRR
4
@Tokopedia Wikipedia tidak berwibawa. Sejauh ini, ini hanyalah situs web lain di antara jutaan.
pengguna34660
Meskipun ini berlaku untuk C # 6, pertanyaannya telah diberi tag C # 5 sejak awal. Ini membuat saya bertanya-tanya apakah memiliki jawaban ini di sini membingungkan, atau jika kita harus menghapus tag versi tertentu dalam kasus ini.
julealgon
16

Jika Anda perlu menggunakan asyncpenangan kesalahan, saya merekomendasikan sesuatu seperti ini:

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

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

Masalah dengan memblokir asynckode secara sinkron (terlepas dari utas apa yang menjalankannya) adalah Anda memblokir secara sinkron. Dalam kebanyakan skenario, lebih baik digunakan await.

Pembaruan: Karena Anda perlu memutar ulang, Anda dapat menggunakan ExceptionDispatchInfo.

Stephen Cleary
sumber
1
Terima kasih tapi sayangnya saya sudah tahu metode ini. Itulah yang biasanya saya lakukan, tetapi saya tidak dapat melakukannya di sini. Jika saya hanya menggunakan throw exception;dalam ifpernyataan, jejak tumpukan akan hilang.
pengguna2397050
3

Kami mengekstrak jawaban bagus hvd untuk kelas utilitas yang dapat digunakan kembali berikut dalam proyek kami:

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();
            }
        }
    }
}

Seseorang akan menggunakannya sebagai berikut:

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

Jangan ragu untuk meningkatkan penamaan, kami menyimpannya dengan sengaja. Perhatikan bahwa tidak perlu menangkap konteks di dalam pembungkus karena sudah ditangkap di situs panggilan, karenanya ConfigureAwait(false).

mrts
sumber