pbai 发表于 2026-9-28 06:54:18

PowerBuilder 数组全解:定长/变长/多维数组、边界行为与常用数组函数(PB10 基准 · PB12.5 实测)

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)固定,不能长大
变长数组string ls_a[]0(空)动态,用 追加
多维数组long ll_a各维声明长度固定


最容易踩的坑:定长数组的 UpperBound 等于你声明的个数,不是 0;变长数组的初始 UpperBound 是 0,不是 1。下面逐条实测。



三、UpperBound 与 LowerBound:数组的尺子

两个函数必须记住:


[*]UpperBound(arr):返回数组当前最大下标(变长数组返回元素个数)。
[*]UpperBound(arr, 维号):多维数组取某一维的长度(维号从 1 开始)。
[*]LowerBound(arr) / LowerBound(arr, 维号):返回最小下标,恒为 1(PB 数组是 1 基,不存在 0 基)。



// 示例输入:一个定长整型数组,声明长度为 5
integer li_fixed
// 示例输入:一个变长字符串数组(初始为空)
string ls_var[]
// 示例输入:一个 3 行 4 列的二维长整型数组
long ll_2d

// 定长: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
integer li_i
// 用下标逐个赋值(PB 数组是 1 基)
for li_i = 1 to 5
    li_fixed = li_i * 10   // 10, 20, 30, 40, 50
next
// 直接读任意元素
MessageBox('第 3 个', string(li_fixed))   // 30


定长数组的硬约束:容量不能长大。你不能通过 这种写法给它加元素——越界写入会抛错(见第八节)。如果业务上数量不定,请用变长数组。



五、变长数组:用 追加

变长数组初始为空(UpperBound=0),要往里加元素,标准写法是用「当前最大下标 + 1」作为新下标。


// 示例输入:变长字符串数组(初始为空)
string ls_var[]
// 前三行直接按 1/2/3 写
ls_var = '甲'
ls_var = '乙'
ls_var = '丙'
// 追加第 4 个:必须 UpperBound+1,不能直接写死下标 4(数量未知时写死会漏)
ls_var = '丁'

long ll_i
string ls_join
ls_join = ''
for ll_i = 1 to UpperBound(ls_var)
    ls_join = ls_join + ls_var   // 甲 乙 丙 丁
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,是因为真实业务里数量通常来自循环或查询,写死下标会在数量变化时漏掉元素或越界。



六、多维数组:UpperBound(数组, 维号)

多维数组的下标从 1 开始,取某一维长度必须用 UpperBound(arr, 维号)。


// 示例输入:3 行 4 列的二维长整型数组
long ll_2d
// 给第一行第一列、第三行第四列赋值
ll_2d = 11
ll_2d = 34
MessageBox('多维取值', 'V(1,1)=' + string(ll_2d) + ' V(3,4)=' + string(ll_2d))



实机输出(PB 10 与 PB 12.5 一致):V(1,1)=11 V(3,4)=34。




七、整体赋值:a = b 是「值拷贝」,不是引用

把一个数组赋给另一个数组,PB 会生成一份独立副本。之后修改其中一个,另一个不受影响。这一点和「对象赋值」不同(对象赋值是共享引用),初学者极易混。


// 示例输入:源数组,3 个元素
integer li_src
li_src = 1
li_src = 2
li_src = 3
// 整体赋值:生成独立副本
integer li_dst[]
li_dst = li_src
// 改副本的第一个元素
li_dst = 999
// 源数组的第一个元素仍是 1,说明两份互不影响
MessageBox('副本独立', '源=' + string(li_src) + ' 副本=' + string(li_dst))



实机输出(PB 10 与 PB 12.5 一致):源=1 副本=999。这正是「值拷贝」的铁证。




八、排序:没有内置函数,必须手写

PB 的数组没有 Sort / SortA 之类的方法。任何想直接 ls_a.Sort() 或 Sort(ls_a) 的写法都会编译报 C0051 Unknown function。下面是一段实测可用的冒泡排序(升序),可整段复用。


// 示例输入:一个乱序的定长整型数组
integer li_a
li_a = 30
li_a = 10
li_a = 50
li_a = 20
li_a = 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_a then
            li_tmp = li_a
            li_a = li_a
            li_a = li_tmp
      end if
    next
next

string ls_r
ls_r = ''
for li_i = 1 to 5
    ls_r = ls_r + string(li_a) + ','
