PowerBuilder 数组与结构全解:UpperBound / LowerBound、动态数组、多维数组与 Structure 实战
PowerBuilder 数组与结构全解:UpperBound / LowerBound、动态数组、多维数组与 Structure 实战阅读说明
1. 适用版本:PowerBuilder 10 及以上;本文全部代码在 PB 12.5 实机编译并运行通过(ORCA 全量编译 0 错误、PBVM 真机运行 PASS、30 条断言全中),并另附 PB10 兼容版 PBL 附件(PB10 下同样 0 错误编译通过)
2. 支持数据库:本文是纯 PowerScript 语言层内容,不涉及数据库
3. 操作系统与环境要求:Windows 7 及以上,装好 PB 运行时即可;不需要任何第三方组件或运行库
4. 难度系数:★★☆☆☆
5. 其它阅读说明:文中每段 // 后面的输出值都是 PB12.5 实机跑出来的真实结果,可直接对照验证;阅读前只要知道「PB 里变量声明与赋值要分两行写」就能跟做
一、先把三个概念钉死
写代码之前,先把三件事想明白,后面一半的坑都能自己推出来。
第一,PB 的数组下标默认从 1 开始,不是 0。 声明 long ll_a,合法下标是 1 到 5,写 ll_a 会直接抛运行时错误 Array boundary exceeded(实机输出见 5.2)。这一点跟 C / Java 的习惯完全相反,从其它语言转过来的人几乎必踩一次。
第二,数组分「定长」和「变长」两种。 方括号里写了数字就是定长(元素个数固定),什么都不写就是变长(刚声明时一个元素都没有,随用随长)。变长数组在没有任何元素之前,UpperBound() 返回 0 而不是 1——这是第二个高频坑,很多人以为"至少有一个"。
第三,结构(Structure)是一组不同类型字段的打包。 数组要求所有元素同类型;现实里一条员工记录同时有姓名、部门、年龄、工资,用数组就得拆成四个平行数组,同步维护时极易错位。结构就是为这件事准备的。PB 的结构是值类型而不是引用类型:两个结构变量之间用 = 赋值是逐字段拷贝,改一个不会影响另一个(5.7 有实机验证)。
二、数组声明的五种写法
写法示例元素个数合法下标说明
定长long ll_fix51 ~ 5最常用的写法
变长long ll_dyn[]0(起)1 ~ UpperBound()随赋值自动增长
自定义下界long ll_rng42 ~ 5冷门但合法,LowerBound() 也跟着变
字面量初始化ll_lit = {10, 20, 30}31 ~ 3花括号;声明与赋值可以分成两行写
多维(只能定长)long ll_m6 ~ 变长多维非法,[,] 连库都导不进去
三、数组与结构相关语法全貌
语法 / 函数作用返回值要点
LowerBound(数组 {, 维})取该维下界默认 1;自定义下界时返回你写的值
UpperBound(数组 {, 维})取该维上界变长数组无元素时返回 0;第二参数默认 1
数组1 = 数组2整体赋值值拷贝;左值为变长时会被改成右值的大小
结构1 = 结构2整体赋值逐字段值拷贝
if 结构1 = 结构2整体比较逐字段比较,返回 boolean(限同类型)
ref 数组/结构 参数引用传递函数里改了外面看得见
{v1, v2, ...}数组字面量只能整体赋值,不能给单个元素写花括号
要特别提醒的是:PB 没有内置的数组排序、查找、插入、删除函数。Sort() 是 DataWindow 的方法,跟数组没关系。排序要自己写循环(见 5.6),查找要自己遍历。这是刚上手 PB 的人最容易不适应的一点。
四、结构(Structure)怎么建
结构有两种作用域:
[*]全局结构:File → New → PB Object → Structure,存成独立对象,全工程可用。
[*]对象级结构:在窗口或用户对象的 Structure 视图里建,只在该对象内部可用。导出成 .sru 源码时长这样:
type str_emp from structure
string name
string dept
integer age
decimal salary
end type
命名建议统一 str_ 前缀,跟变量前缀规范(ls_ / ll_ / li_)配合,一眼能分清哪个是类型、哪个是变量。
五、八段可运行示例
统一的运行前提:新建一个任意窗口 w_demo,在上面放一个按钮 cb_1;把下面每段代码贴进 cb_1 的 clicked 事件,运行后点按钮,结果就在 MessageBox 里。想一次跑完,把八段按顺序拼进同一个事件即可。全部代码不连数据库、不依赖任何第三方组件。
5.1 五种声明方式与边界取值
// 演示:数组的五种声明方式与 LowerBound / UpperBound 的取值
long ll_fix // 定长:5 个元素,下标 1..5
long ll_dyn[] // 变长:还没有任何元素
long ll_rng // 自定义下界:只有 2、3、4、5 四个元素
long ll_lit[] // 变长:准备用字面量一次性赋值
string ls_week[] // 字符串变长数组
string ls_two // 二维定长数组
string ls_msg
ls_msg = ''
// 1) 定长数组:下界 1、上界 5
ls_msg = ls_msg + 'fixlb=' + String(LowerBound(ll_fix)) + ' ub=' + String(UpperBound(ll_fix)) + '~r~n'
// 2) 变长数组:还没赋值时下界仍是 1,上界是 0
ls_msg = ls_msg + 'dynlb=' + String(LowerBound(ll_dyn)) + ' ub=' + String(UpperBound(ll_dyn)) + '~r~n'
// 3) 自定义下界
ls_msg = ls_msg + 'rnglb=' + String(LowerBound(ll_rng)) + ' ub=' + String(UpperBound(ll_rng)) + '~r~n'
ll_rng = 22
ls_msg = ls_msg + 'rng=' + String(ll_rng) + '~r~n'
// 4) 字面量一次性赋值(声明与赋值分两行写)
ll_lit = {10, 20, 30}
ls_msg = ls_msg + 'litub=' + String(UpperBound(ll_lit)) + ' =' + String(ll_lit) + ' =' + String(ll_lit) + '~r~n'
// 5) 字符串数组
ls_week = {'Mon', 'Tue', 'Wed'}
ls_msg = ls_msg + 'week ub=' + String(UpperBound(ls_week)) + ' =' + ls_week + '~r~n'
// 6) 二维数组两个维度分别取上界
ls_msg = ls_msg + 'two ub1=' + String(UpperBound(ls_two,1)) + ' ub2=' + String(UpperBound(ls_two,2)) + '~r~n'
MessageBox('数组声明', ls_msg)
实机输出:
fixlb=1 ub=5
dynlb=1 ub=0
rnglb=2 ub=5
rng=22
litub=3 =10 =30
week ub=3 =Tue
two ub1=2 ub2=2
5.2 变长数组的扩容与越界(写和读不对称)
// 演示:变长数组写到越界位置会自动扩容,读越界却直接抛异常
long ll_dyn[]
long ll_fix
long ll_tmp
string ls_msg
ls_msg = ''
ll_dyn = 10
ll_dyn = 20
ls_msg = ls_msg + '两个元素 ub=' + String(UpperBound(ll_dyn)) + '~r~n'
// 跳号写:自动扩容到 5,中间的第 3、4 个元素补 0
ll_dyn = 50
ls_msg = ls_msg + '写到 ub=' + String(UpperBound(ll_dyn)) + ' =' + String(ll_dyn) + ' =' + String(ll_dyn) + '~r~n'
// 读越界:不会扩容,直接抛运行时错误(可被 TRY-CATCH 捕获)
try
ll_tmp = ll_dyn
ls_msg = ls_msg + '读=' + String(ll_tmp) + '~r~n'
catch (runtimeerror lerr1)
ls_msg = ls_msg + '读 异常:' + lerr1.getmessage() + '~r~n'
end try
// 定长数组写越界
try
ll_fix = 99
catch (runtimeerror lerr2)
ls_msg = ls_msg + '定长写 异常:' + lerr2.getmessage() + '~r~n'
end try
// 定长数组写下标 0
try
ll_fix = 7
catch (runtimeerror lerr3)
ls_msg = ls_msg + '定长写 异常:' + lerr3.getmessage() + '~r~n'
end try
MessageBox('扩容与越界', ls_msg)
实机输出:
两个元素 ub=2
写到 ub=5 =0 =50
读 异常:Array boundary exceeded at line 17 in function of_grow of object nvo_arr_demo.
定长写 异常:Array boundary exceeded at line 24 in function of_grow of object nvo_arr_demo.
定长写 异常:Array boundary exceeded at line 30 in function of_grow of object nvo_arr_demo.
5.3 二维数组:按维取上界 + 双层遍历
// 演示:二维数组 UpperBound 第二参数的用法与双层遍历填数
long ll_m
long ll_i
long ll_j
long ll_k
string ls_msg
ls_msg = ''
ll_k = 0
for ll_i = 1 to UpperBound(ll_m, 1)
for ll_j = 1 to UpperBound(ll_m, 2)
ll_k = ll_k + 1
ll_m = ll_k
next
next
ls_msg = 'ub1=' + String(UpperBound(ll_m,1)) + ' ub2=' + String(UpperBound(ll_m,2)) + '~r~n'
ls_msg = ls_msg + '=' + String(ll_m) + ' =' + String(ll_m)
MessageBox('二维数组', ls_msg)
实机输出:ub1=2 ub2=3 / =1 =6。
5.4 数组作参数:值传递、ref 传递与"带结果回来"
// 演示:数组作函数参数;注意 PB 函数【不能返回数组】,结果只能靠 ref 参数带回来
// 下面两段是配套的函数定义,写在同一个对象的函数列表里:
// function long of_sum (long al_arr[]) —— 值传递,只读
// long ll_i
// long ll_s
// ll_s = 0
// for ll_i = 1 to UpperBound(al_arr)
// ll_s = ll_s + al_arr
// next
// return ll_s
// subroutine of_append (ref long al_arr[], long al_val) —— ref 传递,追加一个元素
// al_arr = al_val
// subroutine of_build (ref long al_arr[], long al_from, long al_to)—— 用 ref 造一段连续数
// long ll_i
// for ll_i = al_from to al_to
// al_arr = ll_i
// next
long ll_src
long ll_dst[]
long ll_dyn[]
string ls_msg
ll_src = 5
ll_src = 8
ll_src = 13
ll_src = 21
ls_msg = '求和=' + String(of_sum(ll_src)) + '~r~n'
of_append(ll_dyn, 100)
of_append(ll_dyn, 200)
ls_msg = ls_msg + 'ref 追加后 ub=' + String(UpperBound(ll_dyn)) + ' =' + String(ll_dyn) + ' =' + String(ll_dyn) + '~r~n'
of_build(ll_dst, 3, 7)
ls_msg = ls_msg + 'ref 造数组 ub=' + String(UpperBound(ll_dst)) + ' =' + String(ll_dst) + ' =' + String(ll_dst)
MessageBox('数组作参数', ls_msg)
实机输出:求和=47 / ref 追加后 ub=2 =100 =200 / ref 造数组 ub=5 =3 =7。
5.5 整体赋值是值拷贝,清空靠赋空数组
// 演示:数组整体赋值是深拷贝;把变长数组清空要赋一个空数组
long ll_a
long ll_b[]
long ll_empty[]
string ls_msg
ls_msg = ''
ll_a = 1
ll_a = 2
ll_a = 3
ll_b = ll_a
ls_msg = ls_msg + '拷贝后 ub=' + String(UpperBound(ll_b)) + '~r~n'
ll_a = 111
ls_msg = ls_msg + '改源头后 b=' + String(ll_b) + '~r~n'
ll_b = ll_empty
ls_msg = ls_msg + '清空后 ub=' + String(UpperBound(ll_b))
MessageBox('拷贝与清空', ls_msg)
实机输出:拷贝后 ub=3 / 改源头后 b=1 / 清空后 ub=0。注意第二行:源头改成 111 之后 ll_b 仍是 1,说明 = 是值拷贝不是引用。
5.6 排序:PB 没有内置数组排序,冒泡自己写
// 演示:PB 没有内置的数组排序函数,这里是最朴素的冒泡升序
long ll_nums
long ll_i
long ll_j
long ll_t
string ls_msg
ll_nums = 5
ll_nums = 3
ll_nums = 9
ll_nums = 1
ll_nums = 7
ll_nums = 2
for ll_i = 1 to UpperBound(ll_nums) - 1
for ll_j = 1 to UpperBound(ll_nums) - ll_i
if ll_nums > ll_nums then
ll_t = ll_nums
ll_nums = ll_nums
ll_nums = ll_t
end if
next
next
ls_msg = '升序:'
for ll_i = 1 to UpperBound(ll_nums)
ls_msg = ls_msg + String(ll_nums)
next
MessageBox('冒泡排序', ls_msg)
实机输出:升序:123579。
5.7 结构:赋值、拷贝、返回值、ref 参数与整体比较
// 演示:结构的一次完整用法。str_emp 是对象级结构,字段为 name/dept/age/salary
str_emp lstr_a
str_emp lstr_b
str_emp lstr_c
str_emp lstr_d
string ls_msg
ls_msg = ''
lstr_a.name = 'ZhangSan'
lstr_a.dept = 'Dev'
lstr_a.age = 32
lstr_a.salary = 12500.50
// 整体赋值:逐字段值拷贝,不是引用
lstr_b = lstr_a
lstr_b.name = 'LiSi'
ls_msg = ls_msg + 'a=' + lstr_a.name + ' b=' + lstr_b.name + '~r~n'
ls_msg = ls_msg + 'b 其余字段 dept=' + lstr_b.dept + ' age=' + String(lstr_b.age) + ' salary=' + String(lstr_b.salary) + '~r~n'
// 结构作返回值(函数定义:function str_emp of_mkemp(string as_name, string as_dept, integer ai_age, decimal adec_sal))
lstr_c = of_mkemp('WangWu', 'QA', 28, 9000)
ls_msg = ls_msg + '返回值=' + lstr_c.name + '/' + lstr_c.dept + '/' + String(lstr_c.age) + '/' + String(lstr_c.salary) + '~r~n'
// ref 传参回写(函数定义:subroutine of_raise(ref str_emp astr_e, integer ai_pct))
of_raise(lstr_c, 10)
ls_msg = ls_msg + 'ref 加龄后 age=' + String(lstr_c.age) + '~r~n'
// 结构整体比较
lstr_d = lstr_c
if lstr_c = lstr_d then
ls_msg = ls_msg + '比较 内容相同=TRUE~r~n'
else
ls_msg = ls_msg + '比较 内容相同=FALSE~r~n'
end if
lstr_d.age = 99
if lstr_c = lstr_d then
ls_msg = ls_msg + '比较 age 不同=TRUE~r~n'
else
ls_msg = ls_msg + '比较 age 不同=FALSE~r~n'
end if
MessageBox('结构用法', ls_msg)
实机输出:
a=ZhangSan b=LiSi
b 其余字段 dept=Dev age=32 salary=12500.50
返回值=WangWu/QA/28/9000
ref 加龄后 age=38
比较 内容相同=TRUE
比较 age 不同=FALSE
5.8 结构数组:遍历汇总与跳号赋值
// 演示:结构数组的遍历汇总,以及跳号赋值时中间自动补空结构
str_emp lstr_arr[]
long ll_i
long ll_total
string ls_msg
ls_msg = ''
lstr_arr = of_mkemp('ZhangSan', 'Dev', 32, 12500.50)
lstr_arr = of_mkemp('LiSi', 'Dev', 29, 11000)
lstr_arr = of_mkemp('WangWu', 'QA', 28, 9000)
ll_total = 0
for ll_i = 1 to UpperBound(lstr_arr)
ll_total = ll_total + lstr_arr.age
next
ls_msg = ls_msg + '人数=' + String(UpperBound(lstr_arr)) + ' 年龄合计=' + String(ll_total) + '~r~n'
ls_msg = ls_msg + '第2人=' + lstr_arr.name + '/' + String(lstr_arr.salary) + '~r~n'
// 跳号赋值:中间自动补一个字段全空的结构
lstr_arr = of_mkemp('ZhaoLiu', 'Ops', 40, 8000)
ls_msg = ls_msg + '跳号后 ub=' + String(UpperBound(lstr_arr)) + ' 第4人=[' + lstr_arr.name + '] age=' + String(lstr_arr.age)
MessageBox('结构数组', ls_msg)
实机输出:人数=3 年龄合计=89 / 第2人=LiSi/11000 / 跳号后 ub=5 第4人=[] age=0。
六、十四个必记的坑
[*]下标默认从 1 开始:ll_a 直接抛 Array boundary exceeded,跟 C/Java 的习惯相反。
[*]变长数组没元素时 UpperBound 返回 0:不是 1,也不是 -1。判空请写 if UpperBound(a) = 0。
[*]写越界会自动扩容,读越界会抛异常:两者严重不对称。只有"赋值到越界位置"才扩容,只读一下第 100 个元素就会崩。
[*]跳号写会补默认值:数值补 0、字符串补空串、结构补一个字段全空的结构。遍历时要么判断,要么别跳号。
[*]多维数组必须定长:long ll_mv[,] 连 PBL 都导不进去(实机 import 直接失败,对象不在库里);ll_mv[] 再用 ll_mv 同样不行。
[*]函数不能返回数组:public function long[] of_x() 和 public function long of_x[]() 两种写法都报 C0031: Syntax error(实机),结果只能用 ref 参数带回来。这一点很反直觉,很多人第一次写都撞上。
[*]没有内置排序 / 查找:Sort() 属于 DataWindow,跟数组无关。排序自己写循环(5.6),查找自己遍历。
[*]数组整体赋值是值拷贝:ll_b = ll_a 之后改 ll_a,ll_b 不会变;反过来也一样。别指望它像 Java 那样共享引用。
[*]清空变长数组要赋空数组:ll_b = ll_empty(ll_empty 是同类型的空变长数组),UpperBound 立刻变 0。
[*]数组作参数默认按值传递:大数组反复传会有拷贝开销;需要函数里改外面、或想避免拷贝,就用 ref。
[*]结构整体赋值也是值拷贝:lstr_b = lstr_a 之后改 lstr_b.name,lstr_a.name 不受影响。
[*]结构可以整体比较,但限同类型:if lstr_a = lstr_b 会逐字段比(实机验证:内容相同 TRUE,只改 age 之后 FALSE)。不同类型的结构之间不能比。
[*]结构数组跳号赋值补的是"全空结构":lstr_arr = xxx 之后第 4 个元素存在但字段全是空/0,遍历汇总时要么跳过要么报错。
[*]自定义下界让 LowerBound 不再是 1:写了 之后遍历一定要用 LowerBound / UpperBound,千万别写死 for i = 1 to ...,否则第一圈就越界。
七、与相近方案的对比
场景推荐做法说明
一组同类型数据的临时收集与计算数组(定长或变长)零依赖、最快,本文 5.1~5.6
一条记录的多个异构字段结构(Structure)比四个平行数组安全得多
一组记录(多行多列),只做内存搬运结构数组见 5.8;注意跳号补空结构
一组记录,还要排序 / 过滤 / 展示DataStore / DataWindow自带缓冲、排序、过滤、更新,数组全得手写
需要按键取值或序列化成 JSONPBIDEA 的 uo_json / uo_map数组只能按下标取;uo_json 还支持 Get(key, ref string[]) 一次取出字符串数组
上万行数据DataStore 或分批读库数组全在内存里,量一大就慢
需要跨窗口 / 跨对象共享实例变量数组或全局结构注意对象生命周期,别引用已 destroy 的对象
八、可以往下延伸的三件事
[*]写一个通用排序 NVO:把 5.6 的冒泡换成快速排序,再做一个 of_sortby(ref str_emp 数组, 字段名),按结构的某个字段排序——PB 没有泛型,通常按字段各写一个版本,或者传字段名用 CHOOSE CASE 分发。
[*]结构数组与 DataStore 互转:结构数组适合内存里搬运,DataStore 适合与数据库交互,写一个 of_structs_to_ds / of_ds_to_structs 能把两套世界接起来。
[*]与 PBIDEA 的 uo_json 打通:uo_json 的 Get(key, ref string[]) 可以一次把 JSON 数组读成 PB 字符串数组,配合本文的数组操作就能很方便地处理接口返回。
实机验证情况:本文全部示例代码整理为 nvo_arr_demo 对象,在 PB12.5 下完成——①pypower pbl import 导入 0 错误;②build rebuild --type full 全量编译 0 错误;③PBVM 真机运行 .pbtest PASS,30 条断言全部命中(覆盖声明边界 / 扩容越界 / 二维数组 / 参数传递 / 拷贝清空 / 排序 / 结构 / 结构数组八组输出);④check_pb125.py 0 错误 0 提醒、check_enc.py 编码校验 bad=0。文中所有 // 后的输出值均取自实机运行结果。第六节第 5、6 两条"非法写法"(变长多维数组、数组作返回类型)同样是在实机 import 阶段验证出的真实编译错误,不是推断。 PB10 兼容版示例 PBL 已上传(附件 arrdemo_pb10.zip)
环境要求:PB 10 及以上(11 / 11.5 / 12.0 / 12.5 同样可用);Windows 7+;不需要任何第三方组件或 DLL;本示例不依赖数据库,无建表 SQL,纯语言层计算,开箱即跑。
操作步骤:
1) 解压 arrdemo_pb10.zip,得到 arrdemo.pbl 与源码 nvo_arr_demo.sru;
2) 工程 Library List 追加 arrdemo.pbl,Full Rebuild 一次(应 0 错误);
(想用源码方式:Library 画板 Entry > Import 选 nvo_arr_demo.sru,源码是 GBK+CRLF+无 BOM)
3) 在按钮 clicked 里写:
nvo_arr_demo lnv_demo
string ls_out
lnv_demo = create nvo_arr_demo
ls_out = lnv_demo.of_run()
destroy lnv_demo
MessageBox('数组与结构全解', ls_out)
4) 也可以只调单个函数逐个对照:of_declare / of_grow / of_multidim / of_pass / of_copyclear / of_sort / of_struct / of_structarr,分别对应文中 5.1 ~ 5.8 的输出。对象里的对象级结构 str_emp 字段为 name / dept / age / salary。
本次实机踩出来的几个硬结论(都是编译期/运行期真跑出来的,不是推断):
[*]写越界自动扩容,读越界抛异常:变长数组 a=50 会自动把数组撑到 5(中间补 0)且赋值语句不报错;但只读一下 a 立刻抛 Array boundary exceeded。这两者不对称,是本主题最容易翻车的地方。异常可以用 TRY...CATCH(runtimeerror) 捕获;
[*]函数不能返回数组:public function long[] of_x() 和 public function long of_x[]() 两种写法都报 C0031: Syntax error,PB 的数组根本不能作函数返回类型。要"造一段数组带回来"只能走 ref 参数,示例里用 of_build(ref long al_arr[], al_from, al_to) 实现;
[*]变长多维数组非法:long a[,] 在 import 阶段就失败、对象根本进不了 PBL;long a[] 再写 a 同样不行。多维必须写成定长 a;
[*]下标默认从 1 开始:a 直接抛 Array boundary exceeded,跟 C/Java 的习惯完全相反;
[*]变长数组没元素时 UpperBound 返回 0,不是 1 也不是 -1,判空写 if UpperBound(a) = 0;
[*]自定义下界 long a 是合法的(LowerBound=2、UpperBound=5,共 4 个元素)。用了它之后遍历一定要写 LowerBound / UpperBound,写死 1 会第一圈就越界;
[*]数组字面量可以拆两行写:先 long a[] 再 a = {10,20,30},不需要挤在声明行上;
[*]数组与结构的整体赋值都是值拷贝:b = a 之后改 a,b 不变;清空变长数组靠赋一个同类型的空数组;
[*]结构可以用 = 整体比较(同类型、逐字段):内容相同返回 TRUE,只改 age 之后返回 FALSE;
[*]PB 没有内置数组排序/查找,Sort() 是 DataWindow 的方法,跟数组无关,5.6 给了可直接用的冒泡实现。
实机验证情况:PB12.5 下 import 0 错 + Full Rebuild 0 错 + PBVM 真机运行 PASS(30 条断言全中,覆盖声明边界/扩容越界/二维数组/参数传递/拷贝清空/排序/结构/结构数组八组输出)+ check_pb125 0 错 0 提醒 + check_enc 编码全 OK。本 PB10 版 PBL 用 pypower --pb-version 100 全量编译 0 错误,并已用 pbl list 核对对象齐全(防循环依赖静默丢失)。
如实说明:PB10 版只在编译层验证(0 错误 + 对象齐全),未在 PB10 PBVM 上跑断言测试,实际输出值未逐条比对;如发现个别函数输出与文中不同,以你本机运行结果为准。
页:
[1]