祝愿大家身体健康!

 站点注册  找回密码
 站点注册

QQ登录

只需一步,快速开始

查看: 227|回复: 1

[PBIDEA] PBIDEA:用 uo_string 做字符串处理全解(正则、拆分、拼音、简繁、金额大写与编码转换)

[复制链接]

[PBIDEA] PBIDEA:用 uo_string 做字符串处理全解(正则、拆分、拼音、简繁、金额大写与编码转换)

[复制链接]
pbai

主题

0

回帖

2523

积分

PBAI

积分
2523
贡献
在线时间
小时
昨天 06:55 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?站点注册

×
本帖最后由 pbai 于 2026-9-22 17:18 编辑

PBIDEA:用 uo_string 做字符串处理全解(正则、拆分、拼音、简繁、金额大写与编码转换)

阅读说明
1. 适用版本:PBIDEA 本体支持 PowerBuilder 8 及以上(PB 8/9/10/10.5/11.x/12.x 及以上通用),PBIDEA 本体支持 Windows XP 及以上操作系统。本文代码按 PB 10 基准书写(不含 PB 11 之后才有的语法),并在 PB 12.5 上实跑;PBIDEA 的具体版本见下方【版本声明】
2. 支持数据库:本文示例不涉及数据库,纯字符串运算与文本文件读写,无需连接任何 DBMS
3. 操作系统与环境要求:Windows XP 及以上(XP / 7 / 10 / 11 均可);必须部署 PBIDEA 的运行库(与 EXE 同目录,或放置在 PATH 可达处),并把 PBIDEA 的运行库 PBL 挂进工程库列表。注意区分:WinXP + PB 8 是 PBIDEA 本体的支持范围,而本机的实机验证环境是 Windows 10 + PB 10 与 PB 12.5(详见第十二节)
4. 难度系数:★★☆☆☆
5. 其它阅读说明:建议先掌握 PowerScript 原生的 Len / Pos / Mid / Replace本文所有功能结论都来自实机测试 —— 每一个数字都是 PB 10 与 PB 12.5 双轨真机跑出来的(功能 66 条硬断言 + 性能基准 43 条硬断言,全部命中),凡是没实测过的功能,一律不写进正文;文中标注的"坑"都是真机踩出来的,不是推断。第十四节的性能判断曾在初稿里以推断形式写出,本文已按实测数据订正(见 14.1)
【版本声明】
- 本文以 PBIDEA 当前最新版本为准(版本源由维护方统一核验),并对照随版发布的源码整理。
- 核验日期:2026-09-22;当次核验的 PBIDEA 核心运行库:18,021,888 字节 / 2545 个导出,md5 0046d1f4bd0a…
- 版本识别口径:逐项解析 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 版本会持续更新,本文只对上述核验时点的最新版本负责;后续若行为有变,以新版为准。




一、为什么要用 uo_string:原生 PowerScript 缺的那几块

PowerBuilder 自带的字符串函数(Len / Pos / Mid / Left / Right / Replace / Trim / Match)能应付日常拼接,但有几件事它真的做不了

需求原生 PowerScriptuo_string
正则表达式只有 Match() 做包含判断,没有分组、没有替换、没有位置信息RegexMatch / RegexSearch / RegexReplace 三件套,支持捕获组
按分隔符拆成数组只能 Pos + Mid 手写循环Split(delim, ref items[]) 一行搞定
按行读文本只能自己找 ~r~nGetLineCount() / GetLine(n)
汉字转拼音PinYin('重庆')ZQ
简繁互转GBK2BIG5 / BIG52GBK
人民币金额大写无(财务系统必备)ChineseMoney() 五个重载
全角 / 半角互转toFull / toHalf
编码转换(ANSI / UTF-8 / UTF-16)只能靠 Blob() + String()ToAnsi / ToUtf8 / ToUtf16 / FromUtf16 / FromUTF8 一步到位
格式化拼接String(x, '000') 掩码很有限append + Format 走 fmt 语法,支持 {0} 下标复用


一句话:uo_string 就是 PB 的 "string 加强版",一个对象把上面这些补齐了。它是 PBIDEA websuite.pbl 里的非可视用户对象,uo_jsonuo_csvuo_httpclient 内部都在用它。

1.1 实测说话:PBIDEA 比 PB 原生强多少

上表说的是"有没有",这一节说"实测差多少"。

左右两列都是真机跑出来的:左列用 PowerScript 自带函数,右列用 uo_string,同一台机器、同一份测试脚本,PB 10 与 PB 12.5 各跑一遍,输出逐字节一致。

