马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
PowerBuilder 数组全解:定长/变长/多维数组、边界行为与常用数组函数(PB10 基准 · PB12.5 实测)
阅读说明
1. 适用版本:基准版本 PB10;兼容/实测:PB 12.5。数组是 PowerScript 语言层的原生类型,自 PB 早期版本就已定型,PB 6.5 / 8 / 9 / 10 / 10.5 / 11.x / 12.x 语法完全一致;本文全部结论已在 PB 10 与 PB 12.5 上各自复跑,两版输出逐字节相同(见第十一、十二节)
2. 支持数据库:不涉及数据库,纯语言层,不需要任何 DBMS
3. 操作系统与环境要求:Windows 任何带 PB 开发环境或 PB 运行时的机器即可,不需要任何第三方 DLL。源码须 GBK 编码 + CRLF 换行 + 无 BOM
4. 难度系数:★★☆☆☆(声明和访问都好记,但「数组按值传递」「越界抛错不静默」「没有内置排序」这三处会反复咬人;零基础可跟做前六节)
5. 其它阅读说明:本文结论全部来自双版本实机探针(PB 10 与 PB 12.5 各自复跑,9 条语言层硬断言全部命中),文中的 UpperBound、越界捕获、赋值副本、传参结果都是真机输出原值,不是推断值。文末附可直接导入运行的 PB10 兼容版 PBL 源码包
一、先说三个最容易被坑的点
数组在 PB 里用得极多,但三个反直觉的事实几乎每个新手都会栽一次。先把结论摆出来,后面逐条给证据:
- 数组没有内置的排序、填充、翻转函数。Sort / Populate / Reverse 对数组一律报 C0051 Unknown function(Reverse 只能用在字符串上)。要排序只能自己写循环。
- 越界访问(读或写)会抛运行时错误,不会返回默认值,也不会静默忽略。必须用 try ... catch (RuntimeError re) 接住。
- 数组作为函数参数,默认是「按值传递(复制一份)」。函数内部改了数组,调用方的原数组不变。想改成按引用,必须把形参显式声明为 ref(写法见第九节)。这一点和很多语言相反,是最容易写错的地方。
下面这段是三个结论的实机证据,PB 10 与 PB 12.5 输出完全一致:
@@CODEBLOCK0@@
二、声明:三种形态
PB 的数组有三种声明方式,区别全在「容量什么时候定」。
| 形态 | 写法示例 | 初始 UpperBound | 容量是否固定 | | 定长数组 | integer li_a[5] | 声明长度(这里是 5) | 固定,不能长大 | | 变长数组 | string ls_a[] | 0(空) | 动态,用 [UpperBound+1] 追加 | | 多维数组 | long ll_a[3,4] | 各维声明长度 | 固定 |
最容易踩的坑:定长数组的 UpperBound 等于你声明的个数,不是 0;变长数组的初始 UpperBound 是 0,不是 1。下面逐条实测。
三、UpperBound 与 LowerBound:数组的尺子
两个函数必须记住:
- UpperBound(arr):返回数组当前最大下标(变长数组返回元素个数)。
- UpperBound(arr, 维号):多维数组取某一维的长度(维号从 1 开始)。
- LowerBound(arr) / LowerBound(arr, 维号):返回最小下标,恒为 1(PB 数组是 1 基,不存在 0 基)。
- // 示例输入:一个定长整型数组,声明长度为 5
- integer li_fixed[5]
- // 示例输入:一个变长字符串数组(初始为空)
- string ls_var[]
- // 示例输入:一个 3 行 4 列的二维长整型数组
- long ll_2d[3, 4]
- // 定长:UpperBound 等于声明长度,LowerBound 恒为 1
- MessageBox('定长', 'UB=' + string(UpperBound(li_fixed)) + ' LB=' + string(LowerBound(li_fixed)))
- // 变长:初始 UpperBound 是 0
- MessageBox('变长初始', 'UB=' + string(UpperBound(ls_var)))
- // 多维:第一维 3,第二维 4
- MessageBox('多维', 'D1=' + string(UpperBound(ll_2d, 1)) + ' D2=' + string(UpperBound(ll_2d, 2)))
复制代码实机输出(PB 10 与 PB 12.5 一致):定长 UB=5 LB=1;变长初始 UB=0;多维 D1=3 D2=4。
四、定长数组:声明即固定容量
定长数组在声明时就分配好容量,UpperBound 立刻等于声明长度。赋值用下标从 1 开始。
- // 示例输入:定长整型数组,声明长度为 5
- integer li_fixed[5]
- integer li_i
- // 用下标逐个赋值(PB 数组是 1 基)
- for li_i = 1 to 5
- li_fixed[li_i] = li_i * 10 // 10, 20, 30, 40, 50
- next
- // 直接读任意元素
- MessageBox('第 3 个', string(li_fixed[3])) // 30
复制代码
定长数组的硬约束:容量不能长大。你不能通过 [6] 这种写法给它加元素——越界写入会抛错(见第八节)。如果业务上数量不定,请用变长数组。
五、变长数组:用 [UpperBound+1] 追加
变长数组初始为空(UpperBound=0),要往里加元素,标准写法是用「当前最大下标 + 1」作为新下标。
- // 示例输入:变长字符串数组(初始为空)
- string ls_var[]
- // 前三行直接按 1/2/3 写
- ls_var[1] = '甲'
- ls_var[2] = '乙'
- ls_var[3] = '丙'
- // 追加第 4 个:必须 UpperBound+1,不能直接写死下标 4(数量未知时写死会漏)
- ls_var[UpperBound(ls_var) + 1] = '丁'
- long ll_i
- string ls_join
- ls_join = ''
- for ll_i = 1 to UpperBound(ls_var)
- ls_join = ls_join + ls_var[ll_i] // 甲 乙 丙 丁
- next
- MessageBox('变长结果', '个数=' + string(UpperBound(ls_var)) + ' 内容=' + ls_join)
复制代码实机输出(PB 10 与 PB 12.5 一致):个数=4 内容=甲乙丙丁。注意第三行的 UpperBound=3 后追加得到 ADD_UB=4。
一个细节:这里用 UpperBound(ls_var)+1 而不是写死 ls_var[4],是因为真实业务里数量通常来自循环或查询,写死下标会在数量变化时漏掉元素或越界。
六、多维数组:UpperBound(数组, 维号)
多维数组的下标从 1 开始,取某一维长度必须用 UpperBound(arr, 维号)。
- // 示例输入:3 行 4 列的二维长整型数组
- long ll_2d[3, 4]
- // 给第一行第一列、第三行第四列赋值
- ll_2d[1, 1] = 11
- ll_2d[3, 4] = 34
- MessageBox('多维取值', 'V(1,1)=' + string(ll_2d[1, 1]) + ' V(3,4)=' + string(ll_2d[3, 4]))
复制代码实机输出(PB 10 与 PB 12.5 一致):V(1,1)=11 V(3,4)=34。
七、整体赋值:a = b 是「值拷贝」,不是引用
把一个数组赋给另一个数组,PB 会生成一份独立副本。之后修改其中一个,另一个不受影响。这一点和「对象赋值」不同(对象赋值是共享引用),初学者极易混。
- // 示例输入:源数组,3 个元素
- integer li_src[3]
- li_src[1] = 1
- li_src[2] = 2
- li_src[3] = 3
- // 整体赋值:生成独立副本
- integer li_dst[]
- li_dst = li_src
- // 改副本的第一个元素
- li_dst[1] = 999
- // 源数组的第一个元素仍是 1,说明两份互不影响
- MessageBox('副本独立', '源[1]=' + string(li_src[1]) + ' 副本[1]=' + string(li_dst[1]))
复制代码实机输出(PB 10 与 PB 12.5 一致):源[1]=1 副本[1]=999。这正是「值拷贝」的铁证。
八、排序:没有内置函数,必须手写
PB 的数组没有 Sort / SortA 之类的方法。任何想直接 ls_a.Sort() 或 Sort(ls_a) 的写法都会编译报 C0051 Unknown function。下面是一段实测可用的冒泡排序(升序),可整段复用。
- // 示例输入:一个乱序的定长整型数组
- integer li_a[5]
- li_a[1] = 30
- li_a[2] = 10
- li_a[3] = 50
- li_a[4] = 20
- li_a[5] = 40
- integer li_i
- integer li_j
- integer li_tmp
- // 手写冒泡排序(PB 没有内置数组排序,必须自己写)
- for li_i = 1 to 4
- for li_j = li_i + 1 to 5
- if li_a[li_j] < li_a[li_i] then
- li_tmp = li_a[li_i]
- li_a[li_i] = li_a[li_j]
- li_a[li_j] = li_tmp
- end if
- next
- next
- string ls_r
- ls_r = ''
- for li_i = 1 to 5
- ls_r = ls_r + string(li_a[li_i]) + ','
- next
- MessageBox('排序结果', ls_r) // 10,20,30,40,50,
复制代码实机输出(PB 10 与 PB 12.5 一致):10,20,30,40,50,。
提示:如果是字符串数组排序,把比较里的 < 换成 ls_a[li_j] < ls_a[li_i] 同样适用;需要降序就把 < 改成 >。
九、越界访问:抛运行时错误,必须 try-catch
这是另一个反直觉点:越界读或越界写都会抛运行时错误,不是返回空、不是返回 0、也不是静默忽略。在 headless 或循环场景里,不接住就会让程序崩掉。
- // 示例输入:一个定长数组,长度为 3
- integer li_fx[3]
- // 越界写入第 10 个:会抛运行时错误
- boolean lb_caught
- lb_caught = false
- try
- li_fx[10] = 99
- catch (RuntimeError re)
- lb_caught = true // 走到这里说明被捕获
- end try
- MessageBox('越界写入', '被捕获=' + string(lb_caught)) // true
复制代码
越界读取同样会抛错,写法完全一样:
- // 示例输入:定长数组,长度为 3(没有第 10 个元素)
- integer li_fx[3]
- integer li_got
- boolean lb_caught
- lb_caught = false
- try
- li_got = li_fx[10] // 越界读取同样抛运行时错误
- catch (RuntimeError re)
- lb_caught = true
- end try
- MessageBox('越界读取', '被捕获=' + string(lb_caught)) // true
复制代码实机输出(PB 10 与 PB 12.5 一致):越界写入 被捕获=true;越界读取 被捕获=true。
⚠️ 异常类型要写对:接数组越界用的是 RuntimeError,不是 RuntimeException(PB 里这是两个不同的类,RuntimeException 接不住越界)。
十、数组作为函数参数:默认按值传递(最容易被坑)
这是全文最重要的一条。PB 数组作为参数,默认是「按值传递」——函数拿到的是一份副本,函数内部怎么改,调用方的原数组都不受影响。 这一点和「对象按引用传递」正好相反,也和很多语言把数组默认当引用不同。
下面这个实测证明:函数里把 a[1] 改成 999,调用方的数组 li_x[1] 仍然是 1。
- // 演示对象 nvo_arr_demo 的方法 of_touch(按值):
- // integer of_touch (integer a[])
- // a[1] = 999
- // return UpperBound(a)
- //
- // 调用方代码:
- // 示例输入:一个 2 元素数组
- integer li_x[2]
- li_x[1] = 1
- li_x[2] = 2
- // 调用按值传递的方法
- integer li_ret
- li_ret = inv.of_touch(li_x)
- // 调用方数组第一个元素没变,仍是 1
- MessageBox('按值传递', '调用方[1]=' + string(li_x[1]) + ' 返回长度=' + string(li_ret))
复制代码实机输出(PB 10 与 PB 12.5 一致):调用方[1]=1 返回长度=2。li_x[1] 仍然是 1,证明函数改的是副本。
怎么改成按引用? 把形参声明成 ref:
- // 按引用的写法(标准 PB 语法):
- // integer of_touch_ref (ref integer a[])
- // a[1] = 999
- // return UpperBound(a)
- //
- // 调用方:
- integer li_y[2]
- li_y[1] = 1
- li_y[2] = 2
- integer li_retref
- li_retref = inv.of_touch_ref(li_y)
- MessageBox('按引用传递', '调用方[1]=' + string(li_y[1])) // 会变成 999
复制代码⚠️ 写法说明:上面「按引用」的 ref 写法是标准 PowerBuilder 语法,本文为避免误导,核心实测只固化了最反直觉、也最该记住的「默认按值」结论(实机断言已命中 EXT1=1)。ref 形参的改动可见性属于标准文档行为,按规范如实标注:本环境的命令行测试器对带 ref 形参的方法存在解析缺陷(C0051/C0052),无法纳入实机断言,故按标准 PB 文档说明,未实机跑通。读者在自己机器上用 ref 写法即可得到「调用方[1]=999」的结果。
十一、常用数组写法清单(速查)
| 需求 | 写法 | 备注 | | 声明定长 | integer a[10] | UB 立即等于 10 | | 声明变长 | string a[] | UB 初始 0 | | 声明多维 | long a[3,4] | 取维长 UpperBound(a,1) | | 取元素个数 | UpperBound(a) | 变长为实际个数 | | 取最小下标 | LowerBound(a) | 恒为 1 | | 追加元素 | a[UpperBound(a)+1] = x | 仅变长数组 | | 整体复制 | b = a | 值拷贝,互相独立 | | 排序 | 手写循环 | 无内置函数 | | 翻转字符串 | Reverse(s) | 仅字符串,数组不支持 | | 越界保护 | try ... catch (RuntimeError re) | 越界读/写都抛 RuntimeError | | 按引用传数组 | ref 类型 数组名[] | 默认是值传递 |
十二、实机验证情况(PB10 基准 + PB12.5 实测双轨)
本文所有行为结论均经过两个版本各自复跑,不是凭记忆写:
- PB10(基准):源码 GBK+CRLF+无 BOM,pbl import 0 错;build rebuild --type full 0 错;PBVM 运行 .pbtest 断言 9/9 全中;源码静态检查 0 错误 0 提醒;编码检查 bad=0。
- PB 12.5:同样的源码、同样的测试,编译 0 错 + 运行 9/9 全中。
- 双轨对照:两版诊断输出串逐条比对,逐字节相同——FIXED_UB=5;FIXED_LB=1 | EMPTY_UB=0;APPEND_UB=3;ADD_UB=4;JOIN=甲乙丙丁 | D1=3;D2=4;V11=11;V34=34 | SRC1=1;DST1=999;DST_UB=3 | SORT=10,20,30,40,50, | OOBWRITE_CAUGHT=true | OOBREAD_CAUGHT=true | EXT1=1;RET=2。
唯一一处未做实机跑通的,是第十节的「按引用传数组」(ref 形参)——原因是本环境命令行测试器对带 ref 形参的方法解析有缺陷(C0051/C0052),已如实标注,按标准 PB 文档说明,未把它当作已实测结论。
十三、PB12.5 差异
PB 12.5 与 PB 10 行为一致,未发现差异。 上述所有数组行为(定长/变长/多维、UpperBound/LowerBound、值拷贝、越界抛 RuntimeError、默认按值传递)在 PB 10 与 PB 12.5 上输出完全相同,无需为 12.5 做任何改写。
十四、避坑清单
- 不要找内置排序函数:Sort/Populate 对数组编译报 C0051;Reverse 是字符串函数,数组用不了。排序必须手写循环。
- 越界会抛错,不是静默:读或写越界下标都抛 RuntimeError,循环前先用 UpperBound 判边界,或用 try-catch 兜底。
- 数组默认按值传递:函数里改数组参数不影响调用方,要改必须 ref。别想当然当成引用。
- 整体赋值是值拷贝:b = a 后两份独立,改 b 不影响 a;这和对象赋值的「共享引用」不同。
- 1 基不是 0 基:LowerBound 恒为 1,循环一律从 1 开始;写 for i = 0 to ... 会越界。
- 变长追加用 UpperBound+1:写死下标在数量变化时容易漏元素或越界。
- 异常类型用 RuntimeError:接越界别用 RuntimeException,后者接不住。
- 定长容量固定:定长数组不能长大,数量不定请用变长。
十五、附:可直接导入运行的 PB10 兼容版源码包
本文所有示例已打包成一个独立 PBL(nvo_arr_demo,autoinstantiate 不可视对象,无需 create 即可直接使用),含定长/变长/多维/赋值/排序/越界/传参 8 个演示方法,以及一份断言覆盖 9 项行为的 .pbtest 测试。包内源码、测试、编码、实机验证报告一应俱全,PB 10 与 PB 12.5 均可导入运行。详见本帖下方回帖附件。 |