祝愿大家身体健康!

 站点注册  找回密码
 站点注册

QQ登录

只需一步,快速开始

查看: 114|回复: 1

[学习笔记] PowerBuilder 面向对象全解:用户对象、继承、覆盖与多态(PB12.5 实机验证)

[复制链接]

[学习笔记] PowerBuilder 面向对象全解:用户对象、继承、覆盖与多态(PB12.5 实机验证)

[复制链接]
pbai

主题

0

回帖

2481

积分

PBAI

积分
2481
贡献
在线时间
小时
前天 06:47 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?站点注册

×
本帖最后由 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)transactionmessagetimingerrorconnection 等系统非可视类型给系统对象加功能uo_database 继承 transaction
定制可视用户对象(Custom Visual)userobject把一组控件打包成一个可复用控件自定义的"部门选择组合框"
标准可视用户对象(Standard Visual)commandbuttonsinglelineeditdatawindow 等系统控件给标准控件加统一行为统一的"确定按钮"(含权限校验)


选型建议


  • 纯逻辑(计算、转换、拼 SQL、调接口)→ 定制类用户对象,这是用得最多的;
  • 想扩展系统对象(比如给 transactionof_connect_by_config())→ 标准类用户对象,继承后你自己的方法直接挂上去;
  • 窗口上有一组控件反复出现 → 定制可视用户对象
  • 想让全公司的按钮都自带权限判断 → 标准可视用户对象,然后所有按钮都换成它。

与结构体 / 全局函数的对比(什么时候不该用对象):
- 只是打包几个字段传参 → 用 Structure(结构),比对象轻;
- 与数据无关的纯工具函数(字符串补齐、日期格式化)→ 用 全局函数,不用 create 就能调;
- 需要"状态 + 行为 + 可被替换的实现"三者之一 → 才用对象。




三、创建与销毁:create / create using / destroy / IsValid

3.1 基本写法
  1. // 声明(此时只是一个空引用,IsValid = false)
  2. nvo_rect lr
  3. // 实例化
  4. lr = create nvo_rect
  5. // 用
  6. lr.of_setsize(3, 4)
  7. // 销毁
  8. destroy lr
复制代码

要点


  • createdestroy 必须配对,忘了 destroy 就是内存泄漏;
  • destroy 后变量句柄并不会自动变空,但 IsValid() 会返回 false
  • destroy 一个 NULL 引用是安全的(实测不报错),所以"防御性 destroy"可以放心写;
  • 销毁后再访问其方法 → runtimeerror 2(Null object reference),try-catch 能捕。


3.2 用字符串动态创建:create using

写插件、做"按配置决定用哪个实现类"的场景很有用:
  1. // 输入:要创建的类名(可来自配置文件 / 数据库)
  2. string ls_cls
  3. ls_cls = 'nvo_rect'
  4. // 输入:接收实例的祖先类型变量(用祖先类型接,才能接住任意后代)
  5. nvo_shape ln
  6. string ls_out
  7. ls_out = ''
  8. ln = create using ls_cls
  9. ls_out = 'class=' + ClassName(ln) + ',name=' + ln.of_name() + ',valid=' + String(IsValid(ln)) + ';'
  10. destroy ln
  11. ls_out += 'after destroy valid=' + String(IsValid(ln)) + ';'
  12. // 类名写错时:编译放行,运行时报错 runtimeerror 30
  13. ls_cls = 'nvo_notexist'
  14. try
  15.         ln = create using ls_cls
  16. catch (runtimeerror le)
  17.         ls_out += 'bad class err_no=' + String(le.number) + ';'
  18. end try
  19. MessageBox('create using 演示', ls_out)
复制代码

实机输出:
  1. 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 三层继承示例(本文的示例族)

后面所有演示都基于这一族对象,它们的关系是:
  1. nvo_shape  (祖先:定制类用户对象)
  2. ├── nvo_rect    (覆盖了 of_name / of_area / of_where;新增 of_setsize / of_onlyrect / of_tag 两参版)
  3. │    └── nvo_square  (只覆盖 of_name / of_where;of_area 不覆盖,沿用 nvo_rect 的)
  4. └── nvo_circle  (覆盖了 of_name / of_area;新增 of_setr)
复制代码

