马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
本帖最后由 pbai 于 2026-9-21 09:09 编辑
PowerBuilder 面向对象全解:用户对象、继承、覆盖与多态(PB12.5 实机验证)
阅读说明
1. 适用版本:基准 PB10,兼容 PB 12.5(两版均已实机「编译 + 运行」通过,行为一致;语法自 PB 6.5 起一致,PB 9+ 起事件继承行为相同)
2. 支持数据库:本文示例不涉及数据库,纯对象模型,无需连接任何 DBMS
3. 操作系统与环境要求:Windows 7 及以上;PowerBuilder 开发环境(或 PBVM 运行时);只需一个空的 PBL 与目标工程,不依赖 PBIDEA 等第三方库
4. 难度系数:★★★☆☆
5. 其它阅读说明:建议先掌握「变量作用域」与「函数与参数传递」;本文所有结论均来自 PB10 与 PB 12.5 双版本实机运行输出(两版输出逐字节一致),文中给出的错误号(C0015 / C0051 / C0123 / runtimeerror 2、23、30、65)都是真机抓到的,不是推断;版本基准详见正文前的【版本基准说明】
【版本基准说明】(2026-09-21 补充)
- 基准版本 = PB10:本文示例与结论以 PB10 为准写作与验证;文中没有使用任何 PB11 及以后才出现的特性。
- PB12.5 对照实测:同一份源码在 PB 12.5 下同样「编译 + 运行」通过,输出串与 PB10 逐字节一致。
- PB12.5 差异:未发现。继承、覆盖、多态、生命周期顺序、错误号(C0015 / C0051 / C0123 / runtimeerror 2、23、30、65)两版完全相同。
- 标题后缀沿用发布当时的旧口径「PB12.5 实机验证」;实际基准为 PB10,PB12.5 为对照实测,特此说明。
一、先说清楚:PB 的"面向对象"和你想的不太一样
很多从 Java / C++ / C# 转过来写 PowerBuilder 的朋友,第一反应是"这不就是类嘛",然后就被几件事打脸:
- 没有接口(interface),也没有抽象类:只有单继承,一个类只能有一个祖先;
- 没有构造函数,也不能在 create 时传参:create nvo_rect 后面不能带括号参数,初始化只能靠 Constructor 事件或者自己写一个 of_init();
- create 语句不会自动触发 Constructor 事件(这一条最容易踩,后面第四节有实测证据);
- 方法全部是"虚"的:祖先类型变量装后代实例,调用一定落到后代实现,不需要写 virtual;
- 事件的继承规则和函数不一样:后代写了同名事件脚本,祖先那段不会自动执行。
本文就围绕"用户对象 → 继承 → 覆盖 → 多态"这条主线,把这五件事一个个讲透。所有结论后面都跟着实机输出,可复现。
二、用户对象(UserObject)的四种类型与选型
PB 里"用户对象"就是你自定义的类。按 是否可视 和 是否从系统类型派生 两个维度分成四种:
| 类型 | 祖先 | 有没有界面 | 典型用途 | 例子 | | 定制类用户对象(Custom Class) | nonvisualobject | 无 | 业务逻辑封装、算法、数据加工 | 本文的 nvo_shape | | 标准类用户对象(Standard Class) | transaction、message、timing、error、connection 等系统非可视类型 | 无 | 给系统对象加功能 | uo_database 继承 transaction | | 定制可视用户对象(Custom Visual) | userobject | 有 | 把一组控件打包成一个可复用控件 | 自定义的"部门选择组合框" | | 标准可视用户对象(Standard Visual) | commandbutton、singlelineedit、datawindow 等系统控件 | 有 | 给标准控件加统一行为 | 统一的"确定按钮"(含权限校验) |
选型建议:
- 纯逻辑(计算、转换、拼 SQL、调接口)→ 定制类用户对象,这是用得最多的;
- 想扩展系统对象(比如给 transaction 加 of_connect_by_config())→ 标准类用户对象,继承后你自己的方法直接挂上去;
- 窗口上有一组控件反复出现 → 定制可视用户对象;
- 想让全公司的按钮都自带权限判断 → 标准可视用户对象,然后所有按钮都换成它。
与结构体 / 全局函数的对比(什么时候不该用对象):
- 只是打包几个字段传参 → 用 Structure(结构),比对象轻;
- 与数据无关的纯工具函数(字符串补齐、日期格式化)→ 用 全局函数,不用 create 就能调;
- 需要"状态 + 行为 + 可被替换的实现"三者之一 → 才用对象。
三、创建与销毁:create / create using / destroy / IsValid
3.1 基本写法
- // 声明(此时只是一个空引用,IsValid = false)
- nvo_rect lr
- // 实例化
- lr = create nvo_rect
- // 用
- lr.of_setsize(3, 4)
- // 销毁
- destroy lr
复制代码
要点:
- create 与 destroy 必须配对,忘了 destroy 就是内存泄漏;
- destroy 后变量句柄并不会自动变空,但 IsValid() 会返回 false;
- destroy 一个 NULL 引用是安全的(实测不报错),所以"防御性 destroy"可以放心写;
- 销毁后再访问其方法 → runtimeerror 2(Null object reference),try-catch 能捕。
3.2 用字符串动态创建:create using
写插件、做"按配置决定用哪个实现类"的场景很有用:
- // 输入:要创建的类名(可来自配置文件 / 数据库)
- string ls_cls
- ls_cls = 'nvo_rect'
- // 输入:接收实例的祖先类型变量(用祖先类型接,才能接住任意后代)
- nvo_shape ln
- string ls_out
- ls_out = ''
- ln = create using ls_cls
- ls_out = 'class=' + ClassName(ln) + ',name=' + ln.of_name() + ',valid=' + String(IsValid(ln)) + ';'
- destroy ln
- ls_out += 'after destroy valid=' + String(IsValid(ln)) + ';'
- // 类名写错时:编译放行,运行时报错 runtimeerror 30
- ls_cls = 'nvo_notexist'
- try
- ln = create using ls_cls
- catch (runtimeerror le)
- ls_out += 'bad class err_no=' + String(le.number) + ';'
- end try
- MessageBox('create using 演示', ls_out)
复制代码
实机输出:
- class=nvo_rect,name=rect,valid=true;after destroy valid=false;bad class err_no=30;
复制代码
两个结论:
- create using 返回的对象,ClassName() 拿到的是真实类名,不是变量声明的祖先名;
- 类名不存在 → runtimeerror 30("Cannot find data type ..."),必须用 try-catch 兜住,否则整个应用崩掉。
3.3 自动实例化变量
在对象里写 nvo_rect lr_rect(不加 create),PB 会在宿主对象创建时自动 create。听起来省事,实际两个坑:
- 你没法控制创建时机,也没法传初始化参数;
- 宿主一创建它就存在,白白占资源。
建议:除了"必然要用到且无状态"的辅助对象,其余一律手动 create + destroy。
四、继承:实例变量作用域与访问权限
4.1 三种访问级别
实例变量声明时可以带修饰符(写在 Declare Instance Variables 里):
| 修饰符 | 自己可见 | 后代可见 | 外部可见 | 说明 | | public(默认) | ✔ | ✔ | ✔ | 外部可直接 lr.il_w | | protected | ✔ | ✔ | ✘ | 外部访问直接编译报错 | | private | ✔ | ✘ | ✘ | 后代也访问不到 |
实测:后代访问祖先的 private 变量 → 编译错误 C0015: Undefined variable: is_secret。也就是说 private 在 PB 里是"对后代也不开放"的,想给后代用就必须 protected。
4.2 三层继承示例(本文的示例族)
后面所有演示都基于这一族对象,它们的关系是:
- nvo_shape (祖先:定制类用户对象)
- ├── nvo_rect (覆盖了 of_name / of_area / of_where;新增 of_setsize / of_onlyrect / of_tag 两参版)
- │ └── nvo_square (只覆盖 of_name / of_where;of_area 不覆盖,沿用 nvo_rect 的)
- └── nvo_circle (覆盖了 of_name / of_area;新增 of_setr)
复制代码
祖先 nvo_shape(新建 Custom Class UserObject,保存为 nvo_shape):
- // ============ nvo_shape 源文件的 PowerScript 部分 ============
- // 实例变量:构造/析构日志(供生命周期演示用)
- // string is_log
- // string is_dtorfile
- forward prototypes
- public function string of_name ()
- public function double of_area ()
- public function string of_desc ()
- public function string of_where ()
- public function string of_tag (string as_pfx)
- public function string of_log ()
- end prototypes
- public function string of_name ();
- return 'shape'
- end function
- public function double of_area ();
- return 0
- end function
- // 关键:祖先方法里调用 of_name / of_area,这两个都可以被后代覆盖
- public function string of_desc ();
- return 'name=' + of_name() + ',area=' + String(of_area())
- end function
- public function string of_where ();
- return 'shape'
- end function
- public function string of_tag (string as_pfx);
- return 'shape:' + as_pfx
- end function
- public function string of_log ();
- return is_log
- end function
- on nvo_shape.create
- call super::create
- TriggerEvent( this, "constructor" )
- end on
- on nvo_shape.destroy
- TriggerEvent( this, "destructor" )
- call super::destroy
- end on
- event constructor;
- is_log += 'shape.ctor;'
- end event
- event destructor;
- int li_f
- is_log += 'shape.dtor;'
- if Len(is_dtorfile) > 0 then
- li_f = FileOpen(is_dtorfile, LineMode!, Write!, LockWrite!, Append!)
- if li_f > 0 then
- FileWrite(li_f, 'shape.dtor')
- FileClose(li_f)
- end if
- end if
- end event
复制代码注意 on nvo_shape.create 里的 TriggerEvent(this, "constructor"):这一行不是可选的装饰,它是 Constructor 事件唯一的触发来源。去掉它,Constructor 一次都不会执行(见 8.1 的实测)。
后裔 nvo_rect(New → Object → 选 Standard/Custom Class,点 "Inherit From" 选 nvo_shape):
- // ============ nvo_rect 源文件的 PowerScript 部分 ============
- // 实例变量:长、宽
- // long il_w
- // long il_h
- forward prototypes
- public function string of_name ()
- public function double of_area ()
- public function string of_where ()
- public subroutine of_setsize (long al_w, long al_h)
- public function string of_onlyrect ()
- public function string of_tag (string as_pfx, long al_n)
- end prototypes
- // 覆盖祖先同名同参同返回类型的方法
- public function string of_name ();
- return 'rect'
- end function
- public function double of_area ();
- return il_w * il_h
- end function
- // super:: 调祖先实现,再拼自己的
- public function string of_where ();
- return 'rect->' + super::of_where()
- end function
- public subroutine of_setsize (long al_w, long al_h);
- il_w = al_w
- il_h = al_h
- end subroutine
- // 后代独有的方法:祖先类型的变量【直接调】会编译不过
- public function string of_onlyrect ();
- return 'onlyrect'
- end function
- // 同名不同参 = 重载(不是覆盖)
- public function string of_tag (string as_pfx, long al_n);
- return 'rect2:' + as_pfx + ':' + String(al_n)
- end function
- // 后代层不要重复写 TriggerEvent,只 call super:: 即可
- on nvo_rect.create
- call super::create
- end on
- on nvo_rect.destroy
- call super::destroy
- end on
- event constructor;
- call super::constructor
- is_log += 'rect.ctor;'
- end event
- event destructor;
- int li_f
- if Len(is_dtorfile) > 0 then
- li_f = FileOpen(is_dtorfile, LineMode!, Write!, LockWrite!, Append!)
- if li_f > 0 then
- FileWrite(li_f, 'rect.dtor')
- FileClose(li_f)
- end if
- end if
- call super::destructor
- end event
复制代码
后裔 nvo_circle:
- // 实例变量:半径
- // double idb_r
- forward prototypes
- public function string of_name ()
- public function double of_area ()
- public subroutine of_setr (double adb_r)
- end prototypes
- public function string of_name ();
- return 'circle'
- end function
- public function double of_area ();
- return 3.14159 * idb_r * idb_r
- end function
- public subroutine of_setr (double adb_r);
- idb_r = adb_r
- end subroutine
- on nvo_circle.create
- call super::create
- end on
- on nvo_circle.destroy
- call super::destroy
- end on
- event constructor;
- call super::constructor
- is_log += 'circle.ctor;'
- end event
复制代码
三层 nvo_square(祖先选 nvo_rect):故意不覆盖 of_area,用来演示"不覆盖就继承祖先实现":
- forward prototypes
- public function string of_name ()
- public function string of_where ()
- end prototypes
- public function string of_name ();
- return 'square'
- end function
- public function string of_where ();
- return 'square->' + super::of_where()
- end function
- on nvo_square.create
- call super::create
- end on
- on nvo_square.destroy
- call super::destroy
- end on
- event constructor;
- call super::constructor
- is_log += 'square.ctor;'
- end event
复制代码
五、覆盖(Override)与 super::
5.1 覆盖的三条硬规则
在后代里声明一个和祖先同名的方法即构成覆盖,必须同时满足:
| 项 | 要求 | 违反后果 | | 方法名 | 完全相同 | 不同名 = 新方法,与覆盖无关 | | 参数表(个数 + 类型 + 顺序) | 完全相同 | 参数不同 = 重载,不是覆盖 | | 返回值类型 | 完全相同 | C0123: Function of_y differs from ancestor only by return type. | | 访问级别 | 可以改(public → protected 编译也过) | 通过祖先类型变量仍能调到后代实现(见 5.3) |
5.2 super:: 逐级上溯
- // nvo_rect.of_where() -> 'rect->' + super::of_where() -> 'rect->shape'
- // nvo_square.of_where() -> 'square->' + super::of_where() -> 'square->rect->shape'
复制代码
实机输出:
- super_rect_where:rect->shape
- super_square_where:square->rect->shape
- super_square_area:25 // square 没写 of_area,直接用了 nvo_rect 的 il_w*il_h = 5*5
- super_square_name:square
复制代码
super:: 是"编译期静态解析"的:它永远指向"当前对象声明所在类的直接祖先",不会因为多态而变。这也是它和"直接调方法名"最大的区别——直接调方法名永远走多态,super:: 永远走祖先。
5.3 一个反直觉的实测:把可见性降为 protected 也拦不住多态
后代把祖先的 public function of_x() 改成 protected function of_x() 覆盖,编译照样通过;用祖先类型的变量去调,仍然执行到后代的 protected 实现(实测返回 child)。
结论:PB 的访问级别主要是编译期约束(外部不能直接点出来),一旦通过祖先引用进入多态通道,后代实现的可见性不再拦截。别指望用 protected 做运行时防护。
六、多态:祖先引用指向后代实例
6.1 核心演示
- // 输入:矩形的长与宽
- long ll_w
- ll_w = 3
- long ll_h
- ll_h = 4
- // 输入:圆的半径
- double ldb_r
- ldb_r = 2
- nvo_shape ln
- nvo_rect lr
- nvo_circle lc
- string ls_out
- ls_out = ''
- // 1) 祖先自己
- ln = create nvo_shape
- ls_out += 'base:' + ClassName(ln) + '/' + ln.of_name() + '/' + String(ln.of_area()) + ';'
- destroy ln
- // 2) 后代变量直接调
- lr = create nvo_rect
- lr.of_setsize(ll_w, ll_h)
- ls_out += 'direct:' + ClassName(lr) + '/' + lr.of_name() + '/' + String(lr.of_area()) + ';'
- // 3) ★祖先变量装后代实例(向上转型,永远安全)
- ln = lr
- ls_out += 'upcast:' + ClassName(ln) + '/' + ln.of_name() + '/' + String(ln.of_area()) + ';'
- // 4) ★祖先方法 of_desc 内部调 of_name / of_area,仍然落到后代实现
- ls_out += 'desc:' + ln.of_desc() + ';'
- destroy lr
- // 5) 换一个后代,同一句 ln.of_area() 行为就变了
- lc = create nvo_circle
- lc.of_setr(ldb_r)
- ln = lc
- ls_out += 'circle:' + ClassName(ln) + '/' + ln.of_name() + '/' + String(ln.of_area()) + ';'
- destroy lc
- MessageBox('多态演示', ls_out)
复制代码
实机输出:
- base:nvo_shape/shape/0;direct:nvo_rect/rect/12;upcast:nvo_rect/rect/12;
- desc:name=rect,area=12;circle:nvo_circle/circle/12.56636;
复制代码
这张输出说明四件事:
- ClassName(ln) 永远返回真实类名 nvo_rect,尽管 ln 声明为 nvo_shape;
- ln.of_name() / ln.of_area() 走的是 nvo_rect 的实现 → 方法默认是虚的;
- ln.of_desc() 是祖先写的方法,它内部调 of_name(),出来的还是 rect → 即使在祖先代码内部,调用也是多态的,这一点和 C++ 非虚函数的行为完全不同;
- 同一个 ln 变量,换一个后代实例,of_area() 结果立刻不同 → 这就是"一份调用、多种实现"。
6.2 向下转型:编译放你过,运行时翻脸
- nvo_shape ln
- nvo_rect lr
- ln = create nvo_shape
- try
- lr = ln // 编译通过!运行时才炸
- catch (runtimeerror le)
- // 实测 err_no = 23
- end try
复制代码
实测:编译 0 错,运行时抛 runtimeerror 23:Cannot assign object of type nvo_shape to variable of type nvo_rect。try-catch 能捕。
建议:需要"从祖先引用拿回后代能力"时,别硬转,改用下面两种方式之一:
- 方案 A(推荐):把需要的能力提升到祖先(哪怕祖先给个空实现 / 返回 false);
- 方案 B:用 DYNAMIC 调用(见 6.3),并配 try-catch。
6.3 DYNAMIC:绕过编译期检查
DYNAMIC 告诉编译器"别查这个方法存不存在,运行时再说":
- nvo_shape ln
- nvo_rect lr
- string ls_out
- lr = create nvo_rect
- ln = lr
- ls_out = ''
- // 祖先 nvo_shape 根本没有 of_onlyrect,但 DYNAMIC 可以调
- ls_out += 'dyn:' + ln.DYNAMIC of_onlyrect() + ';'
- // 连不存在的方法也能编译过,运行时报错 runtimeerror 65
- try
- ls_out += 'miss:' + ln.DYNAMIC of_nosuch() + ';'
- catch (runtimeerror le)
- ls_out += 'miss:err_no=' + String(le.number) + ';'
- end try
- destroy lr
- MessageBox('DYNAMIC 演示', ls_out)
复制代码
实机输出:dyn nlyrect;miss:err_no=65;
对照:不用 DYNAMIC,直接 ln.of_onlyrect() → 编译错误 C0051: Unknown function name: of_onlyrect。
什么时候用:写框架、写通用事件转发、写"我不知道具体类但有约定方法名"的插件机制。业务代码里滥用会让编译期保护全部失效,能静态调就静态调。
七、重载:跨继承不会"隐藏"祖先版本
后代声明一个同名但参数不同的方法 = 重载。实测结论很关键:
- // 祖先:of_tag(string) -> 'shape:A'
- // 后代:of_tag(string, long) -> 'rect2:A:7'
- nvo_rect lr
- nvo_shape ln
- lr = create nvo_rect
- lr.of_tag('A', 7) // -> 'rect2:A:7' 后代新增的重载
- lr.of_tag('A') // -> 'shape:A' ★跨继承重载有效,祖先版本没被隐藏
- ln = lr
- ln.of_tag('A') // -> 'shape:A' 祖先变量只能看见祖先声明的签名
- destroy lr
复制代码
和 C++ / Java 的区别:C++ 里派生类声明同名函数会隐藏基类所有同名版本(除非 using Base::f;);PB 不会隐藏,后代变量照常能调到祖先的重载版本。
但要注意:通过祖先类型变量调用时,只能解析到祖先声明过的签名。想调后代独有的重载,要么用后代类型变量,要么用 DYNAMIC。
八、事件与生命周期:Constructor / Destructor 的三个反直觉点
8.1 create 语句不会自动触发 Constructor
这是最容易被 IDE 默认模板骗到的一条。做对照实验:两个只差一行的根对象。
- // nvo_c2:create 块里【不写】TriggerEvent
- on nvo_c2.create
- call super::create
- end on
- event constructor;
- is_log += 'c2.ctor;'
- end event
复制代码- // nvo_c3:create 块里【写】TriggerEvent
- on nvo_c3.create
- call super::create
- TriggerEvent( this, "constructor" )
- end on
- event constructor;
- is_log += 'c3.ctor;'
- end event
复制代码
实机输出:
- ctor_c2:<> // 空的!Constructor 一次都没跑
- ctor_c3:<c3.ctor;> // 跑了一次
复制代码
结论:create 只是分配内存,Constructor 事件完全靠 on <对象>.create 块里的 TriggerEvent(this, "constructor") 触发。手写源码/清理源码时如果不小心删掉这行,你的初始化代码就静默失效了——不报错、不警告,就是没跑。
配套的第二个坑:如果继承链上每一层都写了 TriggerEvent(PB IDE 早期模板在某些场景下会这样),那么 Constructor 会被触发 N 次(N = 继承层数)。所以正确做法是:
- 只有最"根"的那一层(from nonvisualobject 的那个)写 TriggerEvent;
- 后代层只写 call super::create,让触发器沿链上溯,只触发一次。
8.2 后代事件脚本会【覆盖】祖先事件脚本
再做一个对照:后代 nvo_c4 继承 nvo_shape(祖先有 constructor 脚本),自己定义了 constructor 但不写 call super:::
- ctor_c4:<c4.ctor;> // 只有后代的,祖先 shape.ctor 没执行
- ctor_rect:<shape.ctor;rect.ctor;> // 写了 call super::constructor,祖先的才执行
- ctor_square:<shape.ctor;rect.ctor;square.ctor;> // 三层链全执行
复制代码
结论:
- 函数是"虚"的:后代覆盖后,祖先版本可通过 super:: 精确调到;
- 事件是"覆盖"的:后代写了同名事件脚本,祖先那段默认不会执行,必须显式写 call super::constructor / call super::destructor。
在 PB IDE 里新建后代事件脚本时,默认勾选项叫 Extend Ancestor Script(扩展祖先脚本),勾上相当于自动帮你做 call super::。但手写 .sru 源码、或从别处拷贝源码时这个标记不会带过来,于是就变成纯覆盖。写文章/交付源码时务必手动补 call super::。
8.3 析构顺序:后代先、祖先后
因为 destructor 事件没法在 destroy 之后读实例变量,验证手段是让每一层 destructor 往同一个文件追加一行:
- // 前置:nvo_shape 有实例变量 is_dtorfile(字符串,文件名)
- // 步骤:1) 建 nvo_square 实例;2) 给 is_dtorfile 赋目标文件名;3) destroy;4) 读回文件
- nvo_square lq
- string ls_file
- int li_f
- string ls_line
- string ls_all
- string ls_out
- ls_file = 'diag_lifecycle.txt'
- ls_out = ''
- ls_all = ''
- // 先清空旧文件
- li_f = FileOpen(ls_file, LineMode!, Write!, LockWrite!, Replace!)
- if li_f > 0 then
- FileClose(li_f)
- end if
- lq = create nvo_square
- lq.is_dtorfile = ls_file
- destroy lq
- li_f = FileOpen(ls_file, LineMode!, Read!, LockRead!)
- if li_f > 0 then
- do while FileRead(li_f, ls_line) >= 0
- if Len(ls_all) > 0 then
- ls_all += '|'
- end if
- ls_all += ls_line
- loop
- FileClose(li_f)
- end if
- ls_out = 'dtor order: ' + ls_all
- MessageBox('析构顺序', ls_out)
复制代码
实机输出:dtor order: rect.dtor|shape.dtor
顺序:后代(nvo_rect)先,祖先(nvo_shape)后——和 C++ 析构顺序一致。nvo_square 自己没写 destructor,所以它继承并执行了最近祖先 nvo_rect 的那一份。
8.4 常用系统事件速查
| 事件 | 触发时机 | 常见用途 | | constructor | create 后(需 TriggerEvent) | 初始化实例变量、建内部对象 | | destructor | destroy 前(需 TriggerEvent) | 释放内部对象、关文件、清缓存 | | timer | timing 派生对象按间隔 | 轮询任务 | | error | 运行时错误发生在对象内 | 对象级兜底 | | ue_xxx(自定义) | 自己 TriggerEvent / PostEvent | 业务回调 |
九、对象作参数与返回值:传的是引用
对象变量本身是引用,赋值不会复制对象:
- nvo_rect lr1
- nvo_rect lr2
- lr1 = create nvo_rect
- lr1.of_setsize(3, 4)
- lr2 = lr1 // 只是多了一个引用,没有第二个对象
- lr2.of_setsize(10, 10)
- // 此时 lr1.of_area() 也是 100
- destroy lr1
- // 注意:lr2 现在指向已释放内存,IsValid(lr2) = false,再访问 runtimeerror 2
复制代码
函数传参同理:of_calc(nvo_shape ash) 传进去的是引用,函数内部改对象状态会影响调用方。想"只读"就在文档里约定清楚,PB 没有 const。
想让函数返回对象 → 返回祖先类型,内部 return lr(向上转型,安全)。
十、常见坑清单(全部实测)
| # | 现象 | 实测结果 | 正确做法 | | 1 | 以为 create 会自动跑 Constructor | 不会,必须 TriggerEvent(this,"constructor") | 保留模板里的 TriggerEvent;清理源码时别删 | | 2 | 继承链每层都写 TriggerEvent | Constructor 被触发 N 次(N=层数) | 只在根层写,后代只 call super::create | | 3 | 后代写了同名事件脚本 | 祖先事件不执行 | 显式 call super::<event> | | 4 | 后代访问祖先 private 变量 | 编译 C0015 | 需要给后代用就改 protected | | 5 | 覆盖时只改返回值类型 | 编译 C0123 | 返回值必须一致,要不同类型就换方法名 | | 6 | 祖先变量静态调后代独有方法 | 编译 C0051 | 用 DYNAMIC,或把方法提升到祖先 | | 7 | 向下转型(后代变量 = 祖先实例) | 编译过,运行时 runtimeerror 23 | 别转;提升方法到祖先 | | 8 | create using 类名写错 | 运行时 runtimeerror 30 | try-catch 兜住 | | 9 | DYNAMIC 调不存在的方法 | 运行时 runtimeerror 65 | try-catch 兜住 | | 10 | destroy 后再访问对象 | runtimeerror 2(Null object reference) | destroy 后把变量置空 / 用 IsValid 判 | | 11 | destroy 一个 NULL 引用 | 不报错,静默通过 | 可放心写防御性 destroy | | 12 | IsValid() 对 SetNull 过的对象引用 | 返回 NULL,会把拼接的字符串整条污染成 NULL(写出文件变 0 字节) | 别对对象引用用 SetNull;判空统一用 IsValid() | | 13 | 以为后代方法会隐藏祖先同名重载 | 不会隐藏,跨继承重载有效 | 放心重载 | | 14 | 用 protected 挡住多态调用 | 挡不住,祖先引用照样调到后代实现 | 别把 protected 当运行时防线 | | 15 | 忘了 destroy | 内存泄漏 | create / destroy 配对,长生命周期对象集中管理 |
十一、完整可运行示例(照做即可)
前置条件:新建/打开一个 PBL,按 4.2 节依次创建 4 个定制类用户对象 nvo_shape → nvo_rect → nvo_circle → nvo_square(注意 nvo_square 的祖先选 nvo_rect),源码按上面四段贴进去。
步骤:
- 新建窗口 w_oo_demo,放一个按钮 cb_run;
- 把下面代码贴进 cb_run 的 clicked 事件;
- 运行窗口,点击按钮,MessageBox 会打出全部验证结果。
- // 输入:矩形长宽(直接给示例值,读者可改)
- long ll_w
- ll_w = 3
- long ll_h
- ll_h = 4
- // 输入:圆半径
- double ldb_r
- ldb_r = 2
- nvo_shape ln
- nvo_rect lr
- nvo_circle lc
- nvo_square lq
- string ls_out
- ls_out = ''
- // 1) 多态
- ln = create nvo_shape
- ls_out += 'base:' + ClassName(ln) + '/' + ln.of_name() + ';'
- destroy ln
- lr = create nvo_rect
- lr.of_setsize(ll_w, ll_h)
- ln = lr
- ls_out += 'upcast:' + ClassName(ln) + '/' + ln.of_name() + '/' + String(ln.of_area()) + ';'
- ls_out += 'desc:' + ln.of_desc() + ';'
- destroy lr
- lc = create nvo_circle
- lc.of_setr(ldb_r)
- ln = lc
- ls_out += 'circle:' + ln.of_name() + '/' + String(ln.of_area()) + ';'
- destroy lc
- // 2) super 链与继承
- lq = create nvo_square
- lq.of_setsize(5, 5)
- ls_out += 'where:' + lq.of_where() + ';'
- ls_out += 'area(inherit):' + String(lq.of_area()) + ';'
- ls_out += 'ctor:' + lq.of_log() + ';'
- destroy lq
- // 3) 跨继承重载
- lr = create nvo_rect
- ls_out += 'tag2:' + lr.of_tag('A', 7) + ';'
- ls_out += 'tag1:' + lr.of_tag('A') + ';'
- destroy lr
- // 4) DYNAMIC
- lr = create nvo_rect
- ln = lr
- ls_out += 'dyn:' + ln.DYNAMIC of_onlyrect() + ';'
- destroy lr
- MessageBox('面向对象演示结果', ls_out)
复制代码
预期输出(PB 12.5 实机):
- base:nvo_shape/shape;upcast:nvo_rect/rect/12;desc:name=rect,area=12;
- circle:circle/12.56636;where:square->rect->shape;area(inherit):25;
- ctor:shape.ctor;rect.ctor;square.ctor;;tag2:rect2:A:7;tag1:shape:A;dyn:onlyrect;
复制代码
十二、本文的实机验证情况
- 环境:PB10 与 PB 12.5 双版本,Windows,PBVM 无界面模式运行,仅用标准 PowerScript、不依赖任何第三方库。作基准的是 PB10,PB12.5 为对照。
- 验证项(两版各做一遍):
- 10 个 .sru 源文件导入测试 PBL:PB10 0 错误 / PB12.5 0 错误;
- pypower build rebuild --type full 全量编译:PB10 0 错误 / PB12.5 0 错误;
- PBVM 运行 .pbtest 硬断言:PB10 26 条全中 PASS / PB12.5 26 条全中 PASS(文章示例代码的 9 条断言两版同样全中);
- 两版输出串逐字节一致,未发现版本差异;
- check_pb125.py 结构/API 自检:0 错误 0 提醒;check_enc.py 编码自检:bad = 0。
- 未实测说明:可视用户对象(Custom/Standard Visual)的界面行为依赖真实窗口与消息循环,本文只给出选型与用法说明,未做实机界面验证;PostEvent 属异步投递,需要窗口消息循环,本节只讲 TriggerEvent 同步路径。
- 附件:本文示例已整理为 PB10 兼容版 PBL 并打包(见楼下回帖),含全部源码与说明文档。
一句话记住:PB 里"方法默认虚、事件默认覆盖、create 不自动构造" —— 这三条想明白了,PB 的面向对象就不会再有意外。 |