4、异常处理与捕获:try-catch最佳实践、全局异常处理器、异常信息记录与分析
异常处理这事儿,说实话,很多桌面应用开发者都低估了它的重要性。
我见过太多应用,一遇到意外就崩溃,连个提示都没有。用户只能看到「程序已停止工作」——然后骂娘。你想想看,这多掉价。
今天我们就聊聊怎么把异常处理做好。不是那种教科书式的写法,而是真正能在实战中扛得住事的方案。
4.1 try-catch 的最佳实践
先说说最基础的 try-catch。很多人觉得这玩意儿简单,不就是包一下嘛。但我在项目中踩过不少坑,总结下来有这么几条铁律。
4.1.1 别吞异常
这是最常见的错误。我曾经接手过一个项目,里面全是这种代码:
try
{
// 一些操作
File.ReadAllText(path);
}
catch (Exception ex)
{
// 啥也不干
}
嗯,这种写法等于告诉用户:「出错了,但我懒得管。」
我个人习惯是:要么处理,要么往上抛,千万别沉默。沉默的异常就像定时炸弹,你不知道它什么时候炸。
4.1.2 捕获具体异常,别用万金油
很多新手喜欢直接 catch (Exception),这其实很偷懒。你应该捕获你能处理的具体异常类型。
// 不推荐
try
{
var data = File.ReadAllBytes(path);
}
catch (Exception ex)
{
MessageBox.Show("文件读取失败");
}
// 推荐
try
{
var data = File.ReadAllBytes(path);
}
catch (FileNotFoundException ex)
{
Log.Warn($"文件未找到: {path}");
// 提示用户选择其他文件
}
catch (UnauthorizedAccessException ex)
{
Log.Warn($"无权限访问: {path}");
// 提示用户以管理员身份运行
}
catch (IOException ex)
{
Log.Error($"IO错误: {ex.Message}");
// 重试或退出
}
为什么?因为不同的异常,处理方式完全不同。你想想看,文件不存在和权限不足,能一样处理吗?
4.1.3 保持 try 块尽量小
我见过有人把整个方法体都包在 try 里。这其实不好。try 块越大,你越难定位问题。
4.2 全局异常处理器
try-catch 能解决大部分问题,但总有漏网之鱼。比如事件处理函数里抛出的异常、异步操作中的异常、第三方库内部抛出的异常。
这时候就需要全局异常处理器了。说白了,就是给整个应用装一个「安全网」。
4.2.1 WinForms 中的全局异常处理
// 在 Program.cs 的 Main 方法中
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
// 处理 UI 线程异常
Application.ThreadException += (sender, e) =>
{
Log.Fatal(e.Exception, "UI线程未处理异常");
MessageBox.Show(
$"程序遇到意外错误,即将退出。\n错误: {e.Exception.Message}",
"严重错误",
MessageBoxButtons.OK,
MessageBoxIcon.Error
);
Application.Exit();
};
// 处理非 UI 线程异常
AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
{
var ex = e.ExceptionObject as Exception;
Log.Fatal(ex, "非UI线程未处理异常");
// 记录日志后,让系统处理
};
Application.Run(new MainForm());
}
这里有个坑,我必须要说。UI 线程和非 UI 线程的异常处理机制是分开的。你只注册一个是不行的。
4.2.2 WPF 中的全局异常处理
// App.xaml.cs
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// 处理 Dispatcher 未处理异常
this.DispatcherUnhandledException += (sender, args) =>
{
Log.Fatal(args.Exception, "Dispatcher未处理异常");
MessageBox.Show($"发生未处理异常: {args.Exception.Message}");
args.Handled = true; // 标记为已处理,防止应用崩溃
};
// 处理 Task 未处理异常
TaskScheduler.UnobservedTaskException += (sender, args) =>
{
Log.Fatal(args.Exception, "Task未处理异常");
args.SetObserved(); // 标记为已观察
};
}
}
4.3 异常信息记录与分析
捕获异常只是第一步。真正有价值的是——你怎么记录和分析这些异常。
我曾经在一个项目中,异常日志里全是「Object reference not set to an instance of an object」。你猜怎么着?根本没法排查。因为没有上下文信息。
4.3.1 记录什么信息
一个好的异常日志,至少应该包含这些:
| 信息项 | 说明 | 示例 |
|---|---|---|
| 异常类型 | 具体的异常类名 | System.IO.FileNotFoundException |
| 异常消息 | 异常的 Message 属性 | 找不到文件 'C:\data\config.xml' |
| 堆栈跟踪 | 完整的调用栈 | at MyApp.DataLoader.Load() in ... |
| 时间戳 | 精确到毫秒 | 2024-01-15 14:30:22.456 |
| 用户信息 | 当前登录用户 | DOMAIN\zhangsan |
| 上下文数据 | 关键变量值 | FilePath = "C:\data\config.xml" |
| 操作系统版本 | 环境信息 | Windows 10.0.19045 |
4.3.2 一个实用的日志工具类
public static class ExceptionLogger
{
public static void Log(Exception ex,
[CallerMemberName] string callerName = "",
Dictionary<string, object> context = null)
{
var sb = new StringBuilder();
sb.AppendLine($"时间: {DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}");
sb.AppendLine($"调用者: {callerName}");
sb.AppendLine($"异常类型: {ex.GetType().FullName}");
sb.AppendLine($"异常消息: {ex.Message}");
sb.AppendLine($"堆栈跟踪: {ex.StackTrace}");
if (context != null)
{
sb.AppendLine("上下文数据:");
foreach (var kvp in context)
{
sb.AppendLine($" {kvp.Key} = {kvp.Value}");
}
}
if (ex.InnerException != null)
{
sb.AppendLine("内部异常:");
sb.AppendLine($" 类型: {ex.InnerException.GetType().FullName}");
sb.AppendLine($" 消息: {ex.InnerException.Message}");
}
// 写入文件
File.AppendAllText("error.log", sb.ToString() + Environment.NewLine);
}
}
// 使用示例
try
{
var data = LoadConfig("config.xml");
}
catch (Exception ex)
{
ExceptionLogger.Log(ex, context: new Dictionary<string, object>
{
["ConfigPath"] = "config.xml",
["Environment"] = "Production"
});
}
4.3.3 异常分析三板斧
记录完日志,怎么分析?我一般按这个顺序来:
- 看异常类型和消息——先搞清楚是什么类型的错误。是 IO 错误?网络错误?还是业务逻辑错误?
- 看堆栈跟踪——找到出错的代码行。注意看调用栈的顶部,那才是真正出错的地方。
- 看上下文数据——结合当时的变量值,复现问题。比如文件路径、用户输入、网络状态等。
举个例子。有一次线上反馈说「保存失败」。我一看日志:
- 异常类型:System.UnauthorizedAccessException
- 消息:对路径“C:\Program Files\MyApp\data\”的访问被拒绝
- 上下文:FilePath = "C:\Program Files\MyApp\data\config.json"
嗯,问题很明显了。程序试图往 Program Files 目录写数据,但普通用户没有写权限。解决方案:把数据存到 AppData 目录下。
你看,如果没有上下文数据,你根本不知道是哪个文件、哪个路径出的问题。
4.4 避坑指南
最后,分享几个我踩过的坑:
还有一个常见的坑:异步方法里的异常。如果你用 async void,异常会直接飞到上层,很难捕获。我个人习惯是:能用 async Task 就别用 async void。事件处理函数除外,但事件处理函数里一定要包 try-catch。
好了,异常处理这块就聊这么多。记住一句话:异常不是 bug,是信息。你处理得越好,应用就越稳定,用户就越省心。