马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
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[5],合法下标是 1 到 5,写 ll_a[0] 会直接抛运行时错误 Array boundary exceeded(实机输出见 5.2)。这一点跟 C / Java 的习惯完全相反,从其它语言转过来的人几乎必踩一次。
第二,数组分「定长」和「变长」两种。 方括号里写了数字就是定长(元素个数固定),什么都不写就是变长(刚声明时一个元素都没有,随用随长)。变长数组在没有任何元素之前,UpperBound() 返回 0 而不是 1——这是第二个高频坑,很多人以为"至少有一个"。
第三,结构(Structure)是一组不同类型字段的打包。 数组要求所有元素同类型;现实里一条员工记录同时有姓名、部门、年龄、工资,用数组就得拆成四个平行数组,同步维护时极易错位。结构就是为这件事准备的。PB 的结构是值类型而不是引用类型:两个结构变量之间用 = 赋值是逐字段拷贝,改一个不会影响另一个(5.7 有实机验证)。
二、数组声明的五种写法
| 写法 | 示例 | 元素个数 | 合法下标 | 说明 | | 定长 | long ll_fix[5] | 5 | 1 ~ 5 | 最常用的写法 | | 变长 | long ll_dyn[] | 0(起) | 1 ~ UpperBound() | 随赋值自动增长 | | 自定义下界 | long ll_rng[2 to 5] | 4 | 2 ~ 5 | 冷门但合法,LowerBound() 也跟着变 | | 字面量初始化 | ll_lit = {10, 20, 30} | 3 | 1 ~ 3 | 花括号;声明与赋值可以分成两行写 | | 多维(只能定长) | long ll_m[2,3] | 6 | [1,1] ~ [2,3] | 变长多维非法,[,] 连库都导不进去 |
三、数组与结构相关语法全貌
| 语法 / 函数 | 作用 | 返回值要点 | | 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] // 定长:5 个元素,下标 1..5
- long ll_dyn[] // 变长:还没有任何元素
- long ll_rng[2 to 5] // 自定义下界:只有 2、3、4、5 四个元素
- long ll_lit[] // 变长:准备用字面量一次性赋值
- string ls_week[] // 字符串变长数组
- string ls_two[2,2] // 二维定长数组
- string ls_msg
- ls_msg = ''
- // 1) 定长数组:下界 1、上界 5
- ls_msg = ls_msg + 'fix lb=' + String(LowerBound(ll_fix)) + ' ub=' + String(UpperBound(ll_fix)) + '~r~n'
- // 2) 变长数组:还没赋值时下界仍是 1,上界是 0
- ls_msg = ls_msg + 'dyn lb=' + String(LowerBound(ll_dyn)) + ' ub=' + String(UpperBound(ll_dyn)) + '~r~n'
- // 3) 自定义下界
- ls_msg = ls_msg + 'rng lb=' + String(LowerBound(ll_rng)) + ' ub=' + String(UpperBound(ll_rng)) + '~r~n'
- ll_rng[2] = 22
- ls_msg = ls_msg + 'rng[2]=' + String(ll_rng[2]) + '~r~n'
- // 4) 字面量一次性赋值(声明与赋值分两行写)
- ll_lit = {10, 20, 30}
- ls_msg = ls_msg + 'lit ub=' + String(UpperBound(ll_lit)) + ' [1]=' + String(ll_lit[1]) + ' [3]=' + String(ll_lit[3]) + '~r~n'
- // 5) 字符串数组
- ls_week = {'Mon', 'Tue', 'Wed'}
- ls_msg = ls_msg + 'week ub=' + String(UpperBound(ls_week)) + ' [2]=' + ls_week[2] + '~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)
复制代码
实机输出:
- fix lb=1 ub=5
- dyn lb=1 ub=0
- rng lb=2 ub=5
- rng[2]=22
- lit ub=3 [1]=10 [3]=30
- week ub=3 [2]=Tue
- two ub1=2 ub2=2
复制代码
5.2 变长数组的扩容与越界(写和读不对称)
- // 演示:变长数组写到越界位置会自动扩容,读越界却直接抛异常
- long ll_dyn[]
- long ll_fix[3]
- long ll_tmp
- string ls_msg
- ls_msg = ''
- ll_dyn[1] = 10
- ll_dyn[2] = 20
- ls_msg = ls_msg + '两个元素 ub=' + String(UpperBound(ll_dyn)) + '~r~n'
- // 跳号写:自动扩容到 5,中间的第 3、4 个元素补 0
- ll_dyn[5] = 50
- ls_msg = ls_msg + '写到[5] ub=' + String(UpperBound(ll_dyn)) + ' [3]=' + String(ll_dyn[3]) + ' [5]=' + String(ll_dyn[5]) + '~r~n'
- // 读越界:不会扩容,直接抛运行时错误(可被 TRY-CATCH 捕获)
- try
- ll_tmp = ll_dyn[100]
- ls_msg = ls_msg + '读[100]=' + String(ll_tmp) + '~r~n'
- catch (runtimeerror lerr1)
- ls_msg = ls_msg + '读[100] 异常:' + lerr1.getmessage() + '~r~n'
- end try
- // 定长数组写越界
- try
- ll_fix[4] = 99
- catch (runtimeerror lerr2)
- ls_msg = ls_msg + '定长写[4] 异常:' + lerr2.getmessage() + '~r~n'
- end try
- // 定长数组写下标 0
- try
- ll_fix[0] = 7
- catch (runtimeerror lerr3)
- ls_msg = ls_msg + '定长写[0] 异常:' + lerr3.getmessage() + '~r~n'
- end try
- MessageBox('扩容与越界', ls_msg)
复制代码
实机输出:
- 两个元素 ub=2
- 写到[5] ub=5 [3]=0 [5]=50
- 读[100] 异常:Array boundary exceeded at line 17 in function of_grow of object nvo_arr_demo.
- 定长写[4] 异常:Array boundary exceeded at line 24 in function of_grow of object nvo_arr_demo.
- 定长写[0] 异常:Array boundary exceeded at line 30 in function of_grow of object nvo_arr_demo.
复制代码
5.3 二维数组:按维取上界 + 双层遍历
- // 演示:二维数组 UpperBound 第二参数的用法与双层遍历填数
- long ll_m[2,3]
- 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_i, ll_j] = ll_k
- next
- next
- ls_msg = 'ub1=' + String(UpperBound(ll_m,1)) + ' ub2=' + String(UpperBound(ll_m,2)) + '~r~n'
- ls_msg = ls_msg + '[1,1]=' + String(ll_m[1,1]) + ' [2,3]=' + String(ll_m[2,3])
- MessageBox('二维数组', ls_msg)
复制代码
实机输出:ub1=2 ub2=3 / [1,1]=1 [2,3]=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[ll_i]
- // next
- // return ll_s
- // subroutine of_append (ref long al_arr[], long al_val) —— ref 传递,追加一个元素
- // al_arr[UpperBound(al_arr) + 1] = 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[UpperBound(al_arr) + 1] = ll_i
- // next
- long ll_src[4]
- long ll_dst[]
- long ll_dyn[]
- string ls_msg
- ll_src[1] = 5
- ll_src[2] = 8
- ll_src[3] = 13
- ll_src[4] = 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)) + ' [1]=' + String(ll_dyn[1]) + ' [2]=' + String(ll_dyn[2]) + '~r~n'
- of_build(ll_dst, 3, 7)
- ls_msg = ls_msg + 'ref 造数组 ub=' + String(UpperBound(ll_dst)) + ' [1]=' + String(ll_dst[1]) + ' [5]=' + String(ll_dst[5])
- MessageBox('数组作参数', ls_msg)
复制代码
实机输出:求和=47 / ref 追加后 ub=2 [1]=100 [2]=200 / ref 造数组 ub=5 [1]=3 [5]=7。
5.5 整体赋值是值拷贝,清空靠赋空数组
- // 演示:数组整体赋值是深拷贝;把变长数组清空要赋一个空数组
- long ll_a[3]
- long ll_b[]
- long ll_empty[]
- string ls_msg
- ls_msg = ''
- ll_a[1] = 1
- ll_a[2] = 2
- ll_a[3] = 3
- ll_b = ll_a
- ls_msg = ls_msg + '拷贝后 ub=' + String(UpperBound(ll_b)) + '~r~n'
- ll_a[1] = 111
- ls_msg = ls_msg + '改源头后 b[1]=' + String(ll_b[1]) + '~r~n'
- ll_b = ll_empty
- ls_msg = ls_msg + '清空后 ub=' + String(UpperBound(ll_b))
- MessageBox('拷贝与清空', ls_msg)
复制代码
实机输出:拷贝后 ub=3 / 改源头后 b[1]=1 / 清空后 ub=0。注意第二行:源头改成 111 之后 ll_b[1] 仍是 1,说明 = 是值拷贝不是引用。
5.6 排序:PB 没有内置数组排序,冒泡自己写
- // 演示:PB 没有内置的数组排序函数,这里是最朴素的冒泡升序
- long ll_nums[6]
- long ll_i
- long ll_j
- long ll_t
- string ls_msg
- ll_nums[1] = 5
- ll_nums[2] = 3
- ll_nums[3] = 9
- ll_nums[4] = 1
- ll_nums[5] = 7
- ll_nums[6] = 2
- for ll_i = 1 to UpperBound(ll_nums) - 1
- for ll_j = 1 to UpperBound(ll_nums) - ll_i
- if ll_nums[ll_j] > ll_nums[ll_j + 1] then
- ll_t = ll_nums[ll_j]
- ll_nums[ll_j] = ll_nums[ll_j + 1]
- ll_nums[ll_j + 1] = ll_t
- end if
- next
- next
- ls_msg = '升序:'
- for ll_i = 1 to UpperBound(ll_nums)
- ls_msg = ls_msg + String(ll_nums[ll_i])
- 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[1] = of_mkemp('ZhangSan', 'Dev', 32, 12500.50)
- lstr_arr[2] = of_mkemp('LiSi', 'Dev', 29, 11000)
- lstr_arr[3] = of_mkemp('WangWu', 'QA', 28, 9000)
- ll_total = 0
- for ll_i = 1 to UpperBound(lstr_arr)
- ll_total = ll_total + lstr_arr[ll_i].age
- next
- ls_msg = ls_msg + '人数=' + String(UpperBound(lstr_arr)) + ' 年龄合计=' + String(ll_total) + '~r~n'
- ls_msg = ls_msg + '第2人=' + lstr_arr[2].name + '/' + String(lstr_arr[2].salary) + '~r~n'
- // 跳号赋值:中间自动补一个字段全空的结构
- lstr_arr[5] = of_mkemp('ZhaoLiu', 'Ops', 40, 8000)
- ls_msg = ls_msg + '跳号后 ub=' + String(UpperBound(lstr_arr)) + ' 第4人=[' + lstr_arr[4].name + '] age=' + String(lstr_arr[4].age)
- MessageBox('结构数组', ls_msg)
复制代码
实机输出:人数=3 年龄合计=89 / 第2人=LiSi/11000 / 跳号后 ub=5 第4人=[] age=0。
六、十四个必记的坑
- 下标默认从 1 开始:ll_a[0] 直接抛 Array boundary exceeded,跟 C/Java 的习惯相反。
- 变长数组没元素时 UpperBound 返回 0:不是 1,也不是 -1。判空请写 if UpperBound(a) = 0。
- 写越界会自动扩容,读越界会抛异常:两者严重不对称。只有"赋值到越界位置"才扩容,只读一下第 100 个元素就会崩。
- 跳号写会补默认值:数值补 0、字符串补空串、结构补一个字段全空的结构。遍历时要么判断,要么别跳号。
- 多维数组必须定长:long ll_mv[,] 连 PBL 都导不进去(实机 import 直接失败,对象不在库里);ll_mv[] 再用 ll_mv[1,1] 同样不行。
- 函数不能返回数组: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[5] = xxx 之后第 4 个元素存在但字段全是空/0,遍历汇总时要么跳过要么报错。
- 自定义下界让 LowerBound 不再是 1:写了 [2 to 5] 之后遍历一定要用 LowerBound / UpperBound,千万别写死 for i = 1 to ...,否则第一圈就越界。
七、与相近方案的对比
| 场景 | 推荐做法 | 说明 | | 一组同类型数据的临时收集与计算 | 数组(定长或变长) | 零依赖、最快,本文 5.1~5.6 | | 一条记录的多个异构字段 | 结构(Structure) | 比四个平行数组安全得多 | | 一组记录(多行多列),只做内存搬运 | 结构数组 | 见 5.8;注意跳号补空结构 | | 一组记录,还要排序 / 过滤 / 展示 | DataStore / DataWindow | 自带缓冲、排序、过滤、更新,数组全得手写 | | 需要按键取值或序列化成 JSON | PBIDEA 的 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 阶段验证出的真实编译错误,不是推断。 |