003011024_.NET 异常捕获完整解析
摘要:本文面向工业上位机开发场景,系统梳理 .NET 异常处理的核心机制与工程实践。文章从异常的本质、系统异常类型出发,重点讲解工业软件自定义异常体系与分层异常处理原则,结合相机取图超时、PLC 通讯中断、数据库写入失败等实战案例,总结出一套可落地的异常处理黄金法则:底层抛出、中层包装、上层处理,绝不吞异常,异常时释放资源并执行补偿逻辑,确保设备始终处于安全可控状态。
一、异常的本质与核心价值
1.1 什么是异常
异常是 .NET 提供的结构化错误处理机制,用于处理程序运行时的非正常情况(如硬件断开、文件不存在、网络中断)。它将错误检测和错误处理分离,让代码更清晰、更健壮。
1.2 异常 vs 返回错误码(工业场景对比)
| 方式 | 优点 | 缺点 | 工业场景适用性 |
|---|---|---|---|
| 异常机制 | 自动传播、强制处理、携带完整错误信息、不干扰正常流程 | 有一定性能开销 | ✅工业软件首选 |
| 返回错误码 | 性能高 | 容易被忽略、需要层层判断、无法携带详细信息、污染正常代码 | ❌ 仅用于极高频循环 |
工业场景致命问题:如果用返回错误码,90% 的开发者会忘记检查返回值,导致硬件异常时程序继续运行,最终造成设备损坏或产品报废。
1.3 .NET 异常处理原理
当异常发生时:
- CLR 创建一个异常对象,包含错误消息、堆栈跟踪、错误码等信息
- 异常沿着调用栈向上传播,直到被
catch块捕获 - 如果没有任何
catch块捕获,程序会崩溃并显示未处理异常对话框
二、.NET 系统异常类型(工业场景高频)
所有异常都继承自System.Exception基类,工业上位机最常见的系统异常如下:
| 异常类型 | 触发场景(工业专属) | 严重程度 |
|---|---|---|
NullReferenceException | 空对象引用(最常见)・相机未初始化就调用GetBitmap()・PLC 连接断开后仍尝试读写 | 高 |
TimeoutException | 操作超时・相机取图超时・PLC 通讯超时・数据库查询超时 | 中 |
IOException | IO 操作失败・图像文件保存失败(磁盘满)・配置文件读写失败・USB 设备断开 | 中 |
SocketException | 网络通讯失败・PLC 连接断开・相机网线松动・MES 服务器连接失败 | 高 |
ArgumentException | 参数非法・曝光时间设置为负数・PLC 地址格式错误・图像尺寸为 0 | 低 |
ArgumentNullException | 参数为 null・传入空的 Bitmap 给算法・传入空的条码给数据服务 | 低 |
OutOfMemoryException | 内存不足・Bitmap 未释放导致内存泄漏・一次性加载过多图像 | 高 |
InvalidOperationException | 操作无效・设备未初始化就启动采集・重复调用Dispose()・多线程同时访问硬件 | 高 |
AggregateException | 多任务异常・Task.Run()中发生的异常・多个并行任务同时出错 | 中 |
三、工业软件自定义异常体系(核心)
系统异常只能描述通用错误,工业软件需要定义自己的异常类型,来区分不同层级和类型的错误。
3.1 为什么需要自定义异常
- 分层处理:不同层级抛出不同类型的异常,上层可以针对性处理
- 携带更多信息:可以添加硬件类型、错误码、设备编号等工业专属信息
- 统一错误处理:可以全局捕获自定义异常,做统一的日志记录和报警
3.2 三层异常架构(工业标准)
对应你项目的分层架构,定义三层异常:
csharp
/// <summary> /// 硬件异常基类(硬件抽象层抛出) /// </summary> public class HardwareException : Exception { public string HardwareType { get; } // 硬件类型:Camera/PLC public string ErrorCode { get; } // 硬件SDK错误码 public string DeviceId { get; } // 设备编号 public HardwareException(string message, string hardwareType, string errorCode, string deviceId, Exception innerException) : base(message, innerException) { HardwareType = hardwareType; ErrorCode = errorCode; DeviceId = deviceId; } } /// <summary> /// 服务异常基类(服务层抛出) /// </summary> public class ServiceException : Exception { public string ServiceName { get; } // 服务名称:CameraService/PlcService public ServiceException(string message, string serviceName, Exception innerException) : base(message, innerException) { ServiceName = serviceName; } } /// <summary> /// 业务异常基类(业务逻辑层抛出) /// </summary> public class BusinessException : Exception { public int ErrorCode { get; } // 业务错误码 public BusinessException(string message, int errorCode) : base(message) { ErrorCode = errorCode; } }四、异常捕获与处理机制
4.1 基本语法:try-catch-finally
csharp:
try { // 可能发生异常的代码 var bitmap = _camera.GetBitmap(3000); } catch (HardwareException ex) when (ex.HardwareType == "Camera") { // 只捕获相机硬件异常 _logger.Error($"相机取图失败:{ex.Message},错误码:{ex.ErrorCode}", ex); // 处理逻辑:重启相机 } catch (TimeoutException ex) { // 捕获超时异常 _logger.Error("相机取图超时", ex); } catch (Exception ex) { // 捕获所有其他异常(最后一个catch) _logger.Fatal("未知异常", ex); throw; // 重新抛出,让上层处理 } finally { // 无论是否发生异常都会执行 // 释放资源 bitmap?.Dispose(); }4.2 using 语句:资源自动释放(工业必备)
using语句是try-finally的语法糖,会自动调用Dispose()方法释放资源,是工业软件防止内存泄漏的最重要手段。
正确示例:
csharp:
// 局部 Bitmap 自动释放 using (var bitmap = _camera.GetBitmap(3000)) { if (bitmap != null) { var defects = _algorithmService.DetectDefects(bitmap); } } // 离开using块时自动调用bitmap.Dispose()错误示例:
csharp:
// 内存泄漏!Bitmap 没有释放 var bitmap = _camera.GetBitmap(3000); var defects = _algorithmService.DetectDefects(bitmap);4.3 异常的传播与调用栈
异常会沿着调用栈向上传播,直到被捕获。例如:
plaintext:
MainViewModel.StartProcess() ↓ DetectionProcess.RunAsync() ↓ CameraService.GetBitmap() ↓ IkCamera.GetBitmap() → 抛出 HardwareException如果IkCamera没有捕获异常,异常会向上传播到CameraService,再到DetectionProcess,最后到MainViewModel。
4.4 WPF 全局异常处理(防止程序崩溃)
在App.xaml.cs中添加全局异常处理,捕获所有未处理的异常,防止程序直接崩溃:
csharp:
public partial class App : PrismApplication { protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); // 捕获 UI 线程异常 DispatcherUnhandledException += App_DispatcherUnhandledException; // 捕获非 UI 线程异常 AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; // 捕获 Task 异常 TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; } private void App_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) { e.Handled = true; // 标记为已处理,防止程序崩溃 _logger.Fatal("UI 线程未处理异常", e.Exception); MessageBox.Show($"系统发生严重错误:{e.Exception.Message}\n请重启程序", "致命错误", MessageBoxButton.OK, MessageBoxImage.Error); // 安全退出程序 Shutdown(); } private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { _logger.Fatal("非UI线程未处理异常", (Exception)e.ExceptionObject); } private void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) { e.SetObserved(); // 标记为已观察,防止程序崩溃 _logger.Fatal("Task未观察异常", e.Exception); } }五、分层异常处理原则(工业上位机标准)
底层抛出具体异常,中层包装通用异常,上层处理用户可见异常
绝对不允许吞掉异常不处理,也不允许在底层弹出 MessageBox。
5.1 硬件抽象层(01_Abstractions/02_Implementations)
职责:捕获 SDK 原生异常,包装成统一的HardwareException抛出,添加详细的硬件信息。
禁止:处理异常、弹出 UI、记录日志(除非是 SDK 内部日志)。
示例:IkCamera.cs
csharp:
public Bitmap GetBitmap(int timeoutMs) { try { // 调用埃科相机 SDK return IK_SDK_GetImage(timeoutMs); } catch (IkSdkException ex) { // 包装成统一的 HardwareException throw new HardwareException( $"相机取图失败:{ex.Message}", "Camera", ex.ErrorCode.ToString(), "Camera01", ex); // 保留原始异常堆栈 } catch (TimeoutException ex) { throw new HardwareException( "相机取图超时", "Camera", "TIMEOUT", "Camera01", ex); } }5.2 服务层(03_Services)
职责:捕获硬件异常,包装成ServiceException,记录完整日志,添加重试逻辑。
禁止:弹出 UI、处理业务逻辑。
示例:CameraService.cs
csharp
public Bitmap GetBitmap(int timeoutMs = 3000) { try { return _camera.GetBitmap(timeoutMs); } catch (HardwareException ex) { _logger.Error($"相机服务异常:{ex.Message},错误码:{ex.ErrorCode}", ex); // 重试逻辑 if (ex.ErrorCode == "COMMUNICATION_ERROR") { _logger.Info("尝试重启相机..."); _camera.Reconnect(); return _camera.GetBitmap(timeoutMs); } // 包装成ServiceException抛出 throw new ServiceException( $"相机取图失败:{ex.Message}", "CameraService", ex); } }5.3 业务逻辑层(04_Business)
职责:捕获服务异常,执行补偿逻辑(复位 PLC、停止设备),更新设备状态。
禁止:弹出 UI。
示例:DetectionProcess.cs
csharp:
public async Task<bool> RunAsync() { try { // 1. 等待 PLC 触发 await _plcService.WaitForStartTrigger(); // 2. 取图 var bitmap = await _cameraService.GetBitmapAsync(); if (bitmap == null) { throw new BusinessException("相机取图失败", 1001); } // 3. 发结束信号 _plcService.SendEndTrigger(true); // 4. 算法处理 var defects = await _algorithmService.DetectDefectsAsync(bitmap); // 5. 保存数据 await _dataService.SaveRecordAsync(defects); return defects.Count == 0; } catch (ServiceException ex) { _logger.Error($"检测流程异常:{ex.Message}", ex); // 补偿逻辑:复位所有 PLC 信号,停止设备 await _plcService.ResetAllSignals(); _stateMachine.SetError(); return false; } catch (BusinessException ex) { _logger.Error($"业务异常:{ex.Message},错误码:{ex.ErrorCode}", ex); await _plcService.ResetAllSignals(); _stateMachine.SetError(); return false; } }5.4 ViewModel 层(05_ViewModels)
职责:捕获所有异常,显示用户友好的提示,更新 UI 状态。
禁止:吞掉异常不处理。
示例:MainViewModel.cs
csharp:
private async void StartProcess() { try { IsRunning = true; StatusMessage = "系统运行中"; while (IsRunning) { var result = await _detectionProcess.RunAsync(); // 更新统计 TotalCount++; if (result) OkCount++; else NgCount++; } } catch (Exception ex) { _logger.Fatal("主流程异常", ex); MessageBox.Show( $"系统发生严重错误:{ex.Message}\n请联系设备维护人员", "错误", MessageBoxButton.OK, MessageBoxImage.Error); } finally { IsRunning = false; StatusMessage = "系统已停止"; } }六、工业场景异常处理最佳实践
6.1 绝对不要吞异常
❌ 错误:
csharp:
try { _camera.GetBitmap(); } catch { // 什么都不做!这是工业软件最致命的错误 }✅ 正确:
csharp
try { _camera.GetBitmap(); } catch (Exception ex) { _logger.Error("相机取图失败", ex); throw; // 重新抛出,让上层处理 }6.2 不要捕获所有异常
❌ 错误:
csharp:
try { // 代码 } catch (Exception ex) { // 捕获所有异常,包括 OutOfMemoryException 等致命异常 }✅ 正确:
csharp:
try { // 代码 } catch (HardwareException ex) { // 只处理硬件异常 } catch (TimeoutException ex) { // 只处理超时异常 } // 其他异常向上传播6.3 不要在底层弹出 UI
❌ 错误:
csharp:
// 在 CameraService 中弹出 MessageBox public Bitmap GetBitmap() { try { // 代码 } catch (Exception ex) { MessageBox.Show("相机错误"); // 绝对禁止! } }✅ 正确:
- 底层抛出异常
- 上层 ViewModel 捕获异常并弹出提示
6.4 记录完整的异常信息
异常日志必须包含:
- 异常消息
- 完整的堆栈跟踪
- 硬件类型和错误码
- 发生时间
- 操作人
6.5 异常时必须释放资源
所有非托管资源(Bitmap、文件句柄、硬件句柄)在异常发生时必须释放,使用using语句是最好的方式。
6.6 关键操作要有补偿逻辑
如果异常发生在流程中间,必须执行补偿逻辑,将设备恢复到安全状态:
- 复位所有 PLC 输出信号
- 停止相机采集
- 关闭光源
- 更新设备状态为错误
6.7 不要用异常控制流程
❌ 错误:
csharp:
// 用异常判断文件是否存在,性能极差 try { File.ReadAllText("config.json"); } catch (FileNotFoundException) { // 文件不存在 }✅ 正确:
csharp:
if (File.Exists("config.json")) { File.ReadAllText("config.json"); } else { // 文件不存在 }七、工业上位机实战案例
案例 1:相机取图超时异常
错误写法:
csharp:
public Bitmap GetBitmap() { // 没有异常处理,超时会导致程序崩溃 return IK_SDK_GetImage(3000); }正确写法:
csharp
public Bitmap GetBitmap(int timeoutMs = 3000) { try { var sw = Stopwatch.StartNew(); var bitmap = IK_SDK_GetImage(timeoutMs); _logger.Debug($"相机取图成功,耗时:{sw.ElapsedMilliseconds}ms"); return bitmap; } catch (IkSdkException ex) when (ex.ErrorCode == IkErrorCode.TIMEOUT) { _logger.Error($"相机取图超时,等待了{timeoutMs}ms", ex); throw new HardwareException( "相机取图超时", "Camera", "TIMEOUT", "Camera01", ex); } catch (IkSdkException ex) { _logger.Error($"相机 SDK 错误:{ex.Message},错误码:{ex.ErrorCode}", ex); throw new HardwareException( $"相机取图失败:{ex.Message}", "Camera", ex.ErrorCode.ToString(), "Camera01", ex); } }案例 2:PLC 通讯中断异常
错误写法:
csharp:
public bool ReadBool(string address) { // 通讯中断会抛出 SocketException,导致程序崩溃 return _plcClient.ReadBool(address); }正确写法:
csharp:
public bool ReadBool(string address) { try { return _plcClient.ReadBool(address); } catch (SocketException ex) { _logger.Error($"PLC 通讯中断:{ex.Message}", ex); // 尝试重连 if (!_isReconnecting) { _isReconnecting = true; Task.Run(async () => { while (!await ReconnectAsync()) { await Task.Delay(1000); } _isReconnecting = false; }); } throw new HardwareException( "PLC 通讯中断,正在尝试重连", "PLC", "COMMUNICATION_ERROR", "PLC01", ex); } }案例 3:数据库写入失败异常
错误写法:
csharp:
public void SaveRecord(ProductRecord record) { // 数据库写入失败会导致数据丢失 using var conn = _databaseContext.GetConnection(); conn.Execute("INSERT INTO ProductRecords ...", record); }正确写法:
csharp:
public void SaveRecord(ProductRecord record) { try { using var conn = _databaseContext.GetConnection(); conn.Execute("INSERT INTO ProductRecords ...", record); } catch (SqlException ex) { _logger.Error($"数据库写入失败:{ex.Message}", ex); // 保存到本地文件作为备份 File.WriteAllText( $"Backup/{DateTime.Now:yyyyMMddHHmmssfff}.json", JsonConvert.SerializeObject(record)); throw new ServiceException( "数据库写入失败,数据已备份到本地", "DataService", ex); } }八、常见坑与避坑指南
- 空 catch 块:最致命的错误,会隐藏所有问题,导致设备异常运行
- 丢失原始异常:包装异常时一定要传入
innerException,否则会丢失堆栈信息 - 资源泄漏:异常发生时忘记释放 Bitmap、文件句柄等资源
- 全局异常处理不当:不要在全局异常处理中继续运行程序,应该安全退出
- 异常性能问题:不要在高频循环中使用异常来控制流程,异常的性能开销约为 1000 个 CPU 周期
总结:工业上位机异常处理黄金法则
- 分层处理:底层抛出,中层包装,上层处理
- 绝不吞异常:所有异常都必须被记录或处理
- 安全第一:异常发生时必须将设备恢复到安全状态
- 完整日志:记录所有异常的详细信息,便于问题排查
- 资源释放:使用
using语句自动释放所有非托管资源