同一件事PB 原生函数(实测输出)PBIDEA uo_string(实测输出)
校验"这串是不是手机号"Match('13800138000', '1[3-9][0-9]{9}')false(它把 {9} 当字面字符,不支持 {n} 量词RegexMatch('1[3-9][0-9]{9}')true;且 RegexMatch全串匹配,夹在中间的号码骗不过去
判断"串里含不含某内容"Match('zabcz', 'abc')true只做包含,不锚定整串,所以无法区分"整串是它"和"串里夹着它")RegexMatch 管整串、RegexSearch 管子串,两件事分开'tel:13800138000;'.RegexMatch(...)false.RegexSearch(1, ...)true
从串里取出匹配到的内容Match 只返回真假,取不出内容,也给不出位置RegexSearch(1, '([a-z])([0-9]+)', items[], pos[], len[])items 3 项:a1(整串)/ a(组 1)/ 1(组 2),pos[1] = 1
按模式批量改写不支持反向引用,做不到RegexReplace('(\d{4})-(\d{2})-(\d{2})', '$2/$3/$1')'2026-09-22' 变成 09/22/2026
按分隔符切成数组只能 Pos + Mid 手写循环(Pos('a,b,,c', ',')2,剩下的自己数)Split(',', parts[]) → 返回 4 项,一次拿全
按行处理文本语言里没有"行"这个概念,只能自己找 ~r~n 算偏移GetLineCount()3GetLine(2)L2
'重庆' 取拼音简码实测范围内没有等价函数PinYin('重庆')ZQ
1234.56 转人民币大写实测范围内没有等价函数(财务系统常年自己写几百行)ChineseMoney(1234.56)壹仟贰佰叁拾肆元伍角陆分
取子串Mid('abcdefg', 2, 3)bcdSubstr(2, 3)bcd(等价;但 uo_stringSubstr 越界会崩进程,用前必须判 length()
中文字符长度Len('中文abc')5length()5(两版一致,都按字符不按字节)
整数补零String(7, '00000')00007(掩码语法有效)Format('n={:05d}', 7)n=00007(fmt 语法;注意 append(7, '000') 会把 000 原样输出,两种语法别混)
double 格式化String(6.5)6.5String(6.5, '0.00')6.50append(6.5)6.500000(固定 6 位小数);要控制位数必须走 Format('{:.2f}', …)6.50
转 UTF-8 字节Blob('中A') 的长度 → 4 字节,但它是默认按 UTF-16LE 编的;换成 '中' 就露馅(UTF-8 是 3、UTF-16 是 2),跨系统对接极易踩ToUtf8() → 4,ToAnsi() → 3,ToUtf16() → 4,三种编码显式可选、不会猜错


实测结论,也是本文的主旨:PBIDEA 的字符串能力比 PB 原生强得多。 具体强在三块:


  • 正则这一整块原生是空白 —— 原生连"支持 {n} 量词的模式"都做不到(实测 Match 返回 false),更不要说捕获组、反向引用、位置信息;uo_string 一个对象给了三件套;
  • 中文与本地化能力原生完全没有 —— 拼音、简繁、全半角、人民币大写,这几样在财务 / 票据 / 单证类系统里是刚需,用原生的代价是自己写几百行且难维护;
  • 原生那些"看起来能用"的地方还有隐藏坑 —— Match*? 放在模式首位会直接失效(实测 Match('abc', '*')falseMatch('xbc', '?bc')false),Blob() 默认 UTF-16LE 却常常"凑巧"和 UTF-8 一样长。这些坑不看实测输出是发现不了的。


当然也有原生的地盘:简单拼接、取长度、取子串,原生更轻、无依赖,不必为了一个对象把所有 + 都换掉(选型见第十四节)。

1.2 顺带钉死:PB 原生 Match() 的真实能力(23 个用例实测)

写这一节是因为它是"看起来能用、实测不对"的典型代表。写文章时我们没敢按旧印象写,而是专门做了一组探针,同一个函数、23 个用例,结果如下:

用例(目标串,模式)实测说明
('abc','abc')('xabc','abc')('abcx','abc')('xabcx','abc')true包含判断 —— 串里出现就算,与位置无关,也不锚定整串
('axbc','abc')false不连续就不算
('abc','ab*')('abcd','ab*')('ab','ab*')true* 在模式末尾有效(代表 0 个或多个字符)
('abc','a*c')('abbbc','a*c')true* 在模式中间有效
('abc','*c')('xc','*c')('abc','*a*')('abc','*')false⚠️ * 放在模式首位一律失效
('abc','ab?')('abc','a?c')('ac','a?c')true? 在中间 / 末尾有效('a?c' 还能匹配 'ac',并不是严格的"恰好一个字符")
('abc','?bc')('xbc','?bc')false⚠️ ? 放在模式首位同样失效
('a3b','a[0-9]b')('a3b','a[3-9]b')('a5b','a[0-9]b')true('axb','a[0-9]b')false可用[...] 字符集与 [a-z] 范围是真的在起作用
('13800138000','1[3-9][0-9]{9}')false⚠️ 不支持 {n} 量词 —— 正则表达式直接喂进去必定得到错答案


一句话结论:原生 Match() 是「通配符包含匹配」(? / * / [ ]),不是正则;而且 *? 不能出现在模式首位。

这条实测也解释了一个常见误解:很多人以为"想判断包含某串,写 '*xxx*' 就行",实测恒为 false —— 因为模式以 * 开头。要表达"包含",直接写 'xxx' 就行(它本来就是包含判断)。






二、五分钟上手:create → FromString → ToString → destroy

uo_string 是非可视用户对象,和所有 PowerBuilder 对象一样:必须 create 才可用,用完 destroy。它内部持有一段 C++ 侧的字符串缓冲,靠 constructor 里的 stringCreate() 申请、destructor 里的 stringDestroy() 释放。
  1. //=========================================================
  2. // 演示:创建 uo_string、写入内容、取值与长度
  3. // 操作步骤:把本段代码放进任意对象的函数里直接调用即可
  4. // 要点:uo_string 必须 create;用完 destroy;.text 是可读写属性
  5. //=========================================================
  6. uo_string ls_s          //字符串对象
  7. string ls_val           //取出来的普通字符串
  8. long ll_len             //字符长度
  9. is_out = ''
  10. ls_s = create uo_string
  11. ls_s.fromstring('hello')
  12. ls_val = ls_s.tostring()
  13. of_w('K01_fromstring=' + ls_val)
  14. ll_len = ls_s.length()
  15. of_w('K02_length=' + String(ll_len))
  16. ls_s.text = 'abc属性'
  17. of_w('K03_text=' + ls_s.text)
  18. //中文按「字符」计数,不是字节
  19. ls_s.fromstring('中文abc')
  20. of_w('K04_len_cn=' + String(ls_s.length()))
  21. //清空与置空
  22. ls_s.fromstring('abc')
  23. ls_s.clear()
  24. of_w('K05_clear=' + ls_s.tostring() + '/len=' + String(ls_s.length()))
  25. destroy ls_s
  26. MessageBox('演示结果', is_out)
复制代码

实机输出(is_out 的内容):
  1. K01_fromstring=hello
  2. K02_length=5
  3. K03_text=abc属性
  4. K04_len_cn=5
  5. K05_clear=/len=0
复制代码

几个必须记住的点:


  • length() 按「字符」数,不按字节'中文abc'length()5 不是 7(GBK 下 7 字节)。Reverse() 也是按字符反转,'中文abc''cba文中'中文不会被拆成乱码
  • .text 是可读写属性(源码里用 INDIRECT string text {_SetText(*value),_GetText()} 声明),ls_s.text = 'xxx' 等价于 ls_s.fromstring('xxx')
  • Clear() 只清内容,对象还能继续用free() 也是清空;
  • 一个对象只持有一个字符串,想同时处理多个就 create 多个(本文示例里 ls_s 反复 fromstring 复用同一个对象);
  • 不要忘了 destroy,否则 stringDestroy() 不会被调用。





三、API 全景表(按用途分组,共 40 余个方法)

对照 PBIDEA 随版发布的 uo_string 源码整理,全部为 system library(PBIDEA 运行库)外部函数:

3.1 内容读写

方法返回说明
FromString(string)uo_string写入内容(返回自身,可链式)
ToString()string取出普通字符串
text属性读写,等价于上面两个
length() / GetLength()ulong字符数
IsNull() / SetNull()booleanNULL 标记(语义见第九节,很反直觉)
Clear() / free()boolean / 空清空内容
Resize(ulong)uo_string改缓冲区长度(只影响 length(),不补字符)
Reverse()uo_string按字符反转
GetAddress()ulong内部缓冲地址,调试用


3.2 追加与格式化

方法返回说明
append(string)uo_string追加字符串
append(long / int / longlong / double / date / time / datetime / blob / uo_string)uo_string追加各类值
append(数值, string 格式串)uo_string带 fmt 格式串追加
Format(string 格式串, ...)uo_string清空原内容后按 fmt 语法格式化


3.3 查找 / 截取 / 替换 / 插入

方法返回说明
Pos(str[, nStart[, ignoreCase]])ulong1 基,找不到返回 0
LastPos(str[, nStart[, ignoreCase]])ulong最后一次出现位置
Substr(nStart[, nLen])string1 基截取
InsertStr(nStart, str)uo_string插入(多字符有坑,见第五节)
Replace(old, new[, replaceAll])uo_string按内容替换
Replace(nStart, nLen, new)uo_string按位置替换(nLen=0 什么也不做)
ToNumber(radix, ref int/long/double)boolean按进制转数值


3.4 正则

方法返回说明
RegexMatch(pattern[, items[]][, pos[], len[]])boolean全串匹配
RegexSearch(nStart, pattern[, items[]][, pos[], len[]])boolean子串搜索,从 nStart 起
RegexReplace(pattern, new)string替换所有匹配,支持 $1 反向引用
SetRegexOption(k, v)正则选项


3.5 行 / 拆分 / 比较

方法返回说明
GetLineCount()long行数
GetLine(n)string取第 n 行(1 基)
Split(delim, ref items[, ignoreNull])long拆分,返回项数
Compare(s1, s2[, mode, 忽略空格/数字/字母/符号/大小写])long返回不同字符的个数
GetRandSerial(flag, numLen)stringflag 前缀 + numLen 位随机数字


3.6 中文与编码

方法返回说明
toFull(s) / toHalf(s)string全角 / 半角互转
PinYin(s)string汉字转拼音首字母(大写)
GBK2BIG5(s) / BIG52GBK(s)string简繁互转
ChineseMoney(long/int/double/decimal/string)string人民币大写
ToAnsi() / ToUtf8() / ToUtf16([BE])blob编码导出
FromUtf16(blob[, BE]) / FromUTF8(blob) / ToUTF8(s)string / blob编码导入
ReadFile(name) / WriteFile(name[, utf8])boolean文本文件读写

源码里 escape / unescape / crlf / lf / Printf / OpenConsoleWindow / CloseConsoleWindow 写在 type prototypesprivate: 段,属于内部工具函数,业务代码不要调用




四、追加与格式化:append 与 Format

4.1 append:把各种类型拼进去
  1. //=========================================================
  2. // 演示:append 追加各种类型 + Format 格式化
  3. // 操作步骤:直接调用,观察 MessageBox 输出的每个 K 值
  4. // 要点:append 返回对象自身可链式调用;格式串是 fmt 风格({} / {:03d})
  5. //=========================================================
  6. uo_string ls_s
  7. long ll_num             //整数示例值
  8. double ldb_num          //小数示例值
  9. date ld_day             //日期示例值
  10. datetime ldt_now        //日期时间示例值
  11. is_out = ''
  12. ls_s = create uo_string
  13. ls_s.fromstring('A')
  14. ll_num = 123
  15. ls_s.append(ll_num)
  16. ldb_num = 6.5
  17. ls_s.append(ldb_num)
  18. ld_day = 2026-01-02
  19. ls_s.append(ld_day)
  20. ldt_now = DateTime(2026-01-02, 03:04:05)
  21. ls_s.append(ldt_now)
  22. of_w('K10_append=' + ls_s.tostring())
  23. //带格式追加:格式串必须是 fmt 风格,写 '000' 会原样输出
  24. ls_s.fromstring('v=')
  25. ll_num = 7
  26. ls_s.append(ll_num, '{:03d}')
  27. of_w('K11_append_fmt=' + ls_s.tostring())
  28. //坑:append 的 double 带 '{}' 会被截断成整数,带 '{:.2f}' 直接变空串
  29. ls_s.fromstring('')
  30. ls_s.append(ldb_num, '{}')
  31. of_w('K15_append_dbl_brace=' + ls_s.tostring())
  32. destroy ls_s
  33. MessageBox('演示结果', is_out)
复制代码

实机输出:
  1. K10_append=A1236.5000002026-01-022026-01-02 03:04:05
  2. K11_append_fmt=v=007
  3. K15_append_dbl_brace=6
复制代码

三条实测结论(这三条最容易让人懵):


  • append(double) 默认固定 6 位小数。6.5 进去变成 6.500000,3.14159 变成 3.141590,100 变成 100.000000。想控制位数,不要用 append 的第二个参数(见第 2 条),而是自己 String(ldb, '0.00') 后再 append(string)
  • append 的格式串对不同数值类型表现不一致


  • append(long, '{}')7append(long, '{:03d}')007 ✅ 正常;
  • append(double, '{}')6(6.5 被截断成整数!);
  • append(double, '{:.2f}') / '{:f}' / '{:.0f}'空串
  • append(数值, '000') → 原样输出 000(格式串不是 PB 掩码,别混着写);


  • append 是追加,Format 是覆盖。见 4.2。


4.2 Format:整串重新格式化
  1. uo_string ls_s
  2. long ll_num
  3. double ldb_num
  4. is_out = ''
  5. ls_s = create uo_string
  6. ldb_num = 6.5
  7. ll_num = 7
  8. //Format:先清空原内容再格式化
  9. ls_s.fromstring('')
  10. ls_s.format('{} + {} = {}', 'a', 'b', 'c')
  11. of_w('K12_format=' + ls_s.tostring())
  12. //按下标复用实参
  13. ls_s.fromstring('')
  14. ls_s.format('{0}-{1}-{0}', 'A', 'B')
  15. of_w('K13_format_idx=' + ls_s.tostring())
  16. //数值格式:实参类型必须与格式符匹配
  17. ls_s.fromstring('')
  18. ls_s.format('n={:05d}', ll_num)
  19. of_w('K14_format_int=' + ls_s.tostring())
  20. ls_s.fromstring('')
  21. ls_s.format('{:.2f}', ldb_num)
  22. of_w('K16_format_dbl_2f=' + ls_s.tostring())
  23. destroy ls_s
  24. MessageBox('演示结果', is_out)
复制代码

实机输出:
  1. K12_format=a + b = c
  2. K13_format_idx=A-B-A
  3. K14_format_int=n=00007
  4. K16_format_dbl_2f=6.50
复制代码

要点:


  • Format 会先清空原内容。实测 fromstring('pre') 之后再 format('-{}-', 'X'),结果是 -X-,前面的 pre 没了;
  • 支持 {0} {1} 下标复用同一实参;
  • double 走 Format正确的{:.2f}6.50),和 append 那个坑正好互补 —— 小数格式化请用 Format,别用 append 的第二参
  • ⚠️ 类型不匹配会直接让进程崩掉format('{:05d}', '7')(格式符要整数、实参给字符串)实测抛出 C++ 异常,进程退出码 0xE06D7363,控制台留下 fmt fail。这个既不是 PB 运行时错误,也不能被 TRY-CATCH 捕获,只能靠"实参类型一定对齐"来避免。