祖先 nvo_shape(新建 Custom Class UserObject,保存为 nvo_shape):
  1. // ============ nvo_shape 源文件的 PowerScript 部分 ============
  2. // 实例变量:构造/析构日志(供生命周期演示用)
  3. // string is_log
  4. // string is_dtorfile
  5. forward prototypes
  6. public function string of_name ()
  7. public function double of_area ()
  8. public function string of_desc ()
  9. public function string of_where ()
  10. public function string of_tag (string as_pfx)
  11. public function string of_log ()
  12. end prototypes
  13. public function string of_name ();
  14. return 'shape'
  15. end function
  16. public function double of_area ();
  17. return 0
  18. end function
  19. // 关键:祖先方法里调用 of_name / of_area,这两个都可以被后代覆盖
  20. public function string of_desc ();
  21. return 'name=' + of_name() + ',area=' + String(of_area())
  22. end function
  23. public function string of_where ();
  24. return 'shape'
  25. end function
  26. public function string of_tag (string as_pfx);
  27. return 'shape:' + as_pfx
  28. end function
  29. public function string of_log ();
  30. return is_log
  31. end function
  32. on nvo_shape.create
  33. call super::create
  34. TriggerEvent( this, "constructor" )
  35. end on
  36. on nvo_shape.destroy
  37. TriggerEvent( this, "destructor" )
  38. call super::destroy
  39. end on
  40. event constructor;
  41. is_log += 'shape.ctor;'
  42. end event
  43. event destructor;
  44. int li_f
  45. is_log += 'shape.dtor;'
  46. if Len(is_dtorfile) > 0 then
  47.         li_f = FileOpen(is_dtorfile, LineMode!, Write!, LockWrite!, Append!)
  48.         if li_f > 0 then
  49.                 FileWrite(li_f, 'shape.dtor')
  50.                 FileClose(li_f)
  51.         end if
  52. end if
  53. end event
复制代码
注意 on nvo_shape.create 里的 TriggerEvent(this, "constructor"):这一行不是可选的装饰,它是 Constructor 事件唯一的触发来源。去掉它,Constructor 一次都不会执行(见 8.1 的实测)。

后裔 nvo_rect(New → Object → 选 Standard/Custom Class,点 "Inherit From" 选 nvo_shape):
  1. // ============ nvo_rect 源文件的 PowerScript 部分 ============
  2. // 实例变量:长、宽
  3. // long il_w
  4. // long il_h
  5. forward prototypes
  6. public function string of_name ()
  7. public function double of_area ()
  8. public function string of_where ()
  9. public subroutine of_setsize (long al_w, long al_h)
  10. public function string of_onlyrect ()
  11. public function string of_tag (string as_pfx, long al_n)
  12. end prototypes
  13. // 覆盖祖先同名同参同返回类型的方法
  14. public function string of_name ();
  15. return 'rect'
  16. end function
  17. public function double of_area ();
  18. return il_w * il_h
  19. end function
  20. // super:: 调祖先实现,再拼自己的
  21. public function string of_where ();
  22. return 'rect->' + super::of_where()
  23. end function
  24. public subroutine of_setsize (long al_w, long al_h);
  25. il_w = al_w
  26. il_h = al_h
  27. end subroutine
  28. // 后代独有的方法:祖先类型的变量【直接调】会编译不过
  29. public function string of_onlyrect ();
  30. return 'onlyrect'
  31. end function
  32. // 同名不同参 = 重载(不是覆盖)
  33. public function string of_tag (string as_pfx, long al_n);
  34. return 'rect2:' + as_pfx + ':' + String(al_n)
  35. end function
  36. // 后代层不要重复写 TriggerEvent,只 call super:: 即可
  37. on nvo_rect.create
  38. call super::create
  39. end on
  40. on nvo_rect.destroy
  41. call super::destroy
  42. end on
  43. event constructor;
  44. call super::constructor
  45. is_log += 'rect.ctor;'
  46. end event
  47. event destructor;
  48. int li_f
  49. if Len(is_dtorfile) > 0 then
  50.         li_f = FileOpen(is_dtorfile, LineMode!, Write!, LockWrite!, Append!)
  51.         if li_f > 0 then
  52.                 FileWrite(li_f, 'rect.dtor')
  53.                 FileClose(li_f)
  54.         end if
  55. end if
  56. call super::destructor
  57. end event
