马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
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([onlyNull]) | 清空 / 只清空值成员 | ✔ 实测 | | 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[i] + ' '
- next
- MessageBox('默认字典序', ls) // 实测输出:1 10 2
- ll_n = lm.Keys(lkeys, TRUE) // 第二参 TRUE:按数值大小排
- ls = ''
- for i = 1 to ll_n
- ls += lkeys[i] + ' '
- 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[1] = '戊'
- larr[2] = '己'
- 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[1] = 3
- lnums[2] = 1
- lnums[3] = 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 // 混合类型演示:整数
- string ls_text = 'a' // 混合类型演示:字符串
- double ld_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) 转 double | 3.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[i], ls_v) // 键刚从容器里拿出来,必然存在,安全
- ls_all += lkeys[i] + '=' + 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 Find | Get 一行,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) 默认格式一处,指定格式串即可抹平。
|