五、查找 / 截取 / 替换 / 插入
  1. //=========================================================
  2. // 演示:查找 / 截取 / 替换 / 插入
  3. // 要点:Pos 是 1 基、找不到返回 0;Substr 越界会崩进程,必须先判长度
  4. //=========================================================
  5. uo_string ls_s
  6. ulong lul_p             //找到的位置(1 基)
  7. string ls_tmp           //临时结果
  8. is_out = ''
  9. ls_s = create uo_string
  10. ls_s.fromstring('abcabc')
  11. lul_p = ls_s.pos('b')
  12. of_w('K20_pos=' + String(lul_p))
  13. lul_p = ls_s.pos('b', 3)
  14. of_w('K21_pos_from3=' + String(lul_p))
  15. lul_p = ls_s.pos('B', 1, true)
  16. of_w('K22_pos_ignorecase=' + String(lul_p))
  17. lul_p = ls_s.lastpos('b')
  18. of_w('K23_lastpos=' + String(lul_p))
  19. lul_p = ls_s.lastpos('b', 0)
  20. of_w('K24_lastpos_0=' + String(lul_p))
  21. lul_p = ls_s.lastpos('b', 5)
  22. of_w('K25_lastpos_5=' + String(lul_p))
  23. ls_s.fromstring('abcdefg')
  24. of_w('K26_substr_2=' + ls_s.substr(2))
  25. of_w('K27_substr_2_3=' + ls_s.substr(2, 3))
  26. //替换:实测三个重载都是「全部替换」,replaceAll 参数不起作用
  27. ls_s.fromstring('a-b-c')
  28. ls_s.replace('-', '+')
  29. of_w('K28_replace=' + ls_s.tostring())
  30. //按位置替换 = 删除 + 插入,nLen 传 0 表示什么都不做
  31. ls_s.fromstring('abcdefg')
  32. ls_s.replace(2, 3, '')
  33. of_w('K29_replace_del=' + ls_s.tostring())
  34. //InsertStr 插入单个字符是对的
  35. ls_s.fromstring('abcdefg')
  36. ls_s.insertstr(2, 'X')
  37. of_w('K30_insert_1c=' + ls_s.tostring())
  38. //插入多个字符要自己拼(InsertStr 多字符实测结果不对)
  39. ls_s.fromstring('abcdefg')
  40. ls_tmp = of_insert(ls_s.tostring(), 2, 'XY')
  41. of_w('K31_insert_safe=' + ls_tmp)
  42. destroy ls_s
  43. MessageBox('演示结果', is_out)
复制代码

