ทางออกที่ดีสำหรับการรอคอยในการลอง / จับ / ในที่สุด?


94

ฉันต้องเรียกใช้asyncเมธอดในcatchบล็อกก่อนที่จะโยนข้อยกเว้นอีกครั้ง (ด้วยการติดตามสแต็ก) เช่นนี้

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

แต่น่าเสียดายที่คุณไม่สามารถใช้awaitใน a catchหรือfinallyblock ได้ ฉันได้เรียนรู้ว่าเป็นเพราะคอมไพเลอร์ไม่มีทางย้อนกลับไปในcatchบล็อกเพื่อดำเนินการตามawaitคำสั่งของคุณหรืออะไรทำนองนั้น ...

ฉันพยายามใช้Task.Wait()เพื่อแทนที่awaitและฉันมีการหยุดชะงัก ฉันค้นหาบนเว็บว่าฉันจะหลีกเลี่ยงปัญหานี้ได้อย่างไรและพบไซต์นี้

เนื่องจากฉันไม่สามารถเปลี่ยนasyncวิธีการและฉันไม่รู้ว่าพวกเขาใช้ConfigureAwait(false)หรือไม่ฉันจึงสร้างวิธีการเหล่านี้ขึ้นมาซึ่งใช้วิธีFunc<Task>เริ่มต้นวิธี async เมื่อเราอยู่ในเธรดอื่น (เพื่อหลีกเลี่ยงการหยุดชะงัก) และรอให้มันเสร็จสิ้น:

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 block ได้ตั้งแต่ C # 6.0 (ดูคำตอบของฉันด้านล่าง)
Adi Lester

3
ข้อความแสดงข้อผิดพลาดC # 5.0ที่เกี่ยวข้อง: CS1985 : ไม่สามารถรอในเนื้อหาของประโยค catch CS1984 : ไม่สามารถรอในเนื้อหาของประโยคสุดท้ายได้
DavidRR

คำตอบ:


174

คุณสามารถย้ายที่อยู่นอกตรรกะของการcatchป้องกันและ rethrow 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เป็น (ตามคำตอบของ Stephen Cleary)
Varvara Kalinina

2
ฉันเดาได้ว่าถ้าคุณตัดสินใจที่จะปลูกExceptionใหม่คุณจะสูญเสียสิ่งที่ผ่านมาทั้งหมดStackTrace?
Varvara Kalinina

1
@VarvaraKalinina แน่นอน

54

คุณควรทราบว่าตั้งแต่ C # 6.0 ก็เป็นไปได้ที่จะใช้awaitในcatchและfinallyบล็อกเพื่อให้คุณสามารถในความเป็นจริงการทำเช่นนี้:

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

ใหม่ C # 6.0 คุณสมบัติรวมถึงคนที่ฉันเพิ่งกล่าวถึงอยู่ที่นี่หรือเป็นวิดีโอที่นี่


การรองรับการรอคอยในการจับ / บล็อกสุดท้ายใน C # 6.0 ยังระบุไว้ในวิกิพีเดีย
DavidRR

4
@DavidRR. Wikipedia ไม่ได้เป็นผู้เขียน เป็นเพียงเว็บไซต์อื่นในหมู่ล้านเท่าที่เป็นห่วง
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)โปรดทราบว่าไม่มีความจำเป็นที่จะจับบริบทภายในห่อหุ้มในขณะที่มันถูกจับแล้วในเว็บไซต์โทรเพราะฉะนั้น

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.