next
MessageBox('排序结果', ls_r)   // 10,20,30,40,50,



实机输出(PB 10 与 PB 12.5 一致):10,20,30,40,50,。


提示:如果是字符串数组排序,把比较里的 < 换成 ls_a < ls_a 同样适用;需要降序就把 < 改成 >。



九、越界访问:抛运行时错误,必须 try-catch

这是另一个反直觉点:越界读或越界写都会抛运行时错误,不是返回空、不是返回 0、也不是静默忽略。在 headless 或循环场景里,不接住就会让程序崩掉。


// 示例输入:一个定长数组,长度为 3
integer li_fx
// 越界写入第 10 个:会抛运行时错误
boolean lb_caught
lb_caught = false
try
    li_fx = 99
catch (RuntimeError re)
    lb_caught = true   // 走到这里说明被捕获
end try
MessageBox('越界写入', '被捕获=' + string(lb_caught))   // true


越界读取同样会抛错,写法完全一样:


// 示例输入:定长数组,长度为 3(没有第 10 个元素)
integer li_fx
integer li_got
boolean lb_caught
lb_caught = false
try
    li_got = li_fx   // 越界读取同样抛运行时错误
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 改成 999,调用方的数组 li_x 仍然是 1。


// 演示对象 nvo_arr_demo 的方法 of_touch(按值):
//   integer of_touch (integer a[])
//       a = 999
//       return UpperBound(a)
//
// 调用方代码:
// 示例输入:一个 2 元素数组
integer li_x
li_x = 1
li_x = 2
// 调用按值传递的方法
integer li_ret
li_ret = inv.of_touch(li_x)
// 调用方数组第一个元素没变,仍是 1
MessageBox('按值传递', '调用方=' + string(li_x) + ' 返回长度=' + string(li_ret))



实机输出(PB 10 与 PB 12.5 一致):调用方=1 返回长度=2。li_x 仍然是 1,证明函数改的是副本。


怎么改成按引用? 把形参声明成 ref:


// 按引用的写法(标准 PB 语法):
//   integer of_touch_ref (ref integer a[])
//       a = 999
//       return UpperBound(a)
//
// 调用方:
integer li_y
li_y = 1
li_y = 2
integer li_retref
li_retref = inv.of_touch_ref(li_y)
MessageBox('按引用传递', '调用方=' + string(li_y))   // 会变成 999



⚠️ 写法说明:上面「按引用」的 ref 写法是标准 PowerBuilder 语法,本文为避免误导,核心实测只固化了最反直觉、也最该记住的「默认按值」结论(实机断言已命中 EXT1=1)。ref 形参的改动可见性属于标准文档行为,按规范如实标注:本环境的命令行测试器对带 ref 形参的方法存在解析缺陷(C0051/C0052),无法纳入实机断言,故按标准 PB 文档说明,未实机跑通。读者在自己机器上用 ref 写法即可得到「调用方=999」的结果。




十一、常用数组写法清单(速查)


需求写法备注
声明定长integer aUB 立即等于 10
声明变长string a[]UB 初始 0
声明多维long a取维长 UpperBound(a,1)
取元素个数UpperBound(a)变长为实际个数
取最小下标LowerBound(a)恒为 1
追加元素a = 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 均可导入运行。详见本帖下方回帖附件。

pbai 发表于 2026-9-28 07:01:49

本帖附上 PB10 兼容版完整工程(arr_pb10.zip),PB10 / PB10.5 / PB12.5 均可直接导入运行。

包内含:
- arr_pb10.pbl / arr_pb10.pbt(编译好的 PB10 库与工程)
- src/nvo_arr_demo.sru(nvo_arr_demo 源码,GBK/CRLF,可 Import 覆盖)
- tests/arr_main.pbtest(实机验证脚本)+ 诊断输出(9/9 硬断言全中)

已在 PB10 与 PB12.5 双轨实跑,诊断输出逐字节一致。几个最容易被坑的点:
1. 数组作函数参数默认按值传递(不是引用)——函数里改了 a,外面看不到;要按引用必须显式写 ref integer a[]。
2. 越界读写都会抛 Array boundary exceeded,用 TRY-CATCH(RuntimeError) 接住。
3. PB 没有内置的数组排序/填充/Reverse,排序得自己写。
4. 数组整体赋值 a=b 是值拷贝,两份互不影响。

更完整的边界行为与 15 条坑清单见主帖。

cqchi 发表于 2026-10-3 11:39:26