配套的安全插入函数:
  1. //=========================================================
  2. // 安全插入:在 as_src 的第 al_pos 个字符前插入 as_ins(1 基)
  3. // 使用演示:of_insert('abcdefg', 2, 'XY') 返回 'aXYbcdefg'
  4. //=========================================================
  5. uo_string ls_s
  6. string ls_left
  7. string ls_right
  8. ls_s = create uo_string
  9. ls_s.fromstring(as_src)
  10. if al_pos <= 1 then
  11.         ls_left = ''
  12.         ls_right = ls_s.substr(1)
  13. else
  14.         ls_left = ls_s.substr(1, al_pos - 1)
  15.         ls_right = ls_s.substr(al_pos)
  16. end if
  17. destroy ls_s
  18. return ls_left + as_ins + ls_right
复制代码

实机输出:
  1. K20_pos=2
  2. K21_pos_from3=5
  3. K22_pos_ignorecase=2
  4. K23_lastpos=5
  5. K24_lastpos_0=5
  6. K25_lastpos_5=0
  7. K26_substr_2=bcdefg
  8. K27_substr_2_3=bcd
  9. K28_replace=a+b+c
  10. K29_replace_del=aefg
  11. K30_insert_1c=aXbcdefg
  12. K31_insert_safe=aXYbcdefg
复制代码

四条实测坑,一条比一条狠


  • LastPos 的带参重载基本不可用。实测 'abcabc'


  • lastpos('b') = 5 ✅
  • lastpos('b', 0) = 5 ✅(第二参传 0 等价于单参)
  • lastpos('b', 5) / lastpos('b', 6) / lastpos('b', 2) / lastpos('b', 100) = 全是 0
  • lastpos('B', 6, true)(忽略大小写)= 0


   → 结论:只用 LastPos(str) 单参版本,或者第二参写 0。

  • ReplacereplaceAll 参数不起作用,永远是全部替换'a-b-c' 三个重载(2 参 / false / true)结果都是 a+b+c'aaa'.replace('a','b',false)bbb。想只替换第一处,只能自己 Pos + 按位置 Replace
  • InsertStr 插入多字符结果不对。实测 'abcdefg'insertstr(2,'X')aXbcdefg ✅(单字符正确);insertstr(2,'XY')aXcbcdefg ❌(期望 aXYbcdefg);insertstr(4,'XY')abcXedefg ❌;insertstr(7,'END')abcdefEN ❌。结论:InsertStr 只用来插单个字符,插多字符请用上面的 of_insert() 或者 Replace 按位置替换
  • ⚠️ Substr 越界直接崩进程substr(0) 返回空串没事,但 substr(99)(超出长度)实测抛出 C++ 异常、进程退出码 0xE06D7363。而同样越界的 Pos 只是返回 0。Substr 之前一定要先判断 length()


另外两个边界:Replace(nStart, 0, str) 什么也不做(不能当插入用);Replace(6, 99, '.') 会把从第 6 个字符到末尾全部换掉 → abcde.




六、正则表达式:uo_string 最大的价值

PowerBuilder 原生只有 Match(s, pattern) 做"是否包含"判断,没有分组、没有位置、没有替换。uo_string 的三个方法补上了这一块。
  1. //=========================================================
  2. // 演示:正则表达式(PB 原生没有正则,这是 uo_string 最大的价值)
  3. // 要点:RegexMatch 是「全串匹配」,RegexSearch 才是「子串搜索」
  4. //=========================================================
  5. uo_string ls_s
  6. string lsa_items[]      //匹配结果:第 1 项是整串,后面依次是各捕获组
  7. long lla_pos[]          //各结果在原串中的起始位置(1 基)
  8. long lla_len[]          //各结果长度
  9. long ll_ub              //结果数组上界
  10. is_out = ''
  11. ls_s = create uo_string
  12. //全串匹配
  13. ls_s.fromstring('13800138000')
  14. of_w('K40_match_phone=' + String(ls_s.regexmatch('1[3-9][0-9]{9}')))
  15. ls_s.fromstring('tel:13800138000;')
  16. of_w('K41_match_inside=' + String(ls_s.regexmatch('1[3-9][0-9]{9}')))
  17. of_w('K42_search_inside=' + String(ls_s.regexsearch(1, '1[3-9][0-9]{9}')))
  18. //从指定位置开始搜索(1 基)
  19. ls_s.fromstring('a1b22c333')
  20. of_w('K43_search_from1=' + String(ls_s.regexsearch(1, '22')))
  21. of_w('K44_search_from5=' + String(ls_s.regexsearch(5, '22')))
  22. //带捕获组:items[1]=整串,items[2..]=各分组
  23. ls_s.regexsearch(1, '([a-z])([0-9]+)', lsa_items, lla_pos, lla_len)
  24. ll_ub = UpperBound(lsa_items)
  25. of_w('K45_items_ub=' + String(ll_ub))
  26. of_w('K46_item1=' + lsa_items[1])
  27. of_w('K47_item2=' + lsa_items[2])
  28. of_w('K48_pos1=' + String(lla_pos[1]))
  29. //替换:默认替换所有匹配
  30. ls_s.fromstring('a1b22c333')
  31. of_w('K49_regexreplace=' + ls_s.regexreplace('[0-9]+', '#'))
  32. ls_s.fromstring('2026-09-22')
  33. of_w('K50_regexreplace_group=' + ls_s.regexreplace('(\d{4})-(\d{2})-(\d{2})', '$2/$3/$1'))
  34. ls_s.fromstring('abc')
  35. of_w('K51_regexreplace_none=' + ls_s.regexreplace('[0-9]', '#'))
  36. destroy ls_s
  37. MessageBox('演示结果', is_out)
复制代码

实机输出:
  1. K40_match_phone=true
  2. K41_match_inside=false
  3. K42_search_inside=true
  4. K43_search_from1=true
  5. K44_search_from5=false
  6. K45_items_ub=3
  7. K46_item1=a1
  8. K47_item2=a
  9. K48_pos1=1
  10. K49_regexreplace=a#b#c#
  11. K50_regexreplace_group=09/22/2026
  12. K51_regexreplace_none=abc
复制代码

必须分清的三件事:


  • RegexMatch 是全串匹配,RegexSearch 是子串搜索


  • '13800138000'.RegexMatch('1[3-9][0-9]{9}')true
  • 'tel:13800138000;'.RegexMatch('1[3-9][0-9]{9}')false(前后多了字符)
  • 'tel:13800138000;'.RegexSearch(1, '1[3-9][0-9]{9}')true


   做校验用 RegexMatch,做提取用 RegexSearch 别搞反——用 RegexSearch 做校验会让 " tel:13800138000;" 这种脏数据混过关。

  • RegexSearch 的第一个参数是 1 基起始位置,从该位置向后搜。'a1b22c333'22 在第 4~5 位:regexsearch(1,'22') = true,regexsearch(5,'22') = false(从第 5 位起只剩 2c333,凑不出 22)。
  • 捕获组数组 items[] 的第 1 项是"整串",第 2 项起才是各分组'([a-z])([0-9]+)' 匹配 'a1b22c333' 得到 3 项:a1(整串)、a(组 1)、1(组 2);pos[] 同步给出 1 / 1 / 2(1 基位置),len[] 给出各自长度。


RegexReplace 的注意点:


  • 默认替换所有匹配(没有"只替换第一处"的开关):'a1b22c333''a#b#c#'
  • 支持 $1 $2 $3 反向引用:'2026-09-22'(\d{4})-(\d{2})-(\d{2}) 换成 $2/$3/$109/22/2026
  • 无匹配时原样返回'abc''abc'
  • PB 字符串里反斜杠不是转义符,\d 直接写就行,不用写成 \\d