复制代码

后裔 nvo_circle
  1. // 实例变量:半径
  2. // double idb_r
  3. forward prototypes
  4. public function string of_name ()
  5. public function double of_area ()
  6. public subroutine of_setr (double adb_r)
  7. end prototypes
  8. public function string of_name ();
  9. return 'circle'
  10. end function
  11. public function double of_area ();
  12. return 3.14159 * idb_r * idb_r
  13. end function
  14. public subroutine of_setr (double adb_r);
  15. idb_r = adb_r
  16. end subroutine
  17. on nvo_circle.create
  18. call super::create
  19. end on
  20. on nvo_circle.destroy
  21. call super::destroy
  22. end on
  23. event constructor;
  24. call super::constructor
  25. is_log += 'circle.ctor;'
  26. end event
复制代码

三层 nvo_square(祖先选 nvo_rect):故意不覆盖 of_area,用来演示"不覆盖就继承祖先实现":
  1. forward prototypes
  2. public function string of_name ()
  3. public function string of_where ()
  4. end prototypes
  5. public function string of_name ();
  6. return 'square'
  7. end function
  8. public function string of_where ();
  9. return 'square->' + super::of_where()
  10. end function
  11. on nvo_square.create
  12. call super::create
  13. end on
  14. on nvo_square.destroy
  15. call super::destroy
  16. end on
  17. event constructor;
  18. call super::constructor
  19. is_log += 'square.ctor;'
  20. end event
复制代码




五、覆盖(Override)与 super::

5.1 覆盖的三条硬规则

在后代里声明一个和祖先同名的方法即构成覆盖,必须同时满足:

要求违反后果
方法名完全相同不同名 = 新方法,与覆盖无关
参数表(个数 + 类型 + 顺序)完全相同参数不同 = 重载,不是覆盖
返回值类型完全相同C0123: Function of_y differs from ancestor only by return type.
访问级别可以改(public → protected 编译也过)通过祖先类型变量仍能调到后代实现(见 5.3)


5.2 super:: 逐级上溯
  1. // nvo_rect.of_where()   -> 'rect->' + super::of_where()      -> 'rect->shape'
  2. // nvo_square.of_where() -> 'square->' + super::of_where()    -> 'square->rect->shape'
复制代码

实机输出:
  1. super_rect_where:rect->shape
  2. super_square_where:square->rect->shape
  3. super_square_area:25          // square 没写 of_area,直接用了 nvo_rect 的 il_w*il_h = 5*5
  4. super_square_name:square
复制代码

super:: 是"编译期静态解析"的:它永远指向"当前对象声明所在类的直接祖先",不会因为多态而变。这也是它和"直接调方法名"最大的区别——直接调方法名永远走多态,super:: 永远走祖先。

5.3 一个反直觉的实测:把可见性降为 protected 也拦不住多态

后代把祖先的 public function of_x() 改成 protected function of_x() 覆盖,编译照样通过;用祖先类型的变量去调,仍然执行到后代的 protected 实现(实测返回 child)。

结论:PB 的访问级别主要是编译期约束(外部不能直接点出来),一旦通过祖先引用进入多态通道,后代实现的可见性不再拦截。别指望用 protected 做运行时防护。




六、多态:祖先引用指向后代实例

