马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
本帖最后由 pbai 于 2026-9-2 11:10 编辑
PowerBuilder 错误处理实战:Try-Catch 异常机制与 Error 对象全解
阅读说明
1. 适用版本:PB 8.0 及以上(Try-Catch / THROW 自 PB 8.0 引入;本文按 PB 12.5 验证;纯经典 PowerBuilder,不依赖 PBIDEA 运行库)
2. 支持数据库:本文不涉及数据库操作,无 DBMS 要求
3. 操作系统与环境要求:Windows 7+,已安装 PowerBuilder 开发环境(含 Throwable/Exception/RuntimeError 标准类库)
4. 难度系数:★★☆☆☆
5. 其它阅读说明:前置知识为 PowerScript 基本语法与变量声明;文中示例均为可独立粘贴运行的最小脚本,建议在窗口按钮 clicked 事件中验证
一、PB 里的错误有两类(先分清)
写程序躲不开报错,但 PowerBuilder 的错误要分两种情况看,处理手段完全不同:
- 运行时异常(Throwable):PB 8.0 以后引入的异常体系,用 TRY...CATCH...FINALLY...END TRY 捕获,也可以用 THROW 主动抛出。这是本文的主角。
- 系统错误(System Error):那些 PB 自己都兜不住的严重错误——比如调用了一个不存在的函数、对象为空还去访问属性、除零、数组越界。这类错误不会被 Try-Catch 截住,而是触发应用对象的 SystemError 事件,并把现场信息塞进一个全局的 error 对象。
很多老项目只用 SystemError 事件兜底、从不用 Try-Catch,结果业务逻辑里一旦失败只能靠返回值判断,代码又臭又长。本文把两套机制都讲透,你按场景选。
二、Try-Catch 语法与 Throwable 体系
PB 8.0+ 的异常语法和 Java/C# 几乎一致(已对照 PB12.5 官方帮助核实):
- TRY
- trystatements
- CATCH ( ThrowableType1 exIdentifier1 )
- catchstatements1
- CATCH ( ThrowableType2 exIdentifier2 )
- catchstatements2
- ...
- CATCH ( ThrowableTypeN exIdentifierN )
- catchstatementsN
- FINALLY
- cleanupstatements
- END TRY
复制代码
几个关键点:
- TRY / END TRY 必须成对;中间可以跟一个或多个 CATCH,最后可选 FINALLY。
- CATCH 的圆括号里是「异常类型 + 变量名」:CATCH (RuntimeError re) 表示捕获 RuntimeError 类型,变量 re 用来在块里读取异常信息。
- FINALLY 一定执行:不管 TRY 里是正常走完还是抛了异常、甚至 CATCH 里又抛异常,FINALLY 里的清理代码都会跑——适合关文件、释放对象、解绑事务。
- 异常类型是可继承的:所有异常都源自 Throwable;常用的两个子类是 Exception(普通异常)和 RuntimeError(运行期异常)。CATCH (Throwable t) 能兜住一切,CATCH (RuntimeError re) 只兜运行期异常。
- THROW 主动抛异常:语法是 THROW exlvalue,后面跟一个 Throwable 类型的变量(不能是普通类型)。
RuntimeError 对象最常用的两个方法(来自 Throwable 基类,官方稳定 API):
| 方法 | 作用 | | getMessage() | 返回异常文本 | | setMessage(string) | 设置异常文本(抛之前先写原因) |
下面用一个除零触发运行期异常的最小示例把语法跑通:
前置:新建窗口 w_demo,放一个按钮 cb_run。
步骤:1) 打开窗口 w_demo;2) 在 cb_run 的 clicked 事件贴入下方代码;3) 运行窗口并点击按钮,弹窗显示捕获到的异常文本。 - // ===== 示例输入:故意构造一个会出错的除数(前段声明并直接赋值)=====
- integer li_divisor
- li_divisor = 0
- // ===== 示例输入:被除数 =====
- integer li_dividend
- li_dividend = 100
- TRY
- // 除数为 0 会触发 PB 的 RuntimeError(运行期异常),进入 CATCH
- integer li_result
- li_result = li_dividend / li_divisor
- MessageBox('结果', string(li_result))
- CATCH (RuntimeError re)
- // re 是捕获到的异常对象,getMessage() 取文本
- MessageBox('捕获到运行期异常', re.getMessage())
- FINALLY
- // 无论成功失败都会执行,这里只做演示
- MessageBox('FINALLY', '清理代码已执行')
- END TRY
复制代码
跑起来你会先看到「捕获到运行期异常」弹窗(内容是 PB 内置的除零描述),再看到「FINALLY 清理代码已执行」。这就是 Try-Catch 的基本骨架。
三、THROW 主动抛异常(业务校验的神器)
光会捕获不够,主动 THROW 才是写干净业务代码的开始。比如参数非法、状态不对,与其 return -1 让调用方猜,不如直接抛一个带原因的运行期异常,由上层统一兜。
前置:窗口 w_demo 上按钮 cb_check 的 clicked 事件。
步骤:贴入下方代码并运行,输入为空时按钮会捕获到我们自定义的异常文本。 - // ===== 示例输入:用户输入的用户名(前段声明并直接赋值,模拟界面取到的空值)=====
- string ls_username
- ls_username = ''
- TRY
- IF ls_username = '' THEN
- // 先建一个 RuntimeError 对象,写好原因,再 THROW 抛出
- RuntimeError lre_empty
- lre_empty = CREATE RuntimeError
- lre_empty.setMessage('用户名不能为空,请先填写后再提交')
- THROW lre_empty
- END IF
- // 正常业务逻辑(这里仅演示,不展开)
- MessageBox('提交', '用户 ' + ls_username + ' 校验通过')
- CATCH (RuntimeError re)
- // 上层统一捕获,直接把原因告诉用户
- MessageBox('校验失败', re.getMessage())
- FINALLY
- MessageBox('FINALLY', '校验流程结束')
- END TRY
复制代码
注意两处红线:① RuntimeError lre_empty 与 lre_empty = CREATE RuntimeError 必须拆两行,不能写 RuntimeError lre_empty = CREATE ...;② THROW 后面是变量,不是字符串——所以要先建对象、setMessage、再抛。
四、CATCH 多类型分层捕获(异常细分)
TRY 后面可以跟多个 CATCH,PB 会按书写顺序匹配第一个符合的异常类型。把具体类型写在前面、宽泛的 Throwable 写在最后,就能做到"已知的错给精确提示、未知的错兜底"。
前置:窗口 w_demo 上按钮 cb_multi 的 clicked 事件。
步骤:贴入下方代码运行,观察不同输入触发的不同分支。 - // ===== 示例输入:模拟两种错误来源(前段声明并直接赋值)=====
- integer li_mode
- li_mode = 1 // 1=触发运行期异常;2=触发普通 Exception
- TRY
- IF li_mode = 1 THEN
- integer li_x
- li_x = 10 / 0 // 运行期异常 RuntimeError
- ELSE
- Exception le_norm
- le_norm = CREATE Exception
- le_norm.setMessage('业务状态异常:订单已关闭')
- THROW le_norm // 普通 Exception
- END IF
- CATCH (RuntimeError re)
- MessageBox('运行期异常', re.getMessage())
- CATCH (Exception ex)
- MessageBox('普通异常', ex.getMessage())
- CATCH (Throwable t)
- // 兜底:任何 Throwable 子类都能在这里接住
- MessageBox('未知异常兜底', t.getMessage())
- FINALLY
- MessageBox('FINALLY', '收尾')
- END TRY
复制代码
把 li_mode 改成 2 再跑,就会走 CATCH (Exception ex) 分支。这就是"窄类型在前、宽类型在后"的写法。
五、SystemError 事件与内置 error 对象(兜住兜不住的错)
前面说的 Try-Catch 只能抓 Throwable 体系。那些 PB 解释器级别的致命错误——对象为 NULL 还访问属性、调用不存在的方法、菜单项没找到、数组下标越界等——走的是另一条路:触发应用对象(Application)的 SystemError 事件,并把错误现场写进一个全局的 error 对象。
error 对象(PB 内置,固定字段)主要有这些,用来定位"错在哪":
| 字段 | 含义 | | number | PB 内部错误号 | | text | PB 对错误的简短描述 | | windowmenu | 出错所在的窗口/菜单名 | | object | 出错所在的对象名 | | objectevent | 出错所在的事件/函数名 | | line | 出错脚本的行号 | | errortext | 更详细的错误文本 |
最常见的坑就是 NULL 对象访问。下面演示一个会进 SystemError 的写法(注意:这段代码不要包在 Try-Catch 里,因为 Try-Catch 截不住它;它只能由 SystemError 事件兜):
前置:在应用对象(如 pbtutor)的 SystemError 事件中写入下方代码;另在窗口按钮 cb_null 的 clicked 事件写 w_demo w_null 后直接 w_null.title = 'x'(访问 NULL 对象属性)。
步骤:运行按钮,PB 会跳到应用对象的 SystemError 事件,弹窗显示错误位置。 - // 应用对象 SystemError 事件:统一兜底,避免程序直接崩
- MessageBox('系统错误', &
- '错误号:' + string(error.number) + '~r~n' + &
- '描述:' + error.text + '~r~n' + &
- '对象:' + error.object + ' / ' + error.objectevent + '~r~n' + &
- '行号:' + string(error.line))
复制代码
这段代码里 error 是 PB 在 SystemError 事件里自动注入的全局对象,直接用即可,不需要你声明。它的字段(number/text/object/objectevent/line 等)是 PB 官方固定的,照着用不会错。
六、Try-Catch 与 SystemError 怎么配合(实战架构)
一个健壮的 PB 应用,两套机制要搭在一起用:
- 业务逻辑层用 Try-Catch + THROW:自己能预见的非法输入、状态不对、外部调用失败,主动抛、就近捕获,给友好提示。
- 应用层用 SystemError 兜底:任何没被 Try-Catch 接住的致命错误(NULL 访问、调用不存在的方法),在 SystemError 事件里统一记日志 + 提示,防止整个程序闪退。
- FINALLY 负责释放:凡是 CREATE 出来的对象、打开的文件、占着的事务,在 FINALLY 里 DESTROY / 关闭,避免资源泄漏。
前置:窗口 w_demo 按钮 cb_biz 的 clicked 事件。
步骤:贴入下方代码;演示"业务抛异常 → 捕获提示 → FINALLY 释放对象"的完整链路。 - // ===== 示例输入:订单金额(前段声明并直接赋值,模拟非法负值)=====
- decimal ldc_amount
- ldc_amount = -50
- // ===== 业务对象(演示用,实际项目里可能是 uo_order 之类)=====
- RuntimeError lre_biz
- lre_biz = CREATE RuntimeError
- TRY
- IF ldc_amount <= 0 THEN
- lre_biz.setMessage('订单金额必须大于 0,当前为:' + string(ldc_amount))
- THROW lre_biz
- END IF
- MessageBox('下单', '金额 ' + string(ldc_amount) + ' 校验通过')
- CATCH (RuntimeError re)
- MessageBox('下单失败', re.getMessage())
- FINALLY
- // 释放业务异常对象,避免内存泄漏
- DESTROY lre_biz
- MessageBox('FINALLY', '资源已释放')
- END TRY
复制代码
七、常见坑与易错点(逐条排雷)
- Try-Catch 截不住 NULL 访问和调用不存在的方法:这些是 SystemError 的范畴,别指望 CATCH 能接住,必须靠 SystemError 事件兜底。
- THROW 后面必须是 Throwable 变量:THROW '错了' 是错的,得先 CREATE RuntimeError 再 setMessage 再 THROW。
- 声明与创建拆两行:RuntimeError re = CREATE RuntimeError 非法,必须 RuntimeError re / re = CREATE RuntimeError。
- CATCH 顺序要"窄在前、宽在后":把 Throwable 放第一个 CATCH,后面的具体类型永远进不去,编译还可能报不可达。
- CREATE 的异常对象记得 DESTROY:尤其在循环里 THROW,每次都 NEW 不释放会慢慢吃掉内存;放进 FINALLY 最稳。
- FINALLY 里别再写可能抛异常的代码:FINALLY 抛异常会掩盖 TRY 里的原始错误,定位更麻烦。
- error 对象只在 SystemError 事件有效:在普通函数里写 error.number 会编译报错,它不是全局随手能用的变量。
八、与相近方案对比(怎么选)
| 场景 | 用哪种 | 理由 | | 能预见的业务非法(空值/状态错/外部调用失败) | Try-Catch + THROW | 提示友好、调用栈清晰、可分层捕获 | | 老版本 PB(7 及更早,无 Try-Catch) | 函数返回码 return -1 + 全局错误变量 | PB 9 没有 Throwable 体系,只能靠返回值 | | 致命解释器错误(NULL 访问/调不存在方法) | SystemError 事件 + error 对象 | Try-Catch 截不住,只能应用级兜底 | | 简单的单点校验 | IF ... THEN return | 不值得为每一处都上异常框架 |
一句话总结:业务错误用 Try-Catch 主动抛出就近捕获,致命错误交给 SystemError 兜底,FINALLY 管资源释放——把这套组合起来,PB 程序的健壮性就上了一个台阶。
九、小结
本文核心:PB 8.0+ 用 TRY...CATCH...FINALLY...END TRY 捕获 Throwable 体系异常,THROW 主动抛出(须先 CREATE RuntimeError 并 setMessage),FINALLY 必执行用于清理;而 NULL 访问、调用不存在方法这类致命错误走应用对象 SystemError 事件,现场信息在全局 error 对象(number/text/object/objectevent/line 等字段)里。两条线配合使用:业务错误就近捕获给友好提示,致命错误统一兜底防闪退。记住"窄类型 CATCH 在前、宽类型在后""声明与创建拆两行""CREATE 的对象在 FINALLY 里 DESTROY"这三条,就能写出干净又稳的错误处理代码。 |