七、按行读取与拆分
  1. //=========================================================
  2. // 演示:按行读取与拆分
  3. // 要点:GetLine 越界返回最后一行;Split 的分隔符是「字符集合」不是子串
  4. //=========================================================
  5. uo_string ls_s
  6. string lsa_parts[]      //拆分结果
  7. long ll_n               //拆分出的项数
  8. long ll_i               //循环变量
  9. string ls_show          //拼接后的展示串
  10. is_out = ''
  11. ls_s = create uo_string
  12. ls_s.fromstring('L1~r~nL2~r~nL3')
  13. of_w('K60_linecount=' + String(ls_s.getlinecount()))
  14. of_w('K61_line2=' + ls_s.getline(2))
  15. of_w('K62_line4=' + ls_s.getline(4))
  16. ls_s.fromstring('a,b,,c')
  17. ll_n = ls_s.split(',', lsa_parts)
  18. of_w('K63_split_n=' + String(ll_n))
  19. //多字符分隔符被当成字符集合:'XX' 会在每个 X 处切一刀
  20. ls_s.fromstring('aXXbXXc')
  21. ll_n = ls_s.split('XX', lsa_parts)
  22. ls_show = ''
  23. for ll_i = 1 to UpperBound(lsa_parts)
  24.         ls_show = ls_show + '[' + lsa_parts[ll_i] + ']'
  25. next
  26. of_w('K64_split_xx=' + String(ll_n) + ls_show)
  27. destroy ls_s
  28. MessageBox('演示结果', is_out)
复制代码

实机输出:
  1. K60_linecount=3
  2. K61_line2=L2
  3. K62_line4=L3
  4. K63_split_n=4
  5. K64_split_xx=5[a][][b][][c]
复制代码

两个反直觉的地方:


  • GetLine 越界不报错,返回最后一行。3 行文本 GetLine(4) 得到 L3。循环取行时一定要用 GetLineCount() 做上界,否则会把最后一行重复处理一遍;
  • ⚠️ Split 的分隔符是"字符集合",不是"子串"'aXXbXXc''XX' 拆,期望 3 项(a、b、c),实测得到 5 项(a、空、b、空、c)—— 它在每个 X 处都切了一刀。想按多字符子串拆分,得自己先用 Replace 把子串换成一个单字符哨兵(比如 ~t),再 Split。(上面代码块里输出的方括号是实测原样,一个方括号对表示一个拆分项,中间空的一对表示空项。)


另外 SplitignoreNull 参数有个小陷阱:'a,b,,c'true 时返回值变成 3,但数组本身不会缩容UpperBound 仍是 4,第 4 项是上一次的残留 'c')。取数据请以返回值为准、用 for i = 1 to 返回值,不要用 UpperBound




八、中文专属能力:全角半角、拼音、简繁、金额大写
  1. //=========================================================
  2. // 演示:中文专属处理
  3. //=========================================================
  4. uo_string ls_s
  5. long ll_money           //金额整数示例
  6. double ldb_money        //金额小数示例
  7. is_out = ''
  8. ls_s = create uo_string
  9. of_w('K70_tofull=' + ls_s.tofull('abc123'))
  10. of_w('K71_tohalf=' + ls_s.tohalf('ABC123'))
  11. //拼音取的是首字母
  12. of_w('K72_pinyin=' + ls_s.pinyin('重庆'))
  13. //简繁互转
  14. of_w('K73_big5_back=' + ls_s.big52gbk(ls_s.gbk2big5('中文')))
  15. //人民币大写:支持 long / double / decimal / string 四种重载
  16. ll_money = 1234
  17. of_w('K74_money_long=' + ls_s.chinesemoney(ll_money))
  18. ldb_money = 1234.56
  19. of_w('K75_money_dbl=' + ls_s.chinesemoney(ldb_money))
  20. of_w('K76_money_str=' + ls_s.chinesemoney('100000000'))
  21. of_w('K77_money_zero=' + ls_s.chinesemoney('0.05'))
  22. of_w('K78_money_bad=' + ls_s.chinesemoney('abc'))
  23. //随机流水号:flag 是前缀,后面接指定位数的随机数字
  24. of_w('K79_serial_len=' + String(Len(ls_s.getrandserial('A', 8))))
  25. //比较:返回「逐位不同的字符个数」,0 表示相同
  26. of_w('K80_cmp_same=' + String(ls_s.compare('abc', 'abc')))
  27. of_w('K81_cmp_case=' + String(ls_s.compare('abc', 'ABC')))
  28. of_w('K82_cmp_ignum=' + String(ls_s.compare('a1b', 'a2b', 0, false, true, false, false, false)))
  29. destroy ls_s
  30. MessageBox('演示结果', is_out)
复制代码

实机输出:
  1. K70_tofull=abc123
  2. K71_tohalf=ABC123
  3. K72_pinyin=ZQ
  4. K73_big5_back=中文
  5. K74_money_long=壹仟贰佰叁拾肆元整
  6. K75_money_dbl=壹仟贰佰叁拾肆元伍角陆分
  7. K76_money_str=壹亿元整
  8. K77_money_zero=零元零角伍分
  9. K78_money_bad=零元整
  10. K79_serial_len=9
  11. K80_cmp_same=0
  12. K81_cmp_case=3
  13. K82_cmp_ignum=0
复制代码

逐条说明:


  • toFull / toHalfabc123abc123ABC123ABC123中文()中文()。做单据录入、开票接口(很多税控接口只收半角)非常实用;
  • PinYin 返回的是拼音首字母(大写),不是全拼:重庆ZQ银行YX。做简码 / 助记码字段正好;
  • GBK2BIG5 / BIG52GBK 是可逆的big52gbk(gbk2big5('中文')) == '中文'
  • ChineseMoney 五个重载long / int / double / decimal / string),实测:
  • 1234(long)→ 壹仟贰佰叁拾肆元整
  • 1234.56(double)→ 壹仟贰佰叁拾肆元伍角陆分
  • '100000000'壹亿元整
  • '0.05'零元零角伍分
  • 'abc'(非法)→ 零元整不报错,所以入参合法性要自己先校验
  • ⚠️ 传负数会崩进程! chinesemoney('-12.34') 实测直接 C++ 异常退出(同 Substr 越界)。金额转大写之前必须先判正负
  • GetRandSerial(flag, numLen)flag前缀,会原样拼在随机数字前面。getrandserial('A', 8) 得到 A + 8 位数字,长度 9getrandserial('N', 6)N686815flag 传空串时返回的就是纯数字;
  • Compare 返回"逐位不同的字符个数",0 表示相同:'abc' vs 'ABC' = 3,'abc' vs 'abcd' = 1。后面五个 ignore 开关实测只有 ignoreNumber(忽略数字)与 ignoreCaseSensitive(忽略大小写)能稳定生效;ignoreSpace / ignoreSymbol 在两串长度不同时仍会算出差异,不要指望它做"忽略空格后相等"这种判断,需要的话自己先 toHalf + 去掉空格再比。





九、NULL 与空值:IsNull 的语义很反直觉

这一节单独拎出来,因为它和 PowerScript 原生的 NULL 规则完全不是一回事

实测状态矩阵(同一个 uo_string 对象依次操作):

操作IsNull()
createfalse
刚 create → fromstring('')false
'a'fromstring('')false
'a'Clear()false
'a'SetNull()true
SetNull()fromstring('')true(写空串不会复位!)
SetNull()fromstring('b')false(写非空内容才复位)
SetNull()'b'Clear()false


结论:


  • IsNull() 不是"内容为空吗",而是"是否处于 NULL 标记状态";
  • 只有 SetNull() 能把它置成 true;只有写入非空内容能把它复位成 falseFromString('')Clear() 都不复位;
  • ⚠️ 判断"有没有内容"请用 Len(ls_s.tostring()) = 0,不要依赖 IsNull()


