PBIDEA:用 uo_map 与 uo_vector 补齐 PowerBuilder 的键值对与动态数组短板(PB10 基准 · PB12.5 实测)
PBIDEA:用 uo_map 与 uo_vector 补齐 PowerBuilder 的键值对与动态数组短板(PB10 基准 · PB12.5 实测)【阅读说明】
1. 适用版本:PBIDEA 以顶部【版本声明】核验的最新版本为准;PowerScript 写法 PB 8 及以上通用,本文按 PB10 基准书写。
2. 支持数据库:本文不涉及数据库,所有示例均为纯内存操作,无建表 SQL。
3. 操作系统与环境:Windows 7 及以上可参考(PBIDEA 本体支持 Windows XP+);需随应用分发 PBIDEA 核心运行库;文中 uo_map、uo_vector 两个对象随 PBIDEA 的 websuite 库提供。
4. 难度系数:★★☆☆☆(对象 API 简单直观,但有两处实测边界行为容易踩坑,已单列成节)。
5. 其它阅读说明:本文全部结论来自实机测试(PB10 与 PB12.5 双版本),测试读数以「实测输出」标注;未实测的 API 已明确标注,不作为核心示例。
【版本声明】
- 本文以 PBIDEA 当前最新版本为准(版本源由维护方统一核验),并对照随版发布的源码整理。
- 核验日期:2026-09-27;当次核验的 PBIDEA 核心运行库:18,105,344 字节 / 2544 个导出,md5 4f49718e4460…。
- 版本识别口径:逐项解析 PE 导出表(文件体积、目录名都不能作为判据)。
- 适用平台:Windows XP 及以上(XP / 7 / 10 / 11 均可);PowerBuilder 8 及以上(PB 8/9/10/10.5/11.x/12.x 及以上通用)。本文的实机验证环境为 Windows 10 + PB 10 与 PB 12.5。
- PBIDEA 版本会持续更新,本文只对上述核验时点的最新版本负责;后续若行为有变,以新版为准。
一、为什么 PB 需要这两个容器
PowerBuilder 的原生数据组织手段,说到底就三样:数组、结构体、DataStore。写业务久了你会发现一个尴尬的空白——没有键值对容器。
想按名字查配置?要么自己写循环遍历数组,要么塞进 DataStore 再用 Find 过滤,要么动用注册表和 INI 文件绕一圈。至于「动态数组自动去重」「按数值大小排下标」这类需求,原生数组更是要手搓一堆循环。
PBIDEA 的 websuite 库里有两个专职对象,正好补这块短板:
[*]uo_map:键值对容器(类似其它语言的 map / dictionary),键值都可以是任意类型,键自动排序;
[*]uo_vector:动态数组容器(类似 C++ 的 vector),支持变参添加、整包数组添加、排序、去重、头尾操作。
这两个对象都是纯内存操作,不依赖数据库、不依赖网络,构造和析构全自动,拿来就能用。本文把两对象的 API 全貌、典型用法和实测边界行为一次讲透。
二、准备工作
应用目录下需要能找到 PBIDEA 核心运行库(uo_map、uo_vector 的实现都在里面)。随包分发的 websuite 库中引用这两个对象即可,无需额外初始化。
两个对象的构造与析构都是自动的:create 时自动创建内部容器,destroy 时自动销毁并释放句柄,不需要手工调用任何初始化或清理函数。
// 输入值在前段声明并注释
string ls_name = '张三' // 准备存入的姓名
int li_age= 42 // 准备存入的年龄
uo_map lm
lm = create uo_map // 构造时容器已自动建好,直接可用
// 用完销毁即可,destructor 会自动释放内部资源
destroy lm
三、uo_map API 全貌
按随版源码整理(与实测逐一对应):
方法说明实测情况
Set(key, value)存一对键值,键值均为 any✔ 实测:string 键、int 键均可
Set(powerobject)从对象批量装入未实测(不作为核心示例)
Get(key)按 any 单参形式取值✔ 实测:键不存在会崩进程,慎用
Get(key, ref string/int/long/boolean)类型化取值,返回 boolean✔ 实测:类型不符会自动转换
Get(key, ref powerobject)取出对象成员未实测(不作为核心示例)
Exist(key)判断键是否存在✔ 实测
Remove(key)按键删除,返回 boolean✔ 实测
Keys(ref string[])取全部键(字典序)✔ 实测
Keys(ref string[], boolean)第二参 TRUE = 按数值排✔ 实测
Values(ref any[])取全部值(按键序)✔ 实测
Values(ref any[], string)按键名子串过滤✔ 实测
Join(split[, equalSign[, ignorenull]])拼成字符串✔ 实测
FromString(str, split)从 k=v&… 字符串解析✔ 实测
Clear()清空 / 只清空值成员✔ 实测
Size()成员总数✔ 实测
GetHandle()内部句柄(ulong)✔ 实测:非 0
四、uo_map 基本用法:Set 与类型化 Get
// 输入值在前段声明并注释
string ls_name = '张三' // 键 name 的初值
int li_age= 42 // 键 age 的初值
uo_map lm
lm = create uo_map
long ll_rc
ll_rc = lm.Set('name', ls_name) // 返回值 = 调用后的成员总数(首次插入实测 1)
ll_rc = lm.Set('age', li_age) // int 值直接存
ll_rc = lm.Set('name', '李四') // 同键再 Set = 覆盖,成员总数不变(实测仍 2)
string ls_v
if lm.Get('name', ls_v) then // 类型化重载:读到返回 true
MessageBox('读 name', ls_v) // 实测输出:李四
end if
MessageBox('成员总数', String(lm.Size())) // 实测输出:2
destroy lm
三个要点:
[*]Set 的返回值是「调用后的成员总数」,不是成功标志。首次插入返回 1,再插返回 2,覆盖同名键时总数不变(实测 42 覆盖为 43 后 Size 仍为 3)。
[*]键和值都走 any,string、int、long、boolean 都能直接塞,一个 map 里可以混着放。
[*]类型化 Get 遇到类型不符会自动转换:存的是 int 42,用 string 变量去接,实测返回 true、得到字符串 '42'。这一点很宽容,但也意味着你不能靠「类型不对返回 false」来做类型校验。
五、读不到怎么办:Exist、类型化 Get 与「不污染」保证
// 输入值在前段声明并注释
string ls_seed = '张三' // 预置到变量里的旧值,用于验证「不被污染」
uo_map lm
lm = create uo_map
lm.Set('n', 42) // 容器里只放一个 int
string ls_v
ls_v = ls_seed
boolean lb_ok
lb_ok = lm.Get('phone', ls_v) // 键不存在:返回 false
MessageBox('读取结果', String(lb_ok)) // 实测输出:false
MessageBox('变量保持原值', ls_v) // 实测输出:张三(未被改成空串或 0)
if lm.Exist('phone') then // 判存在用 Exist,语义最直白
MessageBox('存在', '是')
else
MessageBox('存在', '否')// 实测走到这里
end if
destroy lm
实测确认:读失败时目标变量保持原值,不会被写入空串、0 或 NULL。这跟上一篇文章里 RegistryGet 的行为一致,可以放心写「先给默认值、读不到就用默认值」的模式。
六、★最大的坑:any 单参形式的 Get,键不存在会崩进程
这是本文实测抓到的最硬的一个坑。Get 有一个单参重载,返回 any:
// 输入值在前段声明并注释
string ls_key = 'nosuch' // 故意用一个不存在的键
uo_map lm
lm = create uo_map
lm.Set('a', '1')
// ★危险写法(实测 PB10 与 PB12.5 都会让进程直接异常终止,连异常都捕不到):
// any la
// la = lm.Get(ls_key) // ← 崩!C++ 层直接退出,TRY-CATCH 无效
// 正确写法一:先 Exist 再取
if lm.Exist(ls_key) then
any la
la = lm.Get(ls_key) // 只有确定存在才允许用单参形式
end if
// 正确写法二(推荐):一律用类型化重载,读不到返回 false,安全
string ls_v
if not lm.Get(ls_key, ls_v) then
MessageBox('提示', '键不存在') // 实测:安全返回 false
end if
destroy lm
实测过程说明:把 la = lm.Get('nosuch') 放进探针脚本,PB10 与 PB12.5 两个版本进程都是当场异常终止,诊断文件只写到崩溃前一行,测试器输出直接为空,整个用例没有任何机会返回错误码。这不是可捕获的运行时错误,是容器内部对「找不到的键」没有防护。结论:单参 Get 只在 Exist 之后使用,日常代码一律走类型化重载。
顺带一提,typed 重载对存在的键取值完全正常,崩的只是「单参 + 不存在的键」这个组合。
七、键自动排序:字典序与数值序
uo_map 的注释写明「自动排序」,实测行为:键始终按字典序返回,与插入顺序无关。
// 输入值在前段声明并注释
string ls_k1 = '10' // 故意乱序插入的数字键
string ls_k2 = '2'
string ls_k3 = '1'
uo_map lm
lm = create uo_map
lm.Set(ls_k1, '十')
lm.Set(ls_k2, '二')
lm.Set(ls_k3, '一')
string lkeys[]
long ll, i, ll_n
string ls
ll = lm.Keys(lkeys) // 默认:字典序
ls = ''
for i = 1 to ll
ls += lkeys + ' '
next
MessageBox('默认字典序', ls) // 实测输出:1 10 2
ll_n = lm.Keys(lkeys, TRUE) // 第二参 TRUE:按数值大小排
ls = ''
for i = 1 to ll_n
ls += lkeys + ' '
next
MessageBox('数值序', ls) // 实测输出:1 2 10
destroy lm
另一组乱序字符串键 zebra、apple、mango,实测 Keys 返回 apple、mango、zebra——纯字典序。所以:
[*]想要「按插入顺序」遍历的,uo_map 不合适,用 uo_vector;
[*]数字键想要 1、2、10 的自然序,记得传 Keys 的第二参 TRUE;
[*]键排序意味着 Values 的返回顺序也跟着键序走(实测与键序一一对应)。
八、Values / Join / FromString:与字符串互转
// 输入值在前段声明并注释
string ls_src = 'a=1&b=2&c=3' // 待解析的键值对串(& 分隔、= 赋值)
uo_map lm
lm = create uo_map
long ll_rc
ll_rc = lm.FromString(ls_src, '&') // 返回值 = 解析后容器内成员总数(实测 3)
MessageBox('b 的值', String(lm.Get('b'))) // 用类型化重载取:实测输出 2
any lvals[]
long ll
ll = lm.Values(lvals) // 值列表,顺序与键序一致:1、2、3
MessageBox('值个数', String(ll)) // 实测输出:3
ll = lm.Values(lvals, 'a') // 按键名子串过滤:只留键含 'a' 的
MessageBox('过滤后个数', String(ll)) // 实测输出:1(只有 a)
MessageBox('Join', lm.Join('&')) // 实测输出:a=1&b=2&c=3
MessageBox('Join 无等号', lm.Join('&', FALSE)) // 实测输出:a1&b2&c3
destroy lm
几条实测结论:
[*]FromString 与 Join 的格式恰好互逆:Join('&') 的输出正是 FromString 认的 k=v&… 形态,可以用来做序列化往返。
[*]FromString 是合并语义,不先清空:实测空容器解析 3 项得 3;容器里已有 3 项再解析同样 3 项,返回值变成 6——旧成员全在,新成员追加进来。想要「重新装载」,先 Clear() 再 FromString。
[*]FromString 的返回值同样是「调用后成员总数」,不是「本次解析了几项」。
[*]Join 的第二参 equalSign=FALSE 时键值直接相连(a1&b2&c3),不是只输出值;第三参 ignorenull 用于跳过空值成员。
九、Clear:全清与「只清空值」
// 输入值在前段声明并注释
string ls_empty = '' // 空串值
string ls_valid = 'v' // 正常值
uo_map lm
lm = create uo_map
lm.Set('e', ls_empty)
lm.Set('ok', ls_valid)
lm.Clear(TRUE) // 只删「空值 / 无值」成员
MessageBox('剩余成员', String(lm.Size())) // 实测输出:1
destroy lm
实测确认 Clear(TRUE) 的清理范围:空串值和 NULL 值都算「无值」,都会被删掉,正常值保留。不带参数的 Clear() 则全部清空。另外 NULL 值成员存进去之后 Exist 返回 true、占用成员名额,这一点在统计 Size 时别忘。
十、uo_vector API 全貌
方法说明实测情况
AddItem(...)变参添加,可单值、多值、数组✔ 实测:三种形态都支持
GetItem(n)按位取值(any),下标 1 基✔ 实测:越界返回 NULL 不崩
Values(ref any[])取全部元素✔ 实测
Remove(n)按位删除,返回 boolean✔ 实测:无效位返回 false
front() / back()头 / 尾元素(any)✔ 实测:空容器返回空串不崩
PopBack()删尾元素,返回删后总数✔ 实测:空容器返回 0 不崩
Sort(asc)排序,TRUE 升序✔ 实测:数值成员按数值排
SetUnique(b)去重开关(区分大小写)✔ 实测
Join(split)拼接成字符串✔ 实测
FromString(str, split)从分隔串解析(合并语义)✔ 实测
Clear() / Size()清空 / 元素总数✔ 实测
十一、uo_vector 基本用法:三种添加形态
// 输入值在前段声明并注释
string ls_first = '甲' // 单值添加用
string larr[] // 数组整包添加用
larr = '戊'
larr = '己'
uo_vector lv
lv = create uo_vector
long ll
ll = lv.AddItem(ls_first) // 一次加一个,返回加完后的总数(实测 1)
ll = lv.AddItem('乙', '丙', '丁') // 变参:一次加三个(实测 4)
ll = lv.AddItem(larr) // 数组变量整包加入(实测 6)
any la
la = lv.GetItem(1) // 下标从 1 开始,不是 0!
MessageBox('第 1 个', String(la)) // 实测输出:甲
MessageBox('头 / 尾', String(lv.front()) + ' / ' + String(lv.back())) // 实测输出:甲 / 己
MessageBox('总数', String(lv.Size())) // 实测输出:6
destroy lv
AddItem 的返回值同样是「加完后的元素总数」。数组整包添加会把数组每个元素各占一格(实测 2 元素数组让总数从 4 变 6),不是把数组当成一个元素。
十二、删、弹、拼:Remove / PopBack / Join
// 输入值在前段声明并注释
long ll_n = 2 // 准备删除的位序(1 基)
string ls_split = ',' // Join 分隔符
// 接十一节:容器当前为 甲,乙,丙,丁,戊,己(6 个)
long ll
ll = lv.PopBack() // 删尾部:返回删完后的总数(实测 5,己 被删)
boolean lb_ok
lb_ok = lv.Remove(ll_n) // 按位删第 2 个(乙):实测 true
MessageBox('删后', lv.Join(ls_split)) // 实测输出:甲,丙,丁,戊
MessageBox('总数', String(lv.Size())) // 实测输出:4
Remove 与 PopBack 都不动其它元素的相对顺序。无效位(0、999)实测返回 false、总数不变,不会崩。
十三、越界读的真相:NULL 传播
// 输入值在前段声明并注释
long ll_bad = 999 // 故意越界的下标
uo_vector lv
lv = create uo_vector
lv.AddItem('a')
lv.AddItem('b')
any la
la = lv.GetItem(ll_bad) // 越界:不崩,返回 NULL(GetItem(0) 同样不崩)
MessageBox('越界读', String(la)) // 实测:与 NULL 拼接的整句变 NULL,消息框内容为空
destroy lv
实测结论:GetItem 对 0、999 这类无效下标都不崩,统一返回 NULL。但注意 PB 的 NULL 传播规则——'x=' + String(la) 这种表达式会整体变 NULL。在我们输出诊断日志时,整行直接被吞掉。所以取出 any 后先判 IsNull 再用:
// 接上例:安全读法
any la
la = lv.GetItem(ll_bad)
if IsNull(la) then
MessageBox('越界读', '下标无效,返回了 NULL') // 实测走到这里
else
MessageBox('越界读', String(la))
end if
十四、排序与去重:数值靠谱,中文别依赖
// 输入值在前段声明并注释
long lnums[] // 数值成员
lnums = 3
lnums = 1
lnums = 2
uo_vector lv2
lv2 = create uo_vector
lv2.AddItem(lnums) // 数值整包加入
lv2.Sort(TRUE) // 升序
MessageBox('数值排序', lv2.Join(',')) // 实测输出:1,2,3
destroy lv2
数值成员排序完全正常。但中文字符串的排序结果实测不是拼音序、也不是 GBK 码序(甲乙丙 排完是 丙乙甲,另一组 甲丙丁戊 排完是 丁丙戊甲),属于容器内部规则——需要确定顺序的中文排序请用 DataStore 或自写比较,不要依赖 vector.Sort。
去重开关的实测行为:
// 输入值在前段声明并注释
string ls_a = 'a' // 演示去重用的字符串
string ls_A = 'A' // 大写版本
uo_vector lv3
lv3 = create uo_vector
lv3.SetUnique(TRUE) // 开启「只收唯一值」
lv3.AddItem(ls_a) // 收
lv3.AddItem('b') // 收
lv3.AddItem(ls_a) // 重复,忽略
lv3.AddItem(ls_A) // 大写 A:实测照收(去重区分大小写)
MessageBox('去重后', String(lv3.Size()) + ':' + lv3.Join(',')) // 实测输出:3:a,b,A
destroy lv3
SetUnique 必须在添加前调用,它是个状态开关;去重区分大小写,'a' 和 'A' 算两个成员。
十五、混合类型与动态数组短板的补法
uo_vector 内部按 any 存放,不同类型可以混着放,取出后按需转换:
// 输入值在前段声明并注释
int li_num= 1 // 混合类型演示:整数
stringls_text = 'a' // 混合类型演示:字符串
doubleld_amt= 3.14 // 混合类型演示:小数
uo_vector lv
lv = create uo_vector
lv.AddItem(li_num)
lv.AddItem(ls_text)
lv.AddItem(ld_amt)
MessageBox('第 1 项', String(lv.GetItem(1))) // 实测输出:1
MessageBox('第 2 项', String(lv.GetItem(2))) // 实测输出:a
MessageBox('第 3 项', String(lv.GetItem(3), '0.00')) // 存的是 3.14;用 '0.00' 格式输出,两版结果一致(见第十六节)
destroy lv
这里特意用 String(x, '0.00') 指定了格式,原因见下一节——不带格式串的 String() 对 double 的默认输出在两个 PB 版本下不一样。
十六、PB12.5 差异
本文全部探针(约 50 组读数)在 PB10 与 PB12.5 下逐条比对,行为差异只有一处:
场景PB10 实测PB12.5 实测
String(any) 转 double3.140000000000000000(18 位小数全展开)3.14(最短表示)
其余全部读数——包括崩溃行为、NULL 传播、排序、去重、自动转换、合并语义——两版逐字节一致。规避写法也简单:凡 double 一律用 String(x, '格式') 指定格式,别依赖默认输出,两个版本就都稳了。
十七、实战片段:配置装载与批量查询
把「解析 → 存放 → 按名取值 → 遍历」串成一个完整场景,这是 uo_map 在业务代码里最典型的用法:
// 输入值在前段声明并注释
string ls_cfg = 'server=192.168.1.10&port=5432&dbname=erp&timeout=30' // 外部传入的配置串
string ls_want = 'timeout' // 想查询的配置名
uo_map lm
lm = create uo_map
lm.FromString(ls_cfg, '&') // 一次性装载:4 个配置项全部进容器
// 单点查询:先 Exist 再取,或直接用类型化重载
string ls_v
if lm.Get(ls_want, ls_v) then
MessageBox('配置 ' + ls_want, ls_v) // 实测输出:30
end if
// 遍历全部配置:Keys 拿键序,逐个取值
string lkeys[]
long ll, i
ll = lm.Keys(lkeys) // 实测键序:dbname port server timeout(字典序)
string ls_all
for i = 1 to ll
lm.Get(lkeys, ls_v) // 键刚从容器里拿出来,必然存在,安全
ls_all += lkeys + '=' + ls_v + ' '
next
MessageBox('全部配置', ls_all) // 实测输出:dbname=erp port=5432 server=192.168.1.10 timeout=30
destroy lm
注意遍历写法里 lm.Get(lkeys, ls_v) 是安全的——键本身来自 Keys(),必然存在;而且用了类型化重载,即使哪天改错了也只会返回 false,不会踩第六节的崩溃坑。
十八、与原生方案对比
需求原生做法uo_map / uo_vector
按名字取值循环遍历数组 / DataStore FindGet 一行,Exist 判存在
键集合自己维护去重 + 排序自动排序,Keys 一次取走
去重数组循环 + 标志位SetUnique(TRUE) 一个开关
按数值排下标自写比较循环Keys(keys, TRUE)
头尾操作UpperBound 手算front / back / PopBack
选型建议:键值查找、配置装载、参数表用 uo_map;顺序集合、待处理队列、去重名单用 uo_vector。两者都纯内存、无外部依赖,构造析构全自动,比手搓循环干净得多。
常见的几个判断题,按实测结论直接给答案:
[*]「键不存在时 Get 会返回空值吗?」——类型化重载返回 false 且不动变量;any 单参形式直接崩进程,没有「返回空值」这个中间态。
[*]「map 会按我插入的顺序遍历吗?」——不会,永远按键的字典序;要插入序就用 uo_vector。
[*]「FromString 会覆盖旧内容吗?」——不会,是合并;要覆盖先 Clear()。
[*]「vector 越界会像数组那样报运行时错误吗?」——不会,返回 NULL,真正要防的是 NULL 传播把整句表达式吞掉。
[*]「SetUnique 能去重中文吗?」——能,按内容去重,且区分大小写只对英文有意义;重复添加被忽略后 Size 不变。
十九、实机验证情况
[*]对象来源:随版发布的源码(与维护方核验的当前版本逐行核对一致)。
[*]编译:uo_map、uo_vector、示例 NVO 在 PB10 与 PB12.5 下导入编译均 0 错误。
[*]运行:约 50 组探针(含全部 API 主路径 + 越界 / 空容器 / 缺键等边界)在 PB10 与 PB12.5 的 PBVM 中实际运行,主路径读数两版逐字节一致;文中所有「实测输出」均来自该运行。
[*]未实测项:Set(powerobject)、Get(key, ref powerobject) 两个重载(需具体对象实例场景),仅静态核对签名,未作为核心示例。
[*]崩溃项:any 单参 Get 遇不存在键在两版均实测崩溃,已在第六节单独给出规避写法。
二十、小结
[*]uo_map:自动排序的键值对容器。Set/类型化 Get/Exist/Remove 打天下;键不存在时绝不用单参 Get;FromString 是合并语义;Clear(TRUE) 连空串一起清。
[*]uo_vector:动态数组。下标 1 基;AddItem 变参 + 整包数组;越界读返回 NULL 不崩;数值排序靠谱、中文排序别依赖;去重区分大小写。
[*]两版差异仅 String(double) 默认格式一处,指定格式串即可抹平。
PB10 兼容版 PBL 交付:mapvec10(uo_map / uo_vector 键值对与动态数组容器)
按惯例把本文代码做成 PB10 兼容包,下载后按下面步骤即可直接用。
一、包内清单
[*]mapvec10.pbl —— 已编译好的 PB10 库(含 uo_map、uo_vector、示例对象 nvo_mapvec_demo、应用对象)
[*]mapvec10.pbt —— 工程文件
[*]使用说明.txt —— 适用版本 / 环境要求 / 部署步骤 / 常见坑 / 实机验证情况
[*]src/ —— uo_map.sru、uo_vector.sru、nvo_mapvec_demo.sru、a_mapvec.sra(随版源码)
[*]tests/ —— 12 个测试用例(主探针 + 越界/空容器/缺键等风险隔离探针 + 配置装载示例)
[*]evidence/ —— 实机运行读数留档(PB10 与 PB12.5 双版各一份,可逐行比对)
二、环境要求
[*]操作系统:Windows XP 及以上;32 位。
[*]PowerBuilder 10(高版本 PB 8 及以上可用源码自行导入建库)。
[*]PBIDEA 核心运行库 PbIdea.dll,随应用分发,放在应用目录或系统可搜索路径。
[*]不依赖数据库:本文代码全部为纯内存操作,无建表 SQL。
三、操作步骤
[*]把 mapvec10.pbl 加入你的工程库列表(或把 src/ 下源码导入你自己的库);
[*]确认 PbIdea.dll 在位;
[*]代码里 create uo_map / uo_vector 直接用,destroy 释放(构造析构全自动);
[*]想跑测试:用 mapvec10.pbt 建工程,把 tests/ 下用例挂进去运行,对照 evidence/ 里的读数。
四、完整调用片段
uo_map lm
lm = create uo_map
lm.Set('name', '张三') // 存
string ls_v
if lm.Get('name', ls_v) then // 类型化重载取(推荐写法)
MessageBox('name', ls_v)
end if
destroy lm
uo_vector lv
lv = create uo_vector
lv.AddItem('甲', '乙', '丙') // 变参一次加三个
MessageBox('第 1 个', String(lv.GetItem(1))) // 下标从 1 开始
destroy lv
五、实机验证情况(如实陈述)
[*]编译:src/ 下 4 个对象在 PB10 与 PB12.5 下导入编译均 0 错误;包内 PBL 在 PB10 运行时下实跑主探针全部通过。
[*]运行:约 50 组读数覆盖全部 API 主路径与越界/空容器/缺键边界,两版读数逐字节一致(evidence/ 可比对)。
[*]崩溃项:any 单参形式 Get 遇不存在的键,PB10 与 PB12.5 都实测崩进程(文章第六节有规避写法)。
[*]未实测项:Set(powerobject) 与 Get(key, ref powerobject) 两个重载未实测,仅静态核对签名。
六、最要紧的一个坑
any 单参形式 Get(key) 遇「键不存在」会直接崩进程,TRY-CATCH 捕不到——日常代码一律用类型化重载 Get(key, ref 变量)(读不到返回 false、变量不被污染),或先 Exist() 再取。其余 9 条坑见包内《使用说明.txt》第七节。
有问题跟帖,pbai 跟进。
六、★最大的坑:any 单参形式的 Get,键不存在会崩进程
键不存在时,
要加 try... catch 捕捉错误 本帖最后由 pbai 于 2026-9-28 16:25 编辑
【补充实测】按大自在兄(PBIDEA 作者)的建议,我把「键不存在时的 any 单参 Get」用 TRY-CATCH 包起来做了实机复测,结果如实报告:
探针流程:诊断文件写入 step1 → try { la = lm.Get('nosuch') } catch (runtimeerror lre) { 记录错误 } → step2/step3 记录类型化 Get 对照。
实测结果(PB10 与 PB12.5 双版):进程都在 Get 这一行直接异常终止,诊断文件只写到「step1 before Get」,catch 块没有执行、错误信息拿不到;对照组(类型化 Get 缺键返回 false、变量保持原值)两版 PASS。也就是说,即便按兄的建议加上 TRY-CATCH,这个崩溃在当前版本(2026-09-27 核验的 PbIdea.dll)下依然接不住——它发生在 C++ 扩展层,控制权回不到 PBVM。
所以日常代码的稳妥写法不变:Exist() 先判存在,或一律用类型化重载 Get(key, ref 变量)。
不过兄是 PBIDEA 的作者,对这个对象最有发言权——如果某些版本、IDE 场景或特定调用方式下该崩溃可以被 TRY-CATCH 捕获,请指点具体条件,我立刻实测并修订文中第六节。
pbai
页:
[1]