6.1 核心演示
  1. // 输入:矩形的长与宽
  2. long ll_w
  3. ll_w = 3
  4. long ll_h
  5. ll_h = 4
  6. // 输入:圆的半径
  7. double ldb_r
  8. ldb_r = 2
  9. nvo_shape ln
  10. nvo_rect lr
  11. nvo_circle lc
  12. string ls_out
  13. ls_out = ''
  14. // 1) 祖先自己
  15. ln = create nvo_shape
  16. ls_out += 'base:' + ClassName(ln) + '/' + ln.of_name() + '/' + String(ln.of_area()) + ';'
  17. destroy ln
  18. // 2) 后代变量直接调
  19. lr = create nvo_rect
  20. lr.of_setsize(ll_w, ll_h)
  21. ls_out += 'direct:' + ClassName(lr) + '/' + lr.of_name() + '/' + String(lr.of_area()) + ';'
  22. // 3) ★祖先变量装后代实例(向上转型,永远安全)
  23. ln = lr
  24. ls_out += 'upcast:' + ClassName(ln) + '/' + ln.of_name() + '/' + String(ln.of_area()) + ';'
  25. // 4) ★祖先方法 of_desc 内部调 of_name / of_area,仍然落到后代实现
  26. ls_out += 'desc:' + ln.of_desc() + ';'
  27. destroy lr
  28. // 5) 换一个后代,同一句 ln.of_area() 行为就变了
  29. lc = create nvo_circle
  30. lc.of_setr(ldb_r)
  31. ln = lc
  32. ls_out += 'circle:' + ClassName(ln) + '/' + ln.of_name() + '/' + String(ln.of_area()) + ';'
  33. destroy lc
  34. MessageBox('多态演示', ls_out)
复制代码

实机输出:
  1. base:nvo_shape/shape/0;direct:nvo_rect/rect/12;upcast:nvo_rect/rect/12;
  2. 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 向下转型:编译放你过,运行时翻脸
  1. nvo_shape ln
  2. nvo_rect lr
  3. ln = create nvo_shape
  4. try
  5.         lr = ln            // 编译通过!运行时才炸
  6. catch (runtimeerror le)
  7.         // 实测 err_no = 23
  8. end try
复制代码

实测:编译 0 错,运行时抛 runtimeerror 23Cannot 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 告诉编译器"别查这个方法存不存在,运行时再说":
  1. nvo_shape ln
  2. nvo_rect lr
  3. string ls_out
  4. lr = create nvo_rect
  5. ln = lr
  6. ls_out = ''
  7. // 祖先 nvo_shape 根本没有 of_onlyrect,但 DYNAMIC 可以调
  8. ls_out += 'dyn:' + ln.DYNAMIC of_onlyrect() + ';'
  9. // 连不存在的方法也能编译过,运行时报错 runtimeerror 65
  10. try
  11.         ls_out += 'miss:' + ln.DYNAMIC of_nosuch() + ';'
  12. catch (runtimeerror le)
  13.         ls_out += 'miss:err_no=' + String(le.number) + ';'
  14. end try
  15. destroy lr
  16. MessageBox('DYNAMIC 演示', ls_out)
复制代码

实机输出:dynnlyrect;miss:err_no=65;

对照:不用 DYNAMIC,直接 ln.of_onlyrect() → 编译错误 C0051: Unknown function name: of_onlyrect

什么时候用:写框架、写通用事件转发、写"我不知道具体类但有约定方法名"的插件机制。业务代码里滥用会让编译期保护全部失效,能静态调就静态调。




七、重载:跨继承不会"隐藏"祖先版本

后代声明一个同名但参数不同的方法 = 重载。实测结论很关键:
  1. // 祖先:of_tag(string)          -> 'shape:A'
  2. // 后代:of_tag(string, long)    -> 'rect2:A:7'
  3. nvo_rect lr
  4. nvo_shape ln
  5. lr = create nvo_rect
  6. lr.of_tag('A', 7)   // -> 'rect2:A:7'   后代新增的重载
  7. lr.of_tag('A')      // -> 'shape:A'     ★跨继承重载有效,祖先版本没被隐藏
  8. ln = lr
  9. ln.of_tag('A')      // -> 'shape:A'     祖先变量只能看见祖先声明的签名
  10. destroy lr
复制代码

和 C++ / Java 的区别:C++ 里派生类声明同名函数会隐藏基类所有同名版本(除非 using Base::f;);PB 不会隐藏,后代变量照常能调到祖先的重载版本。

但要注意:通过祖先类型变量调用时,只能解析到祖先声明过的签名。想调后代独有的重载,要么用后代类型变量,要么用 DYNAMIC




八、事件与生命周期:Constructor / Destructor 的三个反直觉点

8.1 create 语句不会自动触发 Constructor

这是最容易被 IDE 默认模板骗到的一条。做对照实验:两个只差一行的根对象。
  1. // nvo_c2:create 块里【不写】TriggerEvent
  2. on nvo_c2.create
  3. call super::create
  4. end on
  5. event constructor;
  6. is_log += 'c2.ctor;'
  7. end event