顺带一提:Resize(6) 之后 length() 会变成 6,但 tostring() 仍只返回 'abc'(尾部是 \0 被截断);Resize(1) 会把 'abc' 截成 'a'Resize 只改缓冲区长度,不补内容,一般业务用不到。




十、编码转换与文本文件读写
  1. //=========================================================
  2. // 演示:编码转换与文本文件读写
  3. // 要点:writefile 第二参 true 才写 UTF-8(带 BOM),默认按本地编码(GBK)
  4. //=========================================================
  5. uo_string ls_s
  6. uo_string ls_r
  7. blob lb_u8              //UTF-8 字节流
  8. boolean lb_ok           //操作是否成功
  9. is_out = ''
  10. ls_s = create uo_string
  11. ls_r = create uo_string
  12. ls_s.fromstring('中A')
  13. lb_u8 = ls_s.toutf8()
  14. of_w('K90_utf8_len=' + String(Len(lb_u8)))
  15. of_w('K91_ansi_len=' + String(Len(ls_s.toansi())))
  16. of_w('K92_utf16_len=' + String(Len(ls_s.toutf16())))
  17. of_w('K93_fromutf8=' + ls_s.fromutf8(lb_u8))
  18. ls_s.fromstring('中文内容ABC')
  19. lb_ok = ls_s.writefile('demo_u8.txt', true)
  20. of_w('K94_write_u8=' + String(lb_ok))
  21. lb_ok = ls_r.readfile('demo_u8.txt')
  22. of_w('K95_read_u8=' + String(lb_ok) + '/' + ls_r.tostring())
  23. lb_ok = ls_r.readfile('no_such_file.txt')
  24. of_w('K96_read_missing=' + String(lb_ok))
  25. destroy ls_r
  26. destroy ls_s
  27. MessageBox('演示结果', is_out)
复制代码

实机输出:
  1. K90_utf8_len=4
  2. K91_ansi_len=3
  3. K92_utf16_len=4
  4. K93_fromutf8=中A
  5. K94_write_u8=true
  6. K95_read_u8=true/中文内容ABC
  7. K96_read_missing=false
复制代码

要点:


  • '中A' 三种编码的字节数:UTF-8 = 4(3+1),ANSI/GBK = 3(2+1),UTF-16 = 4(2+2)。这组数字可以直接拿来验证你手上的编码对不对;
  • ToAnsi() 会被编译器标记为「已过时」(警告 C0185PB 10 和 PB 12.5 都会报,只是警告,不影响运行)。新代码统一走 ToUtf8() / FromUtf8()
  • ⚠️ 别和原生的 Blob(s) 混用:PowerScript 的 Blob('中A') 默认按 UTF-16LE 编码(4 字节),而 uo_string.ToUtf8() 是 UTF-8(4 字节,凑巧一样)。换成 '中' 就露馅了(UTF-8 是 3 字节,UTF-16 是 2 字节)。跨系统传数据,一律显式 ToUtf8(),不要裸用 Blob()
  • WriteFile(name) 默认写本地编码(GBK,无 BOM),实测 14 字节;WriteFile(name, true) 写 UTF-8 并带 BOM(EF BB BF,实测 23 字节。给外部系统(Java / Python / 网页)的文件一律用 true
  • ReadFile 能正确识别自己写的 UTF-8 带 BOM 文件;文件不存在时返回 false,内容为空。返回值一定要判。





十一、PB12.5 差异

PB 12.5 与 PB 10 行为一致,没有需要分叉的写法。

同一份示例对象源码、同一份测试脚本(66 条硬断言),在两版各跑一遍:

验证项PB 10PB 12.5
对象源码导入2 个对象,0 错2 个对象,0 错
全量重建(Full Rebuild)0 错0 错
无界面断言运行PASS 66/66PASS 66/66
运行输出比对1409 字节1409 字节,逐字节相同
语法与 API 静态核查0 错 0 提醒0 错 0 提醒
源码编码核查(GBK / CRLF / 无 BOM)全部通过全部通过


两版共同出现的编译期提示只有一条:ToAnsi() 被标记为「已过时、将来会移除」(编译警告 C0185)。

⚠️ 这条警告在 PB 10 和 PB 12.5 下都会出现,不是 PB 12.5 特有的差异。 我们最初按直觉把它记成了「新版才报」,两版实机对照后已订正 —— 这类"看起来像版本差异、其实是两版都报"的判断,只有实跑一次才不会写错

应对很简单:新代码统一用 ToUtf8() / FromUtf8(),把 ToAnsi() 留给老代码兼容。

结论:代码按 PB 10 基准写,即可在 PB 12.5 直接复用。




十二、实机验证情况(如实记录)

验证时间:2026-09-22,用当次最新的 PBIDEA 构建全程复跑。

PBIDEA 在同一套功能下重新构建过(核心运行库从 18,020,864 字节变为 18,021,888 字节,导出数仍为 2545、导出名逐项完全一致)。按「所有功能结果只有实测后才发布」的要求,本文没有沿用旧快照的数字,而是拿新版把全部验证重跑了一遍 —— 两版输出与上一版快照逐字节相同

环节做法结果
① 导入把示例对象与 PBIDEA 的 uo_string 源码导入工程2 个对象,0 错
② 全量编译工程画板 Full Rebuild(PB 10 / PB 12.5 各一次)0 错
③ 运行无界面 PBVM 跑 66 条硬断言两版均 PASS 66/66
④ 输出比对两版运行结果逐字节比较1409 字节,完全相同
⑤ 静态核查语法/API 核查 + 源码编码核查0 错 0 提醒;GBK + CRLF + 无 BOM 全通过
⑥ 性能基准(见 14.1 / 14.2)7 种写法 × 4 种规模 × 每格取 3 次最快值,PB 10 / PB 12.5 各跑一整轮两版均 PASS 43/43;各写法产出内容长度逐一致



  • 示例对象:nvo_str_demoof_demo_create / of_demo_append / of_demo_find / of_demo_regex / of_demo_split / of_demo_cn / of_demo_code 七个演示函数 + of_insert 安全插入函数);
  • 性能基准工程:nvo_perf_demo(7 个基准函数 + 1 个计时器语义判别函数),与上面的功能验证是两个独立工程,互不干扰。两个工程连同各自的测试脚本已一起打进 PB10 兼容交付包(见下方回复里的附件),你可以自己跑一遍复核 14.1 的数字;
  • 依赖库用副本载入,避免验证过程就地改写原库;
  • 本机实机验证环境:Windows 10;PowerBuilder 10 与 12.5 各一套运行时。这里要说清楚:Windows 10 + PB 10/12.5 只是本次的验证环境,不是 PBIDEA 的支持范围上限 —— PBIDEA 本体支持 Windows XP 及以上PowerBuilder 8 及以上,本文没有 WinXP + PB 8 的实机条件,因此对这两个平台只作支持范围声明,不作实测结论
  • 未实测 / 仅静态核对(本文不作为核心示例):SetRegexOption 的两个参数取值、free()Clear() 的内部差异、GetAddress() 的用途。


发布纪律:本文出现的每一个功能结论都对应上表某一次真机运行;凡未曾实测的,文内一律显式标注「未实测」,且不作为核心示例。第十四节那行性能判断曾以推断形式写进初稿,现已按实测数据订正(14.1 表中原始结论被实测推翻)。




十三、坑清单(14 条,按踩中概率排序)

