祝愿大家身体健康!

 找回密码
 站点注册
查看: 591|回复: 1

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

[复制链接]
发表于 2026-9-22 06:55:12 | 显示全部楼层 |阅读模式

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

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

×
本帖最后由 pbai 于 2026-9-23 10:12 编辑

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 双轨真机跑出来的(功能 71 条硬断言 + 性能基准 43 条硬断言 + 版本兼容探针 59 条读数,全部命中),凡是没实测过的功能,一律不写进正文;文中标注的"坑"都是真机踩出来的,不是推断。第十四节的性能判断曾在初稿里以推断形式写出,本文已按实测数据订正(见 14.1)
【版本声明】
- 本文以 PBIDEA 当前最新版本为准(版本源由维护方统一核验),并对照随版发布的源码整理。
- 核验日期:2026-09-23;当次核验的 PBIDEA 核心运行库:18,017,792 字节 / 2544 个导出,md5 048e3e92d4a4…。
- 版本识别口径:逐项解析 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_json、uo_csv、uo_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() → 3;GetLine(2) → L2
'重庆' 取拼音简码实测范围内没有等价函数PinYin('重庆') → ZQ
1234.56 转人民币大写实测范围内没有等价函数(财务系统常年自己写几百行)ChineseMoney(1234.56) → 壹仟贰佰叁拾肆元伍角陆分
取子串Mid('abcdefg', 2, 3) → bcdSubstr(2, 3) → bcd(等价;但 uo_string 的 Substr 越界会崩进程,用前必须判 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.5;String(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', '*') → false、Match('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 格式串追加。double 也是支持的,但这条重载是 2026-09-23 版才补上的,旧版传小数会被截成整数(见 4.1)
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[, bIgnoreCase])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(UTF16BE)blob编码导出(UTF16BE 为可选布尔参数,省略即小端)
FromUtf16(blob[, UTF16BE]) / FromUTF8(blob) / ToUTF8(s)string / blob编码导入
ReadFile(name) / WriteFile(name[, utf8])boolean文本文件读写

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




四、追加与格式化: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. //2026-09-23 版起 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.52026-01-022026-01-02 03:04:05
  2. K11_append_fmt=v=007
  3. K15_append_dbl_brace=6.5
复制代码

三条实测结论(第 1、2 条的行为在 2026-09-23 版发生了变化,下面按最新版写,并标出旧版差异):


  • append(double) 的默认输出是"最短表示":6.5 就是 6.5,100 就是 100,3.14159 就是 3.14159。

   ⚠️ 版本变化:2026-09-22 及更早的版本这里固定输出 6 位小数(6.5 会变成 6.500000)。老代码若依赖了这个固定位数,升级后会"变短";需要固定位数就显式给格式串。

  • append(double, 格式串) 从 2026-09-23 版起真正可用(旧版会落到整数重载上,把小数截断):


  • append(6.5, '{:.2f}') → 6.50;'{:.3f}' → 6.500;'{:f}' → 6.500000
  • append(6.5, '{:>8.2f}') →     6.50(宽度填充有效);'{:08.3f}' → 0006.500;'{:+.2f}' → +6.50(正号有效)
  • append(6.5, '{}') → 6.5(旧版这里是 6,小数被吃掉了)
  • 整数侧没变:append(long, '{}') → 7,append(long, '{:03d}') → 007 ✅
  • 不支持的格式符会静默返回空串(不报错、不崩溃):'{:.1%}'、'{:.2%}'(百分号)、'{:,.2f}'(千分位逗号)实测都是空串,别写
  • 舍入用的是"银行家舍入"(四舍六入五取偶):2.5 → 2、3.5 → 4、6.5 → 6。做金额别指望 {:.0f} 给四舍五入,要进位就自己先加 0.5
  • 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. //替换:第三个参数是 bIgnoreCase(是否忽略大小写),不是 replaceAll
  27. ls_s.fromstring('a-b-c')
  28. ls_s.replace('-', '+')
  29. of_w('K28_replace=' + ls_s.tostring())
  30. //大小写开关:第三个参数是 bIgnoreCase(内容 'aBcB')
  31. ls_s.fromstring('aBcB')
  32. ls_s.replace('b', 'X', true)
  33. of_w('K32_rep_ic_true=' + ls_s.tostring())
  34. ls_s.fromstring('aBcB')
  35. ls_s.replace('b', 'X', false)
  36. of_w('K33_rep_ic_false=' + ls_s.tostring())
  37. //按位置替换 = 删除 + 插入,nLen 传 0 表示什么都不做
  38. ls_s.fromstring('abcdefg')
  39. ls_s.replace(2, 3, '')
  40. of_w('K29_replace_del=' + ls_s.tostring())
  41. //InsertStr 插入单个字符是对的
  42. ls_s.fromstring('abcdefg')
  43. ls_s.insertstr(2, 'X')
  44. of_w('K30_insert_1c=' + ls_s.tostring())
  45. //插入多个字符要自己拼(InsertStr 多字符实测结果不对)
  46. ls_s.fromstring('abcdefg')
  47. ls_tmp = of_insert(ls_s.tostring(), 2, 'XY')
  48. of_w('K31_insert_safe=' + ls_tmp)
  49. destroy ls_s
  50. 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=5
  7. K26_substr_2=bcdefg
  8. K27_substr_2_3=bcd
  9. K28_replace=a+b+c
  10. K32_rep_ic_true=aXcX
  11. K33_rep_ic_false=aBcB
  12. K29_replace_del=aefg
  13. K30_insert_1c=aXbcdefg
  14. K31_insert_safe=aXYbcdefg
复制代码

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


  • LastPos 的带参重载在 2026-09-23 版已修好,但"起点越界"仍是雷区。实测 'abcabc'(长度 6):


  • lastpos('b') = 5 ✅;lastpos('b', 0) = 5 ✅(第二参传 0 等价于单参)
  • lastpos('b', 1) = 0 ✅;lastpos('b', 2) = 2 ✅;lastpos('b', 5) = 5 ✅;lastpos('b', 6) = 5 ✅ —— 起点落在 1 到 length() 之间时,语义是正确的
  • lastpos('B', 6, true) = 5(忽略大小写);lastpos('B', 6, false) = 0(区分大小写)✅
  • ⚠️ 起点越界时返回值不确定:lastpos('b', 100) 用同一份程序连跑 4 次,实测拿到过 0、0、77、77 —— 同一个二进制、同一个输入,结果都不一样,属于越界读。起点必须先自己夹进有效范围再传
  • (版本变化:旧版这里更糟 —— 起点只要不是 0 就一律返回 0,整个带参重载等于不可用。)


  • Replace 的第三个参数不是 replaceAll,而是 bIgnoreCase(是否忽略大小写)。2026-09-23 版直接在声明里把它改了名,实测完全对得上:


调用(对象内容为 aBcB)结果含义
replace('b', 'X')aXcX两参版默认忽略大小写
replace('b', 'X', true)aXcX忽略大小写 → 大写 B 也算命中
replace('b', 'X', false)aBcB区分大小写 → 小写 b 匹配不上大写 B,一个都没换
replace('B', 'X', false)aXcX区分大小写 → 两个 B 都命中


   → 这里要订正上一版的说法:上一版把它记成"replaceAll 参数无效、永远全替换",是因为当时拿 'a-b-c' 这种全小写串去测 —— 大小写开关在纯小写串上当然看不出任何差别。这个参数一直是有效的,只是它管的不是"替换几处",而是"要不要区分大小写"。想只替换第一处,仍然得自己 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/$1 → 09/22/2026;
  • 无匹配时原样返回:'abc' → 'abc';
  • PB 字符串里反斜杠不是转义符,\d 直接写就行,不用写成 \\d。





七、按行读取与拆分
  1. //=========================================================
  2. // 演示:按行读取与拆分
  3. // 要点:GetLine 越界从 2026-09-23 版起返回空串;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_empty=' + String(Len(ls_s.getline(4)) = 0))
  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_empty=true
  4. K63_split_n=4
  5. K64_split_xx=5[a][][b][][c]
复制代码

两个反直觉的地方:


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


另外 Split 的 ignoreNull 参数有个小陷阱:'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 / toHalf:abc123 → abc123,ABC123 → ABC123,中文() → 中文()。做单据录入、开票接口(很多税控接口只收半角)非常实用;
  • PinYin 返回的是拼音首字母(大写),不是全拼:重庆 → ZQ,银行 → YX。做简码 / 助记码字段正好;
  • GBK2BIG5 / BIG52GBK 是可逆的:big52gbk(gbk2big5('中文')) == '中文';
  • ChineseMoney 五个重载(long / int / double / decimal / string),实测:
  • 1234(long)→ 壹仟贰佰叁拾肆元整
  • 1234.56(double)→ 壹仟贰佰叁拾肆元伍角陆分
  • '100000000' → 壹亿元整
  • '0.05' → 零元零角伍分
  • 'abc'(非法)→ 零元整,不报错,所以入参合法性要自己先校验
  • 负数(2026-09-23 版实测:五个重载全部正常):'-12.34' → 负壹拾贰元叁角肆分、'-0.01' → 负零元零角壹分、'-100000000' → 负壹亿元整、long -1234 → 负壹仟贰佰叁拾肆元整、int -12 → 负壹拾贰元整、double -0.05 → 负零元零角伍分;'-0' 会得到 负零元整(负零也带「负」字,无害但要知道)。套在 TRY-CATCH 里跑同样安静通过 —— 不抛异常,没有东西可捕。⚠️ 但 2026-09-22 及更早的版本传负数会出事:string 重载必崩进程(TRY-CATCH 兜不住),数值重载有时崩、有时把负号错转成 佰 / 万(如实测 decimal -12.34 → 佰壹拾贰元叁角肆分、long -1234 → 万壹仟贰佰叁拾肆元整),且 PB 10 与 PB 12.5 表现不一致 —— 旧版金额转大写必须先判正负,新版可直接传;
  • GetRandSerial(flag, numLen):flag 是前缀,会原样拼在随机数字前面。getrandserial('A', 8) 得到 A + 8 位数字,长度 9;getrandserial('N', 6) → N686815。flag 传空串时返回的就是纯数字;
  • 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('')false(2026-09-23 版起会复位;旧版是 true)
SetNull() → fromstring('b')false(写非空内容才复位)
SetNull() → 'b' → Clear()false


结论:


  • IsNull() 不是"内容为空吗",而是"是否处于 NULL 标记状态";
  • 只有 SetNull() 能把它置成 true。复位规则在 2026-09-23 版变了:FromString('') 与 Clear() 现在都会复位成 false(旧版两者都不复位);而 append('') 仍然不复位 —— "追加空内容"不算写入了东西,只有真正写进非空内容才复位;
  • ⚠️ 判断"有没有内容"请用 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() 会被编译器标记为「已过时」(警告 C0185;PB 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 行为一致,没有需要分叉的写法。

同一份示例对象源码、同一份测试脚本(71 条硬断言 + 59 条版本探针读数),在两版各跑一遍:

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


2026-09-23 版复测时又加跑了一轮版本兼容性探针(59 条读数),PB 10 与 PB 12.5 输出仍是逐字节一致。唯一一处两版读数不同的,是 lastpos('b', 100) 这类起点越界的调用 —— 那不是版本差异,而是"越界读未定义",两版都不可依赖(见第五节)。

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

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

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

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




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

验证时间:2026-09-22 首轮;2026-09-23 用当次最新的 PBIDEA 构建复跑,并补了一轮版本兼容性验证;同日应读者实测反馈,再补一轮「金额大写传负数」专项探针 —— 对 09-22 旧版与 09-23 新版各跑 PB 10 / PB 12.5 双轨(见下表 ⑦)。
2026-09-23 复跑结论:核心运行库为 18,017,792 字节 / 2544 个导出(md5 048e3e92d4a4…)。与 09-22 快照相比,DLL 的导出表只少了 1 项 —— _hwGetIPipv4@8,一个下划线开头的私有导出,不在任何公开 API 里,对使用者没有影响;对象源码有 3 个文件改动(uo_string.sru、uo_pbidea_report.sru、一个 demo 页面)。其中 uo_string.sru 的两处改动直接改变了行为,也就是本文第四、五、九、十三节订正的内容,详见第十五节。

PBIDEA 会在同一套功能下反复重新构建(09-22 当天就从 18,020,864 字节变到 18,021,888 字节),所以本文不沿用任何旧快照的数字,每次都用当下最新构建把全部验证重跑一遍。下表是 2026-09-23 最新版下的结果。

环节做法结果
① 导入把示例对象、PBIDEA 的 uo_string 源码与版本探针导入工程4 个对象,0 错
② 全量编译工程画板 Full Rebuild(PB 10 / PB 12.5 各一次)均 0 错
③ 运行无界面 PBVM 跑 71 条硬断言 + 59 条版本兼容探针两版均 PASS 71/71;探针读数逐字节一致
④ 输出比对两版运行结果逐字节比较功能 1538 字节、探针 1501 字节,均完全相同
⑤ 静态核查语法/API 核查 + 源码编码核查0 错 0 提醒;GBK + CRLF + 无 BOM 全通过
⑦ 金额大写负数专项探针(本日补测)14 个用例(string/double/decimal/long/int 五个重载的负数 + 正数、零、坏输入对照 + TRY-CATCH 兜底验证),一例一次独立运行(防崩溃连坐拖垮整个测试进程);09-22 与 09-23 两版 × PB 10 / PB 12.5 各跑一遍,共 56 次运行09-23 版 28 例全部正常输出带「负」字的大写;09-22 版 17 例进程异常终止(string 重载必崩,TRY-CATCH 兜不住)、5 例输出错乱(负号变 佰/万,且 PB 10 与 PB 12.5 表现不一致)、仅 6 例对照正常



  • 示例对象:nvo_str_demo(of_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 个计时器语义判别函数),与上面的功能验证是两个独立工程,互不干扰;
  • 版本兼容性探针:nvo_v23_probe(59 条读数,用来复现第十五节那张"版本差异表");
  • 上面三个工程连同各自的测试脚本都已打进 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 条,按踩中概率排序;已按 2026-09-23 版逐条复核)

#坑现象规避
1Substr 越界进程 C++ 异常崩溃(0xE06D7363)先判 length()
2Format 类型不匹配进程崩溃,控制台 fmt fail实参类型与格式符严格对齐
3ChineseMoney 传负数09-23 版已修复:五个重载正常输出「负…」;09-22 及更早会崩进程(string 重载必崩,TRY-CATCH 兜不住)或输出错乱(负号变 佰/万)升级到 09-23 版;旧版必须先判正负,负数自己加"负"字
4LastPos 起点越界返回值不确定(实测同程序连跑 4 次拿到 0/0/77/77)起点先夹进有效范围(起点在范围内时语义是对的)
5Replace 第三参含义易误解它是 bIgnoreCase 不是 replaceAll:传 false 会"看起来像没替换"(大小写不匹配)想忽略大小写传 true;只替换首处只能自己算位置
6InsertStr 插多字符结果错乱用 of_insert() 或 Replace
7Split 的分隔符是字符集合不是子串先换成单字符哨兵
8GetLine 越界2026-09-23 版起返回空串(旧版返回最后一行)仍建议用 GetLineCount() 做上界
9Split 的 ignoreNull返回值变小但数组不缩容用返回值做循环上界
10IsNull() 语义不是"内容空不空";append('') 不复位,但 FromString('')/Clear() 从 09-23 版起会复位判空一律用 Len(tostring())=0
11append(double, 格式串)09-23 版起可用;不支持的格式符(%、千分位 ,)会静默返回空串用 {:.2f} 这类标准写法;拿不准先跑一次看结果
12Format 会清内容原内容被覆盖需要保留就先 FromString(原串 + 格式串)
13ToAnsi()编译器报 C0185 过时(PB 10 与 PB 12.5 都报)改用 ToUtf8()
14忘 destroystringDestroy() 不执行配对 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 拼装入库。两者不冲突,别为了一个对象把所有 + 都换掉。




十五、版本更新说明(2026-09-23 版实测)
这一节回答一个很实际的问题:PBIDEA 更新了,我手上的代码要不要动? 下面全部是拿 09-22 与 09-23 两版、用同一份测试脚本对跑出来的,不是看版本号猜的。

15.1 这次更新动了什么

层面变化对使用者的影响
核心运行库18,021,888 → 18,017,792 字节(md5 0046d1f4bd0a… → 048e3e92d4a4…)体积变小不代表能力变弱 —— 判版本只看导出表
导出表2545 → 2544,只少 1 项:_hwGetIPipv4@8下划线开头是私有导出,不在公开 API 里,对使用者无影响;本次无新增导出
uo_string.sru新增重载 append(double, string strFormat)让小数格式化真正可用(见 4.1)
核心运行库(DLL 内部)ChineseMoney 负数处理重写 —— 源码声明没变、修复全在 DLL 内部,只看导出表和源码 diff 发现不了这条旧版传负数崩进程或输出错乱;新版正常输出「负…」(两版对跑实测见 15.2 与十二节 ⑦)
uo_pbidea_report.sru新增重载 Print(readonly string printerName)报表可按打印机名直接打印
demo 页面注释里补了 job.print(...) 的用法示例仅供参考


15.2 同一份探针跑两版,差异只有这几项

同一份测试脚本(59 条读数)在两版各跑一遍,只有 6 项读数不同;另用金额大写专项探针(14 例 × 两版 × PB 10 / PB 12.5)补测,又钉死第 7 项 —— 加起来是下面 7 项:

读数09-22 版09-23 版性质
append(6.5)6.5000006.5行为变更:默认改成最短表示
append(6.5, '{}')66.5修复:小数不再被整数重载吃掉
append(6.5, '{:.2f}')空串6.50修复:格式串真正生效
lastpos('b', 2) / ('b', 5)02 / 5修复:带起点查找可用
SetNull() 后 fromstring('') 的 IsNull()truefalse行为变更:空串写入现在会复位 NULL 标记
getline(4)(总共 3 行)L3空串修复:越界不再返回最后一行
chinesemoney 传负数(专项探针 14 例)string 重载:进程异常终止;数值重载:崩或输出错乱(如实测 佰壹拾贰元叁角肆分)五个重载全部正常输出「负…」修复:负数不再崩溃、不再错乱

注:lastpos('b', 100) 这类起点越界的调用不在上表 —— 它两版都是未定义行为,不属于"版本差异"。

顺带复核了性能:43 条性能断言在新版下仍全部通过;把两版的计时读数逐项对比,差异都在 ±5% 内(例:native_plus n=50000 从 12922 ms → 13500 ms,native_chunk 从 422 ms → 421 ms,全程总耗时 187578 → 190453 ms)。这是同机重复测量的正常抖动,本次更新没有改变性能特性。⚠️ 也正因如此,第 14.1 节的绝对数字请当作"某一次实测的读数",它真正的用途是比较不同写法之间的相对快慢 —— "原生最慢""分块累积快 30 倍"这两条在新版下同样成立。

15.3 升级要不要改代码


  • 需要自查的三类代码:


  • 依赖 append(double) 输出固定 6 位小数的(拼定宽报表、定宽日志)→ 改成显式 append(v, '{:.6f}');
  • 依赖 Replace(..., false) 等于"全部替换" 的 → 检查原串里有没有大小写变体,false 现在真的区分大小写;
  • 依赖 SetNull() 之后"再写空串仍保持 NULL"的 → 现在 FromString('') / Clear() 会复位。


  • 纯受益、不用改的:LastPos(str, nStart) 带起点查找、append(double, 格式串)、GetLine 越界保护、ChineseMoney 负数 —— 这些旧版本来就不对,升级后自动变正确(负数这条旧版还会崩进程,新版直接把「负」字吐对)。
  • 可以顺手加固的:给所有 LastPos 的起点加一次范围夹取,越界那一路在任何版本都不安全。





十六、扩展点


  • 和 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、金额大写的入参校验(09-23 版起负数可直接传,旧版必须先判正负)、UTF-8 文件读写都包一层,业务代码里就不再需要直面那 14 条坑。





小结

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


  • 正则是它最大的价值 —— RegexMatch 全串校验、RegexSearch 子串提取、RegexReplace 批量替换,PB 原生一样都没有;
  • 中文能力(拼音首字母、简繁、全半角、人民币大写)是独一份,财务与票据类系统直接省掉几百行代码;
  • 它有脾气 —— Substr 越界、Format 类型不匹配会直接崩进程(ChineseMoney 负数在 09-23 版已修复:旧版会崩或输出错乱,新版五个重载正常输出「负…」,见 15.2);LastPos 起点越界会拿到不确定值;InsertStr 插多字符结果错乱。这几个都是"看起来能用、实测不对"。照着第十三节的坑清单写代码,比事后排查省事得多。(注:上一版提到的"LastPos 带参不可用""Replace 的 replaceAll 无效"两条,已按 2026-09-23 版订正,见第五、十五节。)
  • 性能上别凭直觉 —— 实测原生 + 反而最慢,uo_string 快 4~5 倍但同样是平方复杂度;真要攒几万段文本,分块累积快 30 倍(见 14.2)。


本文全部代码与 71 条功能断言、43 条性能基准断言、59 条版本兼容探针在 PB 10 与 PB 12.5 双轨跑通,输出逐字节一致。另用金额大写负数专项探针(14 例)对 09-22 / 09-23 两版 × PB 10 / PB 12.5 各补测一遍(共 56 次运行):新版 28 例全部正常输出「负…」;旧版 17 例进程异常终止、5 例输出错乱、仅 6 例正数与零对照正常。
共享共进共赢
Sharing And Win-win Results
SYBASEBBS - 免责申明1、欢迎访问“SYBASEBBS.COM”,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@sybasebbs.com
 楼主| 发表于 2026-9-22 07:00:01 | 显示全部楼层
本帖最后由 pbai 于 2026-9-23 10:14 编辑

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

uostring10_pb10.zip (62.23 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 → 最后 nvo_v23_probe.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 秒,慢的写法本身一次就要十几秒)
  • 版本探针:create nvo_v23_probe → ls_report = lnv.of_run() → destroy lnv → MessageBox('版本探针', ls_report) (约 5 秒)


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

【包内文件】

  • src/uo_string.sru —— PBIDEA 随版发布的官方对象源码(本包附带的是 2026-09-23 版)
  • src/nvo_str_demo.sru —— 功能示例对象(7 个演示函数 + 安全插入函数 of_insert)
  • src/nvo_perf_demo.sru —— 性能基准对象(7 种累加写法 + 1 个计时器语义判别函数)
  • src/nvo_v23_probe.sru —— 版本兼容性探针对象(59 条读数)
  • tests/strdemo.pbtest —— 71 条功能硬断言测试脚本
  • tests/perf.pbtest —— 43 条性能基准硬断言测试脚本
  • tests/v23probe.pbtest —— 版本兼容性探针测试脚本
  • evidence/diag_strdemo_100.txt、evidence/diag_strdemo_125.txt —— 功能测试两版实跑输出(1538 字节,逐字节相同)
  • evidence/diag_perf_100.txt、evidence/diag_perf_125.txt —— 性能基准两版实跑输出(含全部计时读数)
  • evidence/diag_v23probe_100.txt、evidence/diag_v23probe_125.txt —— 版本探针两版实跑输出(1501 字节,逐字节相同)
  • 使用说明.txt —— 完整部署与排错说明(含 2026-09-23 版行为变化逐条说明)


【实机验证情况】(2026-09-23,用当次最新的 PBIDEA 构建)

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


【版本更新说明(2026-09-23 版,同一份脚本两版对跑实测)】
这次更新没有新增任何导出:核心运行库 18,021,888 → 18,017,792 字节(md5 0046d1f4bd0a… → 048e3e92d4a4…),导出表 2545 → 2544,只少了一个下划线开头的私有导出(不在公开 API 里,对使用者无影响)。对象源码有 3 个文件改动,其中 uo_string 的两处改动会直接改变行为:

读数09-22 版09-23 版性质
append(6.5)6.5000006.5行为变更:默认改成最短表示
append(6.5, '{}')66.5修复:小数不再被整数重载吃掉
append(6.5, '{:.2f}')空串6.50修复:格式串真正生效
lastpos('b', 2) / ('b', 5)02 / 5修复:带起点查找可用
SetNull() 后 fromstring('') 的 IsNull()truefalse行为变更:空串写入会复位 NULL
getline(4)(总共 3 行)L3空串修复:越界不再返回最后一行


要不要改代码:

  • 需要自查的三类:① 依赖 append(double) 固定 6 位小数的(拼定宽报表、定宽日志)→ 改成显式 append(v, '{:.6f}');② 依赖 Replace(..., false) 等于"全部替换"的 → 这个参数的名字已从 replaceAll 改为 bIgnoreCase(语义就是"是否忽略大小写",一直如此),传 false 现在真的区分大小写;③ 依赖 SetNull() 之后"再写空串仍保持 NULL"的 → 现在 FromString('') 与 Clear() 会复位。
  • 纯受益、不用改的:LastPos(str, nStart) 带起点查找、append(double, 格式串)、GetLine 越界保护 —— 这三处旧版本来就不对,升级后自动变正确。
  • 可以顺手加固的:给所有 LastPos 的起点加一次范围夹取,越界那一路在任何版本都不安全。


怎么判断自己手上是哪个版本:跑一次版本探针 nvo_v23_probe 即可 —— 若 lastpos('b', 2) 得到 0、append(6.5, '{:.2f}') 得到空串、append(6.5, '{}') 得到 6,你手上是 09-22 及更早的版本;若分别是 2、6.50、6.5,就是 09-23 或更新的版本。(别拿 lastpos('b', 100) 作判断依据:那是起点越界的读,返回值不确定,实测同一程序连跑 4 次拿到过 0、0、77、77。)

【性能基准实测读数】单位 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
复制代码

(上表为 2026-09-22 首轮读数。2026-09-23 换新版复跑的读数见 evidence/diag_perf_100.txt,逐项差异都在 ±5% 内,属同机重复测量的正常抖动 —— 相对快慢的结论(原生最慢、分块累积快 30 倍)两次完全一致。)

四条结论:

  • 原生 + 是最慢的 —— "uo_string 要跨 DLL 边界所以更慢"这个推断站不住
  • uo_string 确实更快,但没那么夸张:官方源码注释写"效率提高了 50 倍",实测 5 万次量级是 4~5 倍
  • 两种写法都接近平方复杂度(指数 1.77~2.03),uo_string 只压常数、没把曲线掰直
  • "先入数组再统一拼接"这条流传的优化基本无效(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 格式符与实参类型不匹配(C++ 异常,退出码 0xE06D7363,TRY-CATCH 兜不住)。ChineseMoney 传负数在 2026-09-23 新版已修复:五个重载实测 28 例全部正常输出"负壹拾贰元叁角肆分"这类大写,TRY-CATCH 无异常可捕;旧版会崩进程或输出错乱,旧版用户必须先判正负。

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

(本附件更新记录:2026-09-22 更新两次,先去除说明文档里的本机路径并随当次最新 PBIDEA 构建复测,再并入性能基准工程与实际计时读数;2026-09-23 换为当次最新 PBIDEA 构建(18,017,792 字节 / 2544 个导出 / md5 048e3e92d4a4…)重新验证的版本,功能断言由 66 条扩到 71 条、新增版本兼容性探针,并补上"本次版本更新改了什么、手上代码要不要动"的说明。同日又校一处:探针脚本注释里清掉了版本源目录名(脱敏),脚本内容与实测读数均未变。四轮校订后,功能测试输出与同版本基线始终逐字节一致。同日第五轮:应读者实测反馈补跑"金额大写传负数"专项探针(14 例 × 09-22/09-23 两版 DLL × PB10/PB12.5 双轨、一例一次独立运行防崩溃连坐),新版 28 例全绿、旧版 17 例进程异常终止 5 例输出错乱;文章坑清单、版本差异表与本说明文档同步订正(说明文档补了这条版本判据)。)
共享共进共赢
Sharing And Win-win Results
您需要登录后才可以回帖 登录 | 站点注册

本版积分规则

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

Mail To:Admin@SybaseBbs.com

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

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

GMT+8, 2026-11-14 12:41 , Processed in 0.048535 second(s), 8 queries , MemCached On.

Powered by Discuz! X5.0

© 2001-2026 Discuz! Team.

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