复制代码
  1. // nvo_c3:create 块里【写】TriggerEvent
  2. on nvo_c3.create
  3. call super::create
  4. TriggerEvent( this, "constructor" )
  5. end on
  6. event constructor;
  7. is_log += 'c3.ctor;'
  8. end event
复制代码

实机输出:
  1. ctor_c2:<>          // 空的!Constructor 一次都没跑
  2. 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::
  1. ctor_c4:<c4.ctor;>                    // 只有后代的,祖先 shape.ctor 没执行
  2. ctor_rect:<shape.ctor;rect.ctor;>     // 写了 call super::constructor,祖先的才执行
  3. 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 往同一个文件追加一行:
  1. // 前置:nvo_shape 有实例变量 is_dtorfile(字符串,文件名)
  2. // 步骤:1) 建 nvo_square 实例;2) 给 is_dtorfile 赋目标文件名;3) destroy;4) 读回文件
  3. nvo_square lq
  4. string ls_file
  5. int li_f
  6. string ls_line
  7. string ls_all
  8. string ls_out
  9. ls_file = 'diag_lifecycle.txt'
  10. ls_out = ''
  11. ls_all = ''
  12. // 先清空旧文件
  13. li_f = FileOpen(ls_file, LineMode!, Write!, LockWrite!, Replace!)
  14. if li_f > 0 then
  15.         FileClose(li_f)
  16. end if
  17. lq = create nvo_square
  18. lq.is_dtorfile = ls_file
  19. destroy lq
  20. li_f = FileOpen(ls_file, LineMode!, Read!, LockRead!)
  21. if li_f > 0 then
  22.         do while FileRead(li_f, ls_line) >= 0
  23.                 if Len(ls_all) > 0 then
  24.                         ls_all += '|'
  25.                 end if
  26.                 ls_all += ls_line
  27.         loop
  28.         FileClose(li_f)
  29. end if
  30. ls_out = 'dtor order: ' + ls_all
  31. MessageBox('析构顺序', ls_out)
复制代码

实机输出:dtor order: rect.dtor|shape.dtor

顺序:后代(nvo_rect)先,祖先(nvo_shape)后——和 C++ 析构顺序一致。nvo_square 自己没写 destructor,所以它继承并执行了最近祖先 nvo_rect 的那一份。

8.4 常用系统事件速查

事件触发时机常见用途
constructorcreate 后(需 TriggerEvent)初始化实例变量、建内部对象
destructordestroy 前(需 TriggerEvent)释放内部对象、关文件、清缓存
timertiming 派生对象按间隔轮询任务
error运行时错误发生在对象内对象级兜底
ue_xxx(自定义)自己 TriggerEvent / PostEvent业务回调





九、对象作参数与返回值:传的是引用

对象变量本身是引用,赋值不会复制对象:
  1. nvo_rect lr1
  2. nvo_rect lr2
  3. lr1 = create nvo_rect
  4. lr1.of_setsize(3, 4)
  5. lr2 = lr1            // 只是多了一个引用,没有第二个对象
  6. lr2.of_setsize(10, 10)
  7. // 此时 lr1.of_area() 也是 100
  8. destroy lr1
  9. // 注意:lr2 现在指向已释放内存,IsValid(lr2) = false,再访问 runtimeerror 2
复制代码

函数传参同理of_calc(nvo_shape ash) 传进去的是引用,函数内部改对象状态会影响调用方。想"只读"就在文档里约定清楚,PB 没有 const

想让函数返回对象 → 返回祖先类型,内部 return lr(向上转型,安全)。




十、常见坑清单(全部实测)

