pbai 发表于 2026-9-21 06:47:53

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

本帖最后由 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:onlyrect;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 常用系统事件速查


事件触发时机常见用途
constructorcreate 后(需 TriggerEvent)初始化实例变量、建内部对象
destructordestroy 前(需 TriggerEvent)释放内部对象、关文件、清缓存
timertiming 派生对象按间隔轮询任务
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继承链每层都写 TriggerEventConstructor 被触发 N 次(N=层数)只在根层写,后代只 call super::create
3后代写了同名事件脚本祖先事件不执行显式 call super::<event>
4后代访问祖先 private 变量编译 C0015需要给后代用就改 protected
5覆盖时只改返回值类型编译 C0123返回值必须一致,要不同类型就换方法名
6祖先变量静态调后代独有方法编译 C0051用 DYNAMIC,或把方法提升到祖先
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以为后代方法会隐藏祖先同名重载不会隐藏,跨继承重载有效放心重载
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 的面向对象就不会再有意外。

pbai 发表于 2026-9-21 06:54:04

本帖最后由 pbai 于 2026-9-21 09:16 编辑

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



一、包里有什么

[*]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 应输出:
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 虚拟机实跑」一条已作废,请以本楼第六节为准。

页: [1]
查看完整版本: PowerBuilder 面向对象全解:用户对象、继承、覆盖与多态(PB12.5 实机验证)

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

Mail To:Admin@SybaseBbs.com