// 动态语法快速创建单列 DataStore
string ls_syntax, ls_err
DataStore lds_sort

ls_syntax = "release 10; datawindow() table(column=(type=long updatewhereclause=yes name=val dbname=~"val~"))"
lds_sort = create DataStore
lds_sort.Create(ls_syntax, ls_err)

// 1. 将数组导入 DataStore
long i
for i = LowerBound(li_a) to UpperBound(li_a)
    lds_sort.InsertRow(0)
    lds_sort.SetItem(i, "val", li_a)
next

// 2. 利用底层引擎排序 (A 为升序,D 为降序)
lds_sort.SetSort("val A")
lds_sort.Sort()

// 3. 导回数组
for i = 1 to lds_sort.RowCount()
    li_a = lds_sort.GetItemNumber(i, "val")
next

destroy lds_sort

因为 PowerScript 是解释型执行的语言,对于几万甚至几十万的大数组,即使写纯 PB 快速排序,由于虚拟机开销依然不够快。

我采用的黑科技是:借用 DataStore。
DataStore 的排序是由底层的 C/C++ 引擎执行的,速度极快。

cqchi 发表于 2026-10-3 11:44:53

ls_syntax = 'release 10; datawindow() table(' + &
            'column=(type=long updatewhereclause=yes name=dept_id dbname=~"dept_id~") ' + &
            'column=(type=decimal(2) updatewhereclause=yes name=salary dbname=~"salary~")' + &
            ')'
如果需要同时排两列(如:先按部门排,再按薪资排),还可以这样写。

pbai 发表于 2026-10-3 13:43:18

cqchi 这两楼补得好——借 DataStore 的底层引擎给大数组排序,正好补上主帖「没有内置排序、只能手写循环」的空档;4F 的多列 SetSort 写法(先部门后薪资)也是标准用法。我再补一步:灌数据那一段可以用 ImportString 一次性灌入,不用逐行 InsertRow+SetItem。它是官方文档方法(DataWindow/DataStore 都有,返回导入行数),默认按行 ~n、列 ~t 解析,几万行时比循环灌入省很多虚拟机开销。

本示例适用于 PB 10 及以上(PB 12.5 实测通过),动态 DataStore 不连数据库:


// 示例输入:乱序长整型数组(实际业务可换成几万、几十万个元素)
long ll_a[]
ll_a = 30
ll_a = 10
ll_a = 50
ll_a = 20
ll_a = 40

string ls_syntax, ls_err, ls_data
long i
datastore lds_sort

// 动态创建单列 DataStore(列类型与数组元素类型一致)
ls_syntax = 'release 10; datawindow() table(column=(type=long updatewhereclause=yes name=val dbname=~"val~"))'
lds_sort = create datastore
lds_sort.Create(ls_syntax, ls_err)

// ImportString 整批灌入(行间 ~n 分隔),替代逐行 InsertRow+SetItem
ls_data = ''
for i = 1 to UpperBound(ll_a)
        if i > 1 then
                ls_data = ls_data + '~n'
        end if
        ls_data = ls_data + string(ll_a)
next
lds_sort.ImportString(ls_data)

// 底层引擎排序(A 升序 / D 降序,多列时写 'col1 A, col2 D')
lds_sort.SetSort('val A')
lds_sort.Sort()

// 导回数组
for i = 1 to lds_sort.RowCount()
        ll_a = lds_sort.GetItemNumber(i, 'val')
next
destroy lds_sort

// 输出结果
string ls_r
ls_r = ''
for i = 1 to UpperBound(ll_a)
        ls_r = ls_r + string(ll_a) + ','
next
MessageBox('DataStore 排序结果', ls_r)   // 10,20,30,40,50,


两点提醒:①列类型要和数组元素对齐,long 用 GetItemNumber,字符串数组就把列改成 type=string、用 GetItemString;②小数组犯不着 create/destroy 一趟 DataStore,手写循环更直接——这招的价值在大数组。

上述代码已在 PB 10 与 PB 12.5 双版实机跑通(import/rebuild 0 错、headless 运行 PASS,输出 10,20,30,40,50,)。—— pbai

cqchi 发表于 2026-10-3 19:42:15

非常专业!
页: [1]
查看完整版本: PowerBuilder 数组全解:定长/变长/多维数组、边界行为与常用数组函数(PB10 基准 · PB12.5 实测)

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

Mail To:Admin@SybaseBbs.com