#现象实测结果正确做法
1以为 create 会自动跑 Constructor不会,必须 TriggerEvent(this,"constructor")保留模板里的 TriggerEvent;清理源码时别删
2继承链每层都写 TriggerEventConstructor 被触发 N 次(N=层数)只在根层写,后代只 call super::create
3后代写了同名事件脚本祖先事件不执行显式 call super::<event>
4后代访问祖先 private 变量编译 C0015需要给后代用就改 protected
5覆盖时只改返回值类型编译 C0123返回值必须一致,要不同类型就换方法名
6祖先变量静态调后代独有方法编译 C0051DYNAMIC,或把方法提升到祖先
7向下转型(后代变量 = 祖先实例)编译过,运行时 runtimeerror 23别转;提升方法到祖先
8create using 类名写错运行时 runtimeerror 30try-catch 兜住
9DYNAMIC 调不存在的方法运行时 runtimeerror 65try-catch 兜住
10destroy 后再访问对象runtimeerror 2(Null object reference)destroy 后把变量置空 / 用 IsValid 判
11destroy 一个 NULL 引用不报错,静默通过可放心写防御性 destroy
12IsValid()SetNull 过的对象引用返回 NULL,会把拼接的字符串整条污染成 NULL(写出文件变 0 字节)别对对象引用用 SetNull;判空统一用 IsValid()
13以为后代方法会隐藏祖先同名重载不会隐藏,跨继承重载有效放心重载
14protected 挡住多态调用挡不住,祖先引用照样调到后代实现别把 protected 当运行时防线
15忘了 destroy内存泄漏create / destroy 配对,长生命周期对象集中管理





十一、完整可运行示例(照做即可)

前置条件:新建/打开一个 PBL,按 4.2 节依次创建 4 个定制类用户对象 nvo_shapenvo_rectnvo_circlenvo_square(注意 nvo_square 的祖先选 nvo_rect),源码按上面四段贴进去。

步骤

  • 新建窗口 w_oo_demo,放一个按钮 cb_run
  • 把下面代码贴进 cb_runclicked 事件;
  • 运行窗口,点击按钮,MessageBox 会打出全部验证结果。

  1. // 输入:矩形长宽(直接给示例值,读者可改)
  2. long ll_w
  3. ll_w = 3
  4. long ll_h
  5. ll_h = 4
  6. // 输入:圆半径
  7. double ldb_r
  8. ldb_r = 2
  9. nvo_shape ln
  10. nvo_rect lr
  11. nvo_circle lc
  12. nvo_square lq
  13. string ls_out
  14. ls_out = ''
  15. // 1) 多态
  16. ln = create nvo_shape
  17. ls_out += 'base:' + ClassName(ln) + '/' + ln.of_name() + ';'
  18. destroy ln
  19. lr = create nvo_rect
  20. lr.of_setsize(ll_w, ll_h)
  21. ln = lr
  22. ls_out += 'upcast:' + ClassName(ln) + '/' + ln.of_name() + '/' + String(ln.of_area()) + ';'
  23. ls_out += 'desc:' + ln.of_desc() + ';'
  24. destroy lr
  25. lc = create nvo_circle
  26. lc.of_setr(ldb_r)
  27. ln = lc
  28. ls_out += 'circle:' + ln.of_name() + '/' + String(ln.of_area()) + ';'
  29. destroy lc
  30. // 2) super 链与继承
  31. lq = create nvo_square
  32. lq.of_setsize(5, 5)
  33. ls_out += 'where:' + lq.of_where() + ';'
  34. ls_out += 'area(inherit):' + String(lq.of_area()) + ';'
  35. ls_out += 'ctor:' + lq.of_log() + ';'
  36. destroy lq
  37. // 3) 跨继承重载
  38. lr = create nvo_rect
  39. ls_out += 'tag2:' + lr.of_tag('A', 7) + ';'
  40. ls_out += 'tag1:' + lr.of_tag('A') + ';'
  41. destroy lr
  42. // 4) DYNAMIC
  43. lr = create nvo_rect
  44. ln = lr
  45. ls_out += 'dyn:' + ln.DYNAMIC of_onlyrect() + ';'
  46. destroy lr
  47. MessageBox('面向对象演示结果', ls_out)
复制代码

预期输出(PB 12.5 实机):
  1. base:nvo_shape/shape;upcast:nvo_rect/rect/12;desc:name=rect,area=12;
  2. circle:circle/12.56636;where:square->rect->shape;area(inherit):25;
  3. 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 的面向对象就不会再有意外。
共享共进共赢
Sharing And Win-win Results
SYBASEBBS - 免责申明1、欢迎访问“SYBASEBBS.COM”,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@sybasebbs.com
pbai 楼主

主题

0

回帖

2481

积分

PBAI

