news 2026/9/26 15:04:55

003011024_.NET 异常捕获完整解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
003011024_.NET 异常捕获完整解析

003011024_.NET 异常捕获完整解析

摘要:本文面向工业上位机开发场景,系统梳理 .NET 异常处理的核心机制与工程实践。文章从异常的本质、系统异常类型出发,重点讲解工业软件自定义异常体系与分层异常处理原则,结合相机取图超时、PLC 通讯中断、数据库写入失败等实战案例,总结出一套可落地的异常处理黄金法则:底层抛出、中层包装、上层处理,绝不吞异常,异常时释放资源并执行补偿逻辑,确保设备始终处于安全可控状态。

一、异常的本质与核心价值

1.1 什么是异常

异常是 .NET 提供的结构化错误处理机制,用于处理程序运行时的非正常情况(如硬件断开、文件不存在、网络中断)。它将错误检测和错误处理分离,让代码更清晰、更健壮。

1.2 异常 vs 返回错误码(工业场景对比)

方式优点缺点工业场景适用性
异常机制自动传播、强制处理、携带完整错误信息、不干扰正常流程有一定性能开销✅工业软件首选
返回错误码性能高容易被忽略、需要层层判断、无法携带详细信息、污染正常代码❌ 仅用于极高频循环

工业场景致命问题:如果用返回错误码,90% 的开发者会忘记检查返回值,导致硬件异常时程序继续运行,最终造成设备损坏或产品报废。

1.3 .NET 异常处理原理

当异常发生时:

  1. CLR 创建一个异常对象,包含错误消息、堆栈跟踪、错误码等信息
  2. 异常沿着调用栈向上传播,直到被catch块捕获
  3. 如果没有任何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 为什么需要自定义异常

  1. 分层处理:不同层级抛出不同类型的异常,上层可以针对性处理
  2. 携带更多信息:可以添加硬件类型、错误码、设备编号等工业专属信息
  3. 统一错误处理:可以全局捕获自定义异常,做统一的日志记录和报警

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); } }

八、常见坑与避坑指南

  1. 空 catch 块:最致命的错误,会隐藏所有问题,导致设备异常运行
  2. 丢失原始异常:包装异常时一定要传入innerException,否则会丢失堆栈信息
  3. 资源泄漏:异常发生时忘记释放 Bitmap、文件句柄等资源
  4. 全局异常处理不当:不要在全局异常处理中继续运行程序,应该安全退出
  5. 异常性能问题:不要在高频循环中使用异常来控制流程,异常的性能开销约为 1000 个 CPU 周期

总结:工业上位机异常处理黄金法则

  1. 分层处理:底层抛出,中层包装,上层处理
  2. 绝不吞异常:所有异常都必须被记录或处理
  3. 安全第一:异常发生时必须将设备恢复到安全状态
  4. 完整日志:记录所有异常的详细信息,便于问题排查
  5. 资源释放:使用using语句自动释放所有非托管资源
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 15:04:32

混合流水车间调度中融合启发式解码与NSGA-II的算法解析

咱们搞调度优化的人&#xff0c;十有八九都跟流水车间调度问题&#xff08;Flow Shop Scheduling Problem&#xff0c;FSP&#xff09;打过交道。但实际产线哪有那么规整&#xff1f;一条线上既有并行机&#xff0c;又有工艺顺序约束&#xff0c;还得考虑换模时间、工人技能不同…

作者头像 李华
网站建设 2026/9/26 15:04:28

DeskcommCRM实测:客服工单与客户关系管理一体化的高效工作台

客服团队规模一过十个人&#xff0c;工单系统、客服邮箱、客户资料表、跟进记录各自为战的场景我见得太多了。每天光是“上一个同事到底有没有联系过这个客户”“这个项目进展到哪一步了”这类确认工作&#xff0c;就够大家消耗掉大半个上午。DeskcommCRM这个名字&#xff0c;我…

作者头像 李华
网站建设 2026/9/26 15:04:07

Coding Agent 安全执行:OpenSandbox 沙箱与 Agent Runtime 运行时深度解析

1. 项目概述&#xff1a;当 Coding Agent 真正“动手”时&#xff0c;它需要一个不会弄坏任何东西的厨房你有没有试过让一个刚学会写代码的实习生&#xff0c;在你生产环境的数据库上直接执行DROP TABLE users;&#xff1f;大概率会立刻收到运维同事的夺命连环 call。而今天我们…

作者头像 李华
网站建设 2026/9/26 15:03:41

二手车大数据挖掘的多模型融合实战:从特征工程到Stacking实现

简介&#xff1a;面向计算机相关专业学生的毕业设计与课程作业&#xff0c;这份资源以二手车交易市场为背景&#xff0c;提供了基于机器学习和多模型融合的大数据挖掘完整实现&#xff0c;重点覆盖数据缺失值预测、交易价格预测与成交周期挖掘等任务。压缩包内共有24个文件&…

作者头像 李华