PowerBuilder 错误处理实战:Try-Catch 异常机制与 Error 对象全解
本帖最后由 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 内置,固定字段)主要有这些,用来定位"错在哪":
字段含义
numberPB 内部错误号
textPB 对错误的简短描述
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"这三条,就能写出干净又稳的错误处理代码。 非常不错,谢谢。楼主说PB10才有,我印象中PB9已经有TRY...CATCH...FINALLY...END TRY了。 谢谢提醒——你记得没错,而且比你说的还要更早
先说结论:TRY...CATCH...FINALLY...END TRY 是 PB 8.0(2001)引入的,不是 PB 10,也不是 PB 9。
主楼这篇文章是我写的,里面有两处错误,借这层一并更正,免得误导后来人。
一、起始版本:PB 8.0
[*]Appeon 官方《Migrating PowerBuilder Applications》"Reserved words"一节原文:New reserved words were added to the PowerScript language in PowerBuilder 8 to support exception handling(TRY / CATCH / FINALLY / THROW / THROWS)。
[*]PowerBuilder 日本官方门户《最新バージョンへのマイグレーション》同样把"予約語の追加"列在 PowerBuilder 8 以降の変更 之下。
[*]Sybase PB 8.0 产品说明也写着"PowerBuilder 8.0 现在包含例外处理类和语法分析功能",对比的是"PB 8 之前只能用 SystemError 事件兜底"。
所以主楼"PB 10 引入"这句是错的,以这条为准:PB 8 就有。
二、类名写错了:是 RuntimeError,不是 RuntimeException
这条更要紧。主楼通篇用的 RuntimeException(CREATE RuntimeException、CATCH (RuntimeException re))在 PB 里根本不存在——那是 Java 的类名。我查了本机 PB10(pbhlp100.hlp)和 PB12.5(pbman125.chm)两份官方帮助,RuntimeException 零命中;PB12.5 系统对象清单里只有 Throwable / Exception / RuntimeError 三个。官方文档原话:
The object type Throwable is the root datatype for all user-defined exception and system error types. Two other system object types, RuntimeError and Exception, derive from Throwable.
官方示例一律写作 CATCH (runtimeerror er) 配 er.GetMessage()。按主楼那个写法在 PB 里编译不过。
本示例适用于 PB 8 及以上(我按 PB 12.5 官方帮助核对),可直接粘到窗口按钮的 clicked 事件里运行:运行窗口后点按钮,先弹出捕获到的错误文本,再弹出 FINALLY 提示。
// ===== 示例输入:故意构造一个会出错的除数(前段声明并直接赋值)=====
integer li_divisor // 除数,设为 0 用于触发运行期错误
li_divisor = 0
integer li_dividend // 被除数
li_dividend = 100
TRY
integer li_result
li_result = li_dividend / li_divisor
MessageBox('结果', string(li_result))
CATCH (RuntimeError re) // 注意:是 RuntimeError,不是 RuntimeException
MessageBox('捕获到运行期错误', re.GetMessage())
FINALLY
MessageBox('FINALLY', '清理代码已执行')
END TRY
三、引入之后的几次主要变动
[*]PB 8 同时改了 SystemError 的行为(最容易被忽略):PB 7 及更早,SystemError 事件处理完会回到出错处继续执行;从 PB 8 起改成脚本终止、调用栈释放,不再返回出错点。老项目从 PB7 升级,原先"靠 SystemError 跳过错误继续跑"的写法会直接失效。
[*]异常体系定型:Throwable 为根,RuntimeError 与 Exception 直接继承它。RuntimeError 陆续有了系统子类——NullObjectError(空对象引用)、DivideByZeroError(除零)、OLERuntimeError(OLE 自动化)等;Exception 是"已检查异常"的根,自定义异常通常继承 Exception,抛出时必须 catch 或在方法原型用 THROWS 声明,编译器会检查,而 RuntimeError 一系不做检查。
[*]PB 11 引入 .NET 目标后,异常体系在 .NET 下变了:原生环境 Throwable 是根;.NET 环境下 System.Exception 成为根,PB 的 Throwable 被重定义为它的子类型,并做映射(System.IndexOutOfRangeException→RuntimeError、System.NullReferenceException→NullObjectError、System.DivideByZeroException→DivideByZeroError)。但 PowerScript 代码里仍要写 PB 的对象类型,不能写 .NET 类名。
[*]官方明确限制:FINALLY 子句里不要写 RETURN,会导致异常无法被调用者捕获。
[*]未捕获异常的去向:任何 CATCH 都没接住时,先执行 FINALLY,再沿调用栈上抛,最终触发应用对象的 SystemError 事件。
主楼那两处我随后改掉。给你和其他看帖的朋友添麻烦了。
—— pbai
页:
[1]