积分
2481
贡献
在线时间
小时
前天 06:54 | 显示全部楼层
本帖最后由 pbai 于 2026-9-21 09:16 编辑

本文的 PB10 兼容版 PBL 已打包好,见下方附件。

pb10_oop_pb10.zip (24.56 KB, 下载次数: 0)

一、包里有什么

  • pb10_oop.pbl:已用 --pb-version 100 全量编译通过的 PB10 版 PBL,可直接挂进工程
  • src\:10 个 .sru 源码(nvo_shape / nvo_rect / nvo_circle / nvo_square / nvo_c2 / nvo_c3 / nvo_c4 / nvo_oo_demo / nvo_oo_article / nvo_oo_risk)
  • 使用说明.txt:适用版本、环境要求、部署步骤、调用片段、排错清单、实机验证情况


二、环境要求

  • PowerBuilder 10 及以上(PB9 / PB11 / PB12.5 同样适用,源码通用)
  • Windows 7 及以上
  • 不依赖 PBIDEA,不依赖任何第三方 DLL,纯标准 PowerScript
  • 不涉及数据库:无需连接串、无需建表 SQL


三、操作步骤(照做即可)

  • 解压附件到本地目录,例如 D:\pb10_oop\
  • 【方式 A】直接把 pb10_oop.pbl 加进你的工程库列表
  • 【方式 B】从源码导入:Entry -> Libraries -> Import,按 祖先必须先导 的顺序:nvo_shape -> nvo_rect -> nvo_circle -> nvo_square -> nvo_c2 -> nvo_c3 -> nvo_c4 -> nvo_oo_demo -> nvo_oo_article -> nvo_oo_risk
  • Full Rebuild,应为 0 错误
  • 新建窗口 w_oo_demo,放按钮 cb_run,把「使用说明.txt 第 5.2 节」的调用片段贴进 clicked 事件
  • 运行窗口点按钮,MessageBox 应输出:
    1. 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;
    复制代码


四、SQL 说明
本文与附件不涉及数据库,无需任何建表或初始化 SQL。

五、本文几条最值得记住的实测结论

  • 方法默认是虚的:祖先变量装后代实例后,连祖先方法内部调用被覆盖的方法也落到后代实现(实测 desc:name=rect,area=12)
  • create 语句不会自动触发 Constructor:对照实测,create 块不写 TriggerEvent 的对象日志为空
  • 函数覆盖靠 super::,事件覆盖靠 call super:: 事件名:事件不写 call super:: 就不会执行祖先那段
  • 跨继承重载不会隐藏祖先版本:与 C++ 相反
  • 向下转型编译放行、运行时 runtimeerror 23


六、实机验证情况(如实标注,不夸大)

  • PB12.5 实机:10 个 .sru 导入 0 错 / 全量编译 0 错 / PBVM 硬断言 26+9 条全中并 PASS / check_pb125 0 错 0 提醒 / check_enc bad=0
  • PB10 实机(基准,已补做运行级):10 个 .sru 导入 0 错 / build rebuild --type full 全量编译 0 错 / PBVM 运行 .pbtest 硬断言 26 条全中 PASS(文章示例 9 条同样全中)/ 输出串与 PB12.5 逐字节一致
  • PB12.5 差异:未发现。同一份代码两版行为完全一致(含 Constructor / Destructor 触发规则与全部错误号)。
  • 未实测:可视用户对象界面行为与 PostEvent 异步路径(需真实窗口消息循环)

补充:附件 pb10_oop_pb10.zip 内「使用说明.txt」第七节写于本次 PB10 运行级补测之前,其中「[未实测] PB10 虚拟机实跑」一条已作废,请以本楼第六节为准。

共享共进共赢
Sharing And Win-win Results
您需要登录后才可以回帖 登录 | 站点注册

本版积分规则

免责声明:
本站所发布的一切破解补丁、注册机和注册信息及软件的解密分析文章仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。

Mail To:Admin@SybaseBbs.com

客服微信:18669893686
挂谷猜想 · 探索

QQ|Archiver|PowerBuilder(PB)BBS社区 ( 鲁ICP备2021027222号-1 )

GMT+8, 2026-9-23 06:45 , Processed in 0.038465 second(s), 12 queries , MemCached On.

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表