
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 通讯超时・数据库查询超时中IOExceptionIO 操作失败・图像文件保存失败磁盘满・配置文件读写失败・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-finallycsharp: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.cscsharp: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.cscsharppublic 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.cscsharp:public async Taskbool 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.cscsharp: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 { // 什么都不做这是工业软件最致命的错误 }✅ 正确csharptry { _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); }正确写法csharppublic 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); } }案例 2PLC 通讯中断异常错误写法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语句自动释放所有非托管资源