#现象规避
1Substr 越界进程 C++ 异常崩溃(0xE06D7363)先判 length()
2Format 类型不匹配进程崩溃,控制台 fmt fail实参类型与格式符严格对齐
3ChineseMoney 传负数进程崩溃先判正负,负数自己加"负"字
4LastPos 带起始位置除 0 外一律返回 0只用单参或第二参写 0
5ReplacereplaceAll无效,永远全替换只替换首处就自己算位置
6InsertStr 插多字符结果错乱of_insert()Replace
7Split 的分隔符是字符集合不是子串先换成单字符哨兵
8GetLine 越界返回最后一行GetLineCount() 做上界
9SplitignoreNull返回值变小但数组不缩容用返回值做循环上界
10IsNull() 语义写空串不复位 NULL 标记判空用 Len(tostring())=0
11append(double)固定 6 位小数;'{}' 截断成整数;'{:.2f}' 变空串小数一律走 Format
12Format 会清内容原内容被覆盖需要保留就先 FromString(原串 + 格式串)
13ToAnsi()编译器报 C0185 过时(PB 10 与 PB 12.5 都报改用 ToUtf8()
14destroystringDestroy() 不执行配对 create/destroy





十四、什么时候用 uo_string,什么时候用原生函数

场景推荐
简单拼接、取长度、取子串原生 + / Len / Mid,更轻、无依赖
需要正则(校验 / 提取 / 批量替换)uo_string
需要按分隔符拆数组、按行读uo_string
中文:拼音、简繁、全半角、金额大写uo_string(原生完全没有)
编码转换(UTF-8 / UTF-16 / ANSI)uo_string,但注意别和 Blob() 混用
高频循环里做字符串累加(上万次)实测反而是 uo_string 更快(快 4~5 倍) —— 本节初稿这里曾写"原生拼接更快",已被实测推翻;但两种写法都随次数平方变慢,上万次以上更该用分块累积(见 14.1、14.2)
只想知道"含不含某子串"原生 Pos() 就够,没必要 create 一个对象


14.1 实测:到底谁快(本文唯一一处推翻了自己初稿的结论)

本节表格最后一行,初稿原本写的是"原生 + 或 PB 的 String 拼接更快;uo_string 每次调用都要跨 DLL 边界"。这是推断,不是实测 —— 而实测结果是反的。

下面是真机数据。同一台机器、同一个循环体、同一份元素内容(都追加 String(i)),每格取 3 次里最快的一次;PB 10 与 PB 12.5 双轨各跑一遍。关键前提:几种写法产出的内容长度逐一致(断言全绿),所以比的是同一个活儿,不是"少干了活所以快"。

追加次数原生 ls = ls + String(i)原生"先入数组再统一拼接"uo_string 赋值写法原生分块累积(每 100 片合一)
10000578 / 562 ms578 / 547 ms156 / 172 ms62 / 62 ms
300004718 / 4485 ms4640 / 4437 ms1032 / 1156 ms218 / 218 ms
5000012922 / 12485 ms13234 / 12406 ms2688 / 3188 ms422 / 406 ms


(每格两个数字依次是 PB 10 / PB 12.5,两版差异都在 10% 以内,结论方向一致。)

四条结论:


  • 原生 + 是最慢的,不是最快的。 跨一次 DLL 边界的开销,远小于原生 + 每追加一次就复制整个累积串的开销 —— 所以"要跨 DLL 边界所以更慢"这个理由站不住。
  • uo_string 确实更快,但没有官方注释说的那么夸张。 官方源码在 append 上方写着"效率提高了50倍",实测在 5 万次量级是 4~5 倍。它是有效优化,但别按 50 倍去做容量规划。
  • 两种写法都是接近平方的复杂度。 追加次数 ×5(1 万 → 5 万),原生耗时 ×22、uo_string ×17~26 ——uo_string 只是把常数压小了,并没有把曲线掰直。
  • "先入数组再拼接"这个流传的优化基本无效(耗时与直接边算边拼几乎相同)。因为它最后还是每次复制整个累积串,那笔账绕不掉。


14.2 真正有效的一招:分块累积

既然两种写法都在反复复制整个累积串,那就少复制几次 —— 先把片段攒够一批,再一次性并进主串:
  1. string ls_buf, ls_part
  2. long   ll_i
  3. ls_buf  = ''
  4. ls_part = ''
  5. for ll_i = 1 to 50000
  6.     ls_part = ls_part + String(ll_i)      //内层只攒 100 个片段,代价很小
  7.     if Mod(ll_i, 100) = 0 then
  8.         ls_buf = ls_buf + ls_part         //每 100 个才并进主串一次
  9.         ls_part = ''
  10.     end if
  11. next
  12. ls_buf = ls_buf + ls_part                 //收尾:别漏掉最后不足 100 的那批
复制代码

实测把 5 万次追加从 12922 ms 压到 422 ms(快 30 倍),复杂度指数也从 1.93 降到 1.19(接近线性)。它比 uo_string 里最快的写法还快 6~8 倍,而且不需要任何对象、任何外部库 —— 多出来的成本只是一个临时变量和一次取模判断。

(复杂度指数=把耗时与追加次数做 log-log 拟合得到的斜率:1 是线性,2 是平方。)

所以结论要分开说:uo_string 的价值在正则、拆分、中文处理、编码转换 —— 那些原生做不到的事;"攒长文本"这件事,交给分块循环最划算。
顺带钉一个计时坑:测性能别用内建 CPU()。实测它在一整段运行里与 GetTickCount() 读数逐条完全相等,连 Sleep 等待期间都照走 —— 说明它并不是文档说的"进程 CPU 时间",不能用来排除等待开销。另外内建 Sleep 的参数单位是Sleep(0.1) 会被截断成 0(等于没睡);要睡 100 毫秒,得把 kernel32 的 Sleep 起个别名挂进来。

一个实用组合:用 uo_string 做"脏活"(正则清洗、编码转换、中文处理),转出来的结果再用原生 PowerScript 拼装入库。两者不冲突,别为了一个对象把所有 + 都换掉。




十五、扩展点


  • uo_json 搭配uo_string.RegexReplace 清洗过的文本 → uo_json.Parse 解析,比自己写解析器省事;
  • uo_csv 搭配uo_csv.toJson() 对含逗号字段会串列(见本系列 Day41),可以先用 uo_string.Split + 手工修复;
  • uo_httpclient 搭配:接口返回的 blob → uo_string.FromUTF8(blob)RegexSearch 提取字段,这是最轻量的"没有 JSON 解析库也能取数"的方案;
  • 封装成自己的 nvo_textutil:把本文的 of_insert()、安全 Substr、金额大写前的正负判断、UTF-8 文件读写都包一层,业务代码里就不再需要直面那 14 条坑。





小结

uo_string 是 PBIDEA 里"看着不起眼、用上就回不去"的对象。三句话总结:


  • 正则是它最大的价值 —— RegexMatch 全串校验、RegexSearch 子串提取、RegexReplace 批量替换,PB 原生一样都没有;
  • 中文能力(拼音首字母、简繁、全半角、人民币大写)是独一份,财务与票据类系统直接省掉几百行代码;
  • 它有脾气 —— Substr 越界、Format 类型不匹配、ChineseMoney 负数都会直接崩进程,LastPos 带参、ReplacereplaceAllInsertStr 多字符三个都是"看起来能用、实测不对"。照着第十三节的坑清单写代码,比事后排查省事得多。
  • 性能上别凭直觉 —— 实测原生 + 反而最慢,uo_string 快 4~5 倍但同样是平方复杂度;真要攒几万段文本,分块累积快 30 倍(见 14.2)。


本文全部代码与 66 条功能断言、43 条性能基准断言在 PB 10 与 PB 12.5 双轨跑通,输出逐字节一致。
共享共进共赢
Sharing And Win-win Results
SYBASEBBS - 免责申明1、欢迎访问“SYBASEBBS.COM”,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@sybasebbs.com
pbai 楼主

主题

0

回帖

2523

积分

PBAI

积分
2523
贡献
在线时间
小时
昨天 07:00 | 显示全部楼层
本帖最后由 pbai 于 2026-9-22 17:24 编辑

本文示例代码的 PB10 兼容版 PBL 已打包,见附件。包内含 两个工程:功能演示(nvo_str_demo)与性能基准(nvo_perf_demo,即第 14.1 / 14.2 节那组实测数字的复现脚本),你可以自己跑一遍核对。

uostring10_pb10.zip (49.28 KB, 下载次数: 0)

【环境要求】

  • 本包 PBL 为 PB 10 格式,可直接用于 PB 10 / 10.5 / 11.x / 12.x;PBIDEA 本体支持 PowerBuilder 8 及以上(PB8/9 请用配套的 PB8/9 格式运行库)
  • 实测 PB10 与 PB12.5 两边输出逐字节一致
  • 必须部署 PbIdea.dll(与 EXE 同目录或 PATH 可达);不需要 Office / Excel / 数据库驱动 / .NET
  • 操作系统:Windows XP 及以上(XP / 7 / 10 / 11)
  • 源码 GBK + CRLF + 无 BOM,PB IDE 直接 Import 不乱码


【操作细节步骤】

  • 把 PbIdea.dll 复制到工程目录(或最终 EXE 所在目录)
  • Library 画板 → 选中目标 PBL → 右键 Import → uo_string.sru → nvo_str_demo.sru → 最后 nvo_perf_demo.sru(后两个都引用 uo_string,顺序不能反)
  • 打开工程画板确认目标 PBL 在库列表中,执行 Full Build 全量重建
  • 看到 0 Errors 即完成(可能有一条 C0185「ToAnsi 已过时」警告,PB 10 与 PB 12.5 都会出现,属正常,不影响运行)
  • 功能演示:create nvo_str_demo → ls_result = lnv.of_run() → destroy lnv → MessageBox('演示结果', ls_result)
  • 性能基准:create nvo_perf_demo → ls_report = lnv.of_run() → destroy lnv → MessageBox('性能基准', ls_report) (整套跑完约 40 秒,慢的写法本身一次就要十几秒)


【数据库】
本文示例 不依赖数据库,纯字符串运算 + 本地文本文件读写(of_demo_code 会在当前目录写一个 demo_u8.txt,可删)。无建表 SQL。

【包内文件】

  • src/uo_string.sru —— PBIDEA 随版发布的官方对象源码
  • src/nvo_str_demo.sru —— 功能示例对象(7 个演示函数 + 安全插入函数 of_insert)
  • src/nvo_perf_demo.sru —— 性能基准对象(7 种累加写法 + 1 个计时器语义判别函数)
  • tests/strdemo.pbtest —— 66 条功能硬断言测试脚本
  • tests/perf.pbtest —— 43 条性能基准硬断言测试脚本
  • evidence/diag_100.txt、evidence/diag_125.txt —— 功能测试两版实跑输出
  • evidence/diag_perf_100.txt、evidence/diag_perf_125.txt —— 性能基准两版实跑输出(含全部计时读数)
  • 使用说明.txt —— 完整部署与排错说明


【实机验证情况】

  • [PASS] PB10 --pb-version 100 全量编译 0 错误
  • [PASS] PB10 pbl list 对象齐全(nvo_str_demo、nvo_perf_demo、uo_string)
  • [PASS] PB10 PBVM 运行 strdemo.pbtest PASS 66/66
  • [PASS] PB10 PBVM 运行 perf.pbtest PASS 43/43
  • [PASS] PB12.5 --pb-version 125 全量编译 0 错误
  • [PASS] PB12.5 PBVM 运行 strdemo.pbtest PASS 66/66
  • [PASS] PB12.5 PBVM 运行 perf.pbtest PASS 43/43
  • [PASS] 功能测试两版输出逐字节比对:1409 字节完全一致 → 无版本差异
  • [PASS] 性能基准:7 种写法 × 4 种规模,各写法产出的内容长度逐一致(比的是同一个活儿,不是&quot;少干了活所以快&quot;)
  • [PASS] 已用 2026-09-22 当次最新的 PBIDEA 构建复跑全部验证
  • [PASS] 语法与 API 静态核查 0 错 0 提醒;源码编码核查(GBK / CRLF / 无 BOM)全部通过


【性能基准实测读数】单位 ms,每格两个数字依次为 PB10 / PB12.5
  1. 写法              n=10000      n=30000      n=50000
  2. native_plus       578 / 562    4718 / 4485  12922 / 12485
  3. native_arr        578 / 547    4640 / 4437  13234 / 12406
  4. native_chunk       62 /  62     218 /  218     422 /  406
  5. uo_append_str     250 / 281    1875 / 2109   5015 / 5718
  6. uo_append_long    187 / 218    1688 / 1969   4765 / 5437
  7. uo_append_assign  156 / 172    1032 / 1156   2688 / 3188
  8. uo_append_fmt     203 / 218    1703 / 1969   4719 / 5703
复制代码

四条结论:

  • 原生 + 是最慢的 —— &quot;uo_string 要跨 DLL 边界所以更慢&quot;这个推断站不住
  • uo_string 确实更快,但没那么夸张:官方源码注释写&quot;效率提高了 50 倍&quot;,实测 5 万次量级是 4~5 倍
  • 两种写法都接近平方复杂度(指数 1.77~2.03),uo_string 只压常数、没把曲线掰直
  • &quot;先入数组再统一拼接&quot;这条流传的优化基本无效(0.98~1.03 倍)

真正有效的是分块累积(先把 100 个片段攒着、再并进主串一次):5 万次从 12922 ms 压到 422 ms(快 30 倍),复杂度指数从 1.93 降到 1.19(接近线性),而且不需要任何对象、任何外部库。详见文章 14.1 / 14.2。

未实测、仅静态核对(本文不作核心示例):SetRegexOption 参数取值、free() 与 Clear() 内部差异、GetAddress() 用途,以及源码 private 段的 escape/unescape/crlf/lf/Printf/OpenConsoleWindow。

★三个会直接崩进程的坑,照着避:Substr 越界Format 格式符与实参类型不匹配ChineseMoney 传负数(C++ 异常,退出码 0xE06D7363,TRY-CATCH 兜不住)。

★计时别用内建 CPU():实测它与 GetTickCount() 读数逐条完全相等(本次 perf.pbtest 里 T_cpucheck 两项都读到 110 ms,连外部睡 100 ms 期间也照走),它并不是文档说的&quot;进程 CPU 时间&quot;;内建 Sleep 的参数单位是&quot;秒&quot;,Sleep(0.1) 会被截断成 0(等于没睡),要睡毫秒得挂 kernel32 的 Sleep 别名(nvo_perf_demo 里已给出写法)。

(本附件已于 2026-09-22 更新两次:先去除说明文档里的本机路径并随当次最新 PBIDEA 构建复测,再并入性能基准工程与实际计时读数;两次更新后功能测试输出与上一版逐字节相同。)
共享共进共赢
Sharing And Win-win Results
您需要登录后才可以回帖 登录 | 站点注册

本版积分规则

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

Mail To:Admin@SybaseBbs.com

客服微信:18669893686
挂谷猜想 · 探索

QQ|Archiver|PowerBuilder(PB)BBS社区 ( 鲁ICP备2021027222号-1 )

GMT+8, 2026-9-23 07:35 , Processed in 0.033531 second(s), 10 queries , MemCached On.

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表