一、为什么异步操作异常处理这么重要

在WPF开发中,异步操作无处不在。比如从网络请求数据、读写文件、访问数据库等,这些操作往往需要一定时间才能完成。如果直接在主线程执行这些耗时操作,界面就会卡死,用户体验极差。于是我们使用async/await来让这些操作在后台执行。

但问题来了:异步操作中的异常如果不处理,程序可能直接崩溃,或者异常被"吞掉",导致难以排查的Bug。想象一下,用户点击按钮加载数据,结果因为网络问题导致异常,程序直接闪退,用户肯定一脸懵。所以,正确处理异步异常是保证应用稳定性的关键。

二、异步异常处理的常见误区

很多开发者在使用异步时容易陷入以下误区:

  1. 以为try-catch能捕获所有异常
    实际上,异步方法中的异常只有在await时才会抛出。如果忘记await,异常可能根本不会被捕获。

  2. 忽略Task的异常处理
    直接调用异步方法而不处理返回的Task,会导致异常被忽略。比如:

    // 错误示例:异常会被忽略
    private void Button_Click(object sender, RoutedEventArgs e)
    {
        LoadDataAsync(); // 没有await
    }
    
  3. 未处理全局异常
    即使单个异步操作处理了异常,仍可能有未捕获的异常导致应用崩溃。

三、如何正确捕获异步异常

3.1 基本处理:try-catch + await

最基础的方式是用try-catch包裹await调用:

private async void LoadDataButton_Click(object sender, RoutedEventArgs e)
{
    try
    {
        // 必须await才能捕获异常
        await LoadDataAsync();
    }
    catch (HttpRequestException ex)
    {
        // 处理网络异常
        MessageBox.Show($"网络错误: {ex.Message}");
    }
    catch (Exception ex)
    {
        // 处理其他异常
        MessageBox.Show($"加载失败: {ex.Message}");
    }
}

3.2 更健壮的方式:TaskScheduler.UnobservedTaskException

有些异常可能逃过try-catch,比如在Task.Run中抛出的异常。这时可以订阅全局异常事件:

public App()
{
    // 在App构造函数中注册全局异常处理
    TaskScheduler.UnobservedTaskException += (sender, args) =>
    {
        MessageBox.Show($"未捕获的异步异常: {args.Exception}");
        args.SetObserved(); // 标记为已处理
    };
}

3.3 高级技巧:使用ContinueWith处理Task异常

如果你不想用async/await,可以用ContinueWith处理异常:

private void Button_Click(object sender, RoutedEventArgs e)
{
    LoadDataAsync().ContinueWith(task =>
    {
        if (task.Exception != null)
        {
            // 处理所有聚合异常
            foreach (var ex in task.Exception.InnerExceptions)
            {
                Dispatcher.Invoke(() => MessageBox.Show(ex.Message));
            }
        }
    }, TaskScheduler.FromCurrentSynchronizationContext());
}

四、实战:一个完整的异常处理框架

让我们构建一个更健壮的解决方案,包含以下功能:

  1. 全局异常处理
  2. 日志记录
  3. 用户友好提示

4.1 全局异常处理器

public static class ExceptionHandler
{
    public static void Initialize()
    {
        // UI线程未捕获异常
        Application.Current.DispatcherUnhandledException += (s, e) =>
        {
            HandleException(e.Exception);
            e.Handled = true; // 阻止应用崩溃
        };

        // 非UI线程未捕获异常
        AppDomain.CurrentDomain.UnhandledException += (s, e) =>
        {
            HandleException(e.ExceptionObject as Exception);
        };

        // 异步任务未捕获异常
        TaskScheduler.UnobservedTaskException += (s, e) =>
        {
            HandleException(e.Exception);
            e.SetObserved();
        };
    }

    private static void HandleException(Exception ex)
    {
        // 记录日志
        Logger.Error(ex);
        
        // 显示友好提示
        Application.Current.Dispatcher.Invoke(() =>
        {
            MessageBox.Show("发生错误,已记录。请稍后重试");
        });
    }
}

4.2 在App.xaml.cs中初始化

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        ExceptionHandler.Initialize();
    }
}

4.3 使用示例

private async void LoadDataButton_Click(object sender, RoutedEventArgs e)
{
    try
    {
        var data = await DataService.GetDataAsync();
        // 处理数据...
    }
    catch (SpecificException ex)
    {
        // 特定异常处理
        await ShowErrorDialogAsync("特定错误", ex.Message);
    }
    catch (Exception ex)
    {
        // 通用异常处理
        Logger.Error(ex);
        await ShowErrorDialogAsync("错误", "操作失败");
    }
}

五、注意事项与最佳实践

  1. 不要忽略Task
    每个异步调用都应该被await或者正确处理返回的Task

  2. 区分可恢复和不可恢复错误
    网络错误可以重试,内存不足错误可能需要终止应用。

  3. 避免async void
    除了事件处理器,尽量使用async Task而不是async void,因为后者难以追踪异常。

  4. 记录完整异常信息
    不仅要记录Message,还要记录StackTraceInnerException

  5. 考虑使用Polly进行重试
    对于网络请求等可能临时失败的操作,可以使用Polly库实现自动重试:

    var retryPolicy = Policy
        .Handle<HttpRequestException>()
        .WaitAndRetryAsync(3, retryAttempt => 
            TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
    
    await retryPolicy.ExecuteAsync(async () => 
    {
        await GetDataFromApi();
    });
    

六、总结

在WPF中正确处理异步异常需要多层次的防御:

  1. 基本的try-catchawait组合
  2. 全局异常处理机制
  3. 完善的日志记录
  4. 用户友好的错误提示

记住,一个健壮的应用程序不是没有异常的应用程序,而是能够妥善处理所有异常的应用程序。通过本文介绍的技术,你可以大大提升WPF应用的稳定性和用户体验。