马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
本帖最后由 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)能应付日常拼接,但有几件事它真的做不了:
| 需求 | 原生 PowerScript | uo_string | | 正则表达式 | 只有 Match() 做包含判断,没有分组、没有替换、没有位置信息 | RegexMatch / RegexSearch / RegexReplace 三件套,支持捕获组 | | 按分隔符拆成数组 | 只能 Pos + Mid 手写循环 | Split(delim, ref items[]) 一行搞定 | | 按行读文本 | 只能自己找 ~r~n | GetLineCount() / 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) → bcd | Substr(2, 3) → bcd(等价;但 uo_string 的 Substr 越界会崩进程,用前必须判 length()) | | 中文字符长度 | Len('中文abc') → 5 | length() → 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.50 | append(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() 释放。
- //=========================================================
- // 演示:创建 uo_string、写入内容、取值与长度
- // 操作步骤:把本段代码放进任意对象的函数里直接调用即可
- // 要点:uo_string 必须 create;用完 destroy;.text 是可读写属性
- //=========================================================
- uo_string ls_s //字符串对象
- string ls_val //取出来的普通字符串
- long ll_len //字符长度
- is_out = ''
- ls_s = create uo_string
- ls_s.fromstring('hello')
- ls_val = ls_s.tostring()
- of_w('K01_fromstring=' + ls_val)
- ll_len = ls_s.length()
- of_w('K02_length=' + String(ll_len))
- ls_s.text = 'abc属性'
- of_w('K03_text=' + ls_s.text)
- //中文按「字符」计数,不是字节
- ls_s.fromstring('中文abc')
- of_w('K04_len_cn=' + String(ls_s.length()))
- //清空与置空
- ls_s.fromstring('abc')
- ls_s.clear()
- of_w('K05_clear=' + ls_s.tostring() + '/len=' + String(ls_s.length()))
- destroy ls_s
- MessageBox('演示结果', is_out)
复制代码
实机输出(is_out 的内容):
- K01_fromstring=hello
- K02_length=5
- K03_text=abc属性
- K04_len_cn=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() | boolean | NULL 标记(语义见第九节,很反直觉) | | 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]]) | ulong | 1 基,找不到返回 0 | | LastPos(str[, nStart[, ignoreCase]]) | ulong | 最后一次出现位置 | | Substr(nStart[, nLen]) | string | 1 基截取 | | 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) | string | flag 前缀 + 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 prototypes 的 private: 段,属于内部工具函数,业务代码不要调用。
四、追加与格式化:append 与 Format
4.1 append:把各种类型拼进去
- //=========================================================
- // 演示:append 追加各种类型 + Format 格式化
- // 操作步骤:直接调用,观察 MessageBox 输出的每个 K 值
- // 要点:append 返回对象自身可链式调用;格式串是 fmt 风格({} / {:03d})
- //=========================================================
- uo_string ls_s
- long ll_num //整数示例值
- double ldb_num //小数示例值
- date ld_day //日期示例值
- datetime ldt_now //日期时间示例值
- is_out = ''
- ls_s = create uo_string
- ls_s.fromstring('A')
- ll_num = 123
- ls_s.append(ll_num)
- ldb_num = 6.5
- ls_s.append(ldb_num)
- ld_day = 2026-01-02
- ls_s.append(ld_day)
- ldt_now = DateTime(2026-01-02, 03:04:05)
- ls_s.append(ldt_now)
- of_w('K10_append=' + ls_s.tostring())
- //带格式追加:格式串必须是 fmt 风格,写 '000' 会原样输出
- ls_s.fromstring('v=')
- ll_num = 7
- ls_s.append(ll_num, '{:03d}')
- of_w('K11_append_fmt=' + ls_s.tostring())
- //坑:append 的 double 带 '{}' 会被截断成整数,带 '{:.2f}' 直接变空串
- ls_s.fromstring('')
- ls_s.append(ldb_num, '{}')
- of_w('K15_append_dbl_brace=' + ls_s.tostring())
- destroy ls_s
- MessageBox('演示结果', is_out)
复制代码
实机输出:
- K10_append=A1236.5000002026-01-022026-01-02 03:04:05
- K11_append_fmt=v=007
- 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, '{}') → 7,append(long, '{:03d}') → 007 ✅ 正常;
- append(double, '{}') → 6(6.5 被截断成整数!);
- append(double, '{:.2f}') / '{:f}' / '{:.0f}' → 空串;
- append(数值, '000') → 原样输出 000(格式串不是 PB 掩码,别混着写);
- append 是追加,Format 是覆盖。见 4.2。
4.2 Format:整串重新格式化
- uo_string ls_s
- long ll_num
- double ldb_num
- is_out = ''
- ls_s = create uo_string
- ldb_num = 6.5
- ll_num = 7
- //Format:先清空原内容再格式化
- ls_s.fromstring('')
- ls_s.format('{} + {} = {}', 'a', 'b', 'c')
- of_w('K12_format=' + ls_s.tostring())
- //按下标复用实参
- ls_s.fromstring('')
- ls_s.format('{0}-{1}-{0}', 'A', 'B')
- of_w('K13_format_idx=' + ls_s.tostring())
- //数值格式:实参类型必须与格式符匹配
- ls_s.fromstring('')
- ls_s.format('n={:05d}', ll_num)
- of_w('K14_format_int=' + ls_s.tostring())
- ls_s.fromstring('')
- ls_s.format('{:.2f}', ldb_num)
- of_w('K16_format_dbl_2f=' + ls_s.tostring())
- destroy ls_s
- MessageBox('演示结果', is_out)
复制代码
实机输出:
- K12_format=a + b = c
- K13_format_idx=A-B-A
- K14_format_int=n=00007
- 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 捕获,只能靠"实参类型一定对齐"来避免。
五、查找 / 截取 / 替换 / 插入
- //=========================================================
- // 演示:查找 / 截取 / 替换 / 插入
- // 要点:Pos 是 1 基、找不到返回 0;Substr 越界会崩进程,必须先判长度
- //=========================================================
- uo_string ls_s
- ulong lul_p //找到的位置(1 基)
- string ls_tmp //临时结果
- is_out = ''
- ls_s = create uo_string
- ls_s.fromstring('abcabc')
- lul_p = ls_s.pos('b')
- of_w('K20_pos=' + String(lul_p))
- lul_p = ls_s.pos('b', 3)
- of_w('K21_pos_from3=' + String(lul_p))
- lul_p = ls_s.pos('B', 1, true)
- of_w('K22_pos_ignorecase=' + String(lul_p))
- lul_p = ls_s.lastpos('b')
- of_w('K23_lastpos=' + String(lul_p))
- lul_p = ls_s.lastpos('b', 0)
- of_w('K24_lastpos_0=' + String(lul_p))
- lul_p = ls_s.lastpos('b', 5)
- of_w('K25_lastpos_5=' + String(lul_p))
- ls_s.fromstring('abcdefg')
- of_w('K26_substr_2=' + ls_s.substr(2))
- of_w('K27_substr_2_3=' + ls_s.substr(2, 3))
- //替换:实测三个重载都是「全部替换」,replaceAll 参数不起作用
- ls_s.fromstring('a-b-c')
- ls_s.replace('-', '+')
- of_w('K28_replace=' + ls_s.tostring())
- //按位置替换 = 删除 + 插入,nLen 传 0 表示什么都不做
- ls_s.fromstring('abcdefg')
- ls_s.replace(2, 3, '')
- of_w('K29_replace_del=' + ls_s.tostring())
- //InsertStr 插入单个字符是对的
- ls_s.fromstring('abcdefg')
- ls_s.insertstr(2, 'X')
- of_w('K30_insert_1c=' + ls_s.tostring())
- //插入多个字符要自己拼(InsertStr 多字符实测结果不对)
- ls_s.fromstring('abcdefg')
- ls_tmp = of_insert(ls_s.tostring(), 2, 'XY')
- of_w('K31_insert_safe=' + ls_tmp)
- destroy ls_s
- MessageBox('演示结果', is_out)
复制代码
配套的安全插入函数:
- //=========================================================
- // 安全插入:在 as_src 的第 al_pos 个字符前插入 as_ins(1 基)
- // 使用演示:of_insert('abcdefg', 2, 'XY') 返回 'aXYbcdefg'
- //=========================================================
- uo_string ls_s
- string ls_left
- string ls_right
- ls_s = create uo_string
- ls_s.fromstring(as_src)
- if al_pos <= 1 then
- ls_left = ''
- ls_right = ls_s.substr(1)
- else
- ls_left = ls_s.substr(1, al_pos - 1)
- ls_right = ls_s.substr(al_pos)
- end if
- destroy ls_s
- return ls_left + as_ins + ls_right
复制代码
实机输出:
- K20_pos=2
- K21_pos_from3=5
- K22_pos_ignorecase=2
- K23_lastpos=5
- K24_lastpos_0=5
- K25_lastpos_5=0
- K26_substr_2=bcdefg
- K27_substr_2_3=bcd
- K28_replace=a+b+c
- K29_replace_del=aefg
- K30_insert_1c=aXbcdefg
- 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。
- Replace 的 replaceAll 参数不起作用,永远是全部替换。'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 的三个方法补上了这一块。
- //=========================================================
- // 演示:正则表达式(PB 原生没有正则,这是 uo_string 最大的价值)
- // 要点:RegexMatch 是「全串匹配」,RegexSearch 才是「子串搜索」
- //=========================================================
- uo_string ls_s
- string lsa_items[] //匹配结果:第 1 项是整串,后面依次是各捕获组
- long lla_pos[] //各结果在原串中的起始位置(1 基)
- long lla_len[] //各结果长度
- long ll_ub //结果数组上界
- is_out = ''
- ls_s = create uo_string
- //全串匹配
- ls_s.fromstring('13800138000')
- of_w('K40_match_phone=' + String(ls_s.regexmatch('1[3-9][0-9]{9}')))
- ls_s.fromstring('tel:13800138000;')
- of_w('K41_match_inside=' + String(ls_s.regexmatch('1[3-9][0-9]{9}')))
- of_w('K42_search_inside=' + String(ls_s.regexsearch(1, '1[3-9][0-9]{9}')))
- //从指定位置开始搜索(1 基)
- ls_s.fromstring('a1b22c333')
- of_w('K43_search_from1=' + String(ls_s.regexsearch(1, '22')))
- of_w('K44_search_from5=' + String(ls_s.regexsearch(5, '22')))
- //带捕获组:items[1]=整串,items[2..]=各分组
- ls_s.regexsearch(1, '([a-z])([0-9]+)', lsa_items, lla_pos, lla_len)
- ll_ub = UpperBound(lsa_items)
- of_w('K45_items_ub=' + String(ll_ub))
- of_w('K46_item1=' + lsa_items[1])
- of_w('K47_item2=' + lsa_items[2])
- of_w('K48_pos1=' + String(lla_pos[1]))
- //替换:默认替换所有匹配
- ls_s.fromstring('a1b22c333')
- of_w('K49_regexreplace=' + ls_s.regexreplace('[0-9]+', '#'))
- ls_s.fromstring('2026-09-22')
- of_w('K50_regexreplace_group=' + ls_s.regexreplace('(\d{4})-(\d{2})-(\d{2})', '$2/$3/$1'))
- ls_s.fromstring('abc')
- of_w('K51_regexreplace_none=' + ls_s.regexreplace('[0-9]', '#'))
- destroy ls_s
- MessageBox('演示结果', is_out)
复制代码
实机输出:
- K40_match_phone=true
- K41_match_inside=false
- K42_search_inside=true
- K43_search_from1=true
- K44_search_from5=false
- K45_items_ub=3
- K46_item1=a1
- K47_item2=a
- K48_pos1=1
- K49_regexreplace=a#b#c#
- K50_regexreplace_group=09/22/2026
- 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。
七、按行读取与拆分
- //=========================================================
- // 演示:按行读取与拆分
- // 要点:GetLine 越界返回最后一行;Split 的分隔符是「字符集合」不是子串
- //=========================================================
- uo_string ls_s
- string lsa_parts[] //拆分结果
- long ll_n //拆分出的项数
- long ll_i //循环变量
- string ls_show //拼接后的展示串
- is_out = ''
- ls_s = create uo_string
- ls_s.fromstring('L1~r~nL2~r~nL3')
- of_w('K60_linecount=' + String(ls_s.getlinecount()))
- of_w('K61_line2=' + ls_s.getline(2))
- of_w('K62_line4=' + ls_s.getline(4))
- ls_s.fromstring('a,b,,c')
- ll_n = ls_s.split(',', lsa_parts)
- of_w('K63_split_n=' + String(ll_n))
- //多字符分隔符被当成字符集合:'XX' 会在每个 X 处切一刀
- ls_s.fromstring('aXXbXXc')
- ll_n = ls_s.split('XX', lsa_parts)
- ls_show = ''
- for ll_i = 1 to UpperBound(lsa_parts)
- ls_show = ls_show + '[' + lsa_parts[ll_i] + ']'
- next
- of_w('K64_split_xx=' + String(ll_n) + ls_show)
- destroy ls_s
- MessageBox('演示结果', is_out)
复制代码
实机输出:
- K60_linecount=3
- K61_line2=L2
- K62_line4=L3
- K63_split_n=4
- 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。(上面代码块里输出的方括号是实测原样,一个方括号对表示一个拆分项,中间空的一对表示空项。)
另外 Split 的 ignoreNull 参数有个小陷阱:'a,b,,c' 传 true 时返回值变成 3,但数组本身不会缩容(UpperBound 仍是 4,第 4 项是上一次的残留 'c')。取数据请以返回值为准、用 for i = 1 to 返回值,不要用 UpperBound。
八、中文专属能力:全角半角、拼音、简繁、金额大写
- //=========================================================
- // 演示:中文专属处理
- //=========================================================
- uo_string ls_s
- long ll_money //金额整数示例
- double ldb_money //金额小数示例
- is_out = ''
- ls_s = create uo_string
- of_w('K70_tofull=' + ls_s.tofull('abc123'))
- of_w('K71_tohalf=' + ls_s.tohalf('ABC123'))
- //拼音取的是首字母
- of_w('K72_pinyin=' + ls_s.pinyin('重庆'))
- //简繁互转
- of_w('K73_big5_back=' + ls_s.big52gbk(ls_s.gbk2big5('中文')))
- //人民币大写:支持 long / double / decimal / string 四种重载
- ll_money = 1234
- of_w('K74_money_long=' + ls_s.chinesemoney(ll_money))
- ldb_money = 1234.56
- of_w('K75_money_dbl=' + ls_s.chinesemoney(ldb_money))
- of_w('K76_money_str=' + ls_s.chinesemoney('100000000'))
- of_w('K77_money_zero=' + ls_s.chinesemoney('0.05'))
- of_w('K78_money_bad=' + ls_s.chinesemoney('abc'))
- //随机流水号:flag 是前缀,后面接指定位数的随机数字
- of_w('K79_serial_len=' + String(Len(ls_s.getrandserial('A', 8))))
- //比较:返回「逐位不同的字符个数」,0 表示相同
- of_w('K80_cmp_same=' + String(ls_s.compare('abc', 'abc')))
- of_w('K81_cmp_case=' + String(ls_s.compare('abc', 'ABC')))
- of_w('K82_cmp_ignum=' + String(ls_s.compare('a1b', 'a2b', 0, false, true, false, false, false)))
- destroy ls_s
- MessageBox('演示结果', is_out)
复制代码
实机输出:
- K70_tofull=abc123
- K71_tohalf=ABC123
- K72_pinyin=ZQ
- K73_big5_back=中文
- K74_money_long=壹仟贰佰叁拾肆元整
- K75_money_dbl=壹仟贰佰叁拾肆元伍角陆分
- K76_money_str=壹亿元整
- K77_money_zero=零元零角伍分
- K78_money_bad=零元整
- K79_serial_len=9
- K80_cmp_same=0
- K81_cmp_case=3
- 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'(非法)→ 零元整,不报错,所以入参合法性要自己先校验
- ⚠️ 传负数会崩进程! chinesemoney('-12.34') 实测直接 C++ 异常退出(同 Substr 越界)。金额转大写之前必须先判正负;
- 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() | | 刚 create | false | | 刚 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;只有写入非空内容能把它复位成 false;FromString('') 和 Clear() 都不复位;
- ⚠️ 判断"有没有内容"请用 Len(ls_s.tostring()) = 0,不要依赖 IsNull()。
顺带一提:Resize(6) 之后 length() 会变成 6,但 tostring() 仍只返回 'abc'(尾部是 \0 被截断);Resize(1) 会把 'abc' 截成 'a'。Resize 只改缓冲区长度,不补内容,一般业务用不到。
十、编码转换与文本文件读写
- //=========================================================
- // 演示:编码转换与文本文件读写
- // 要点:writefile 第二参 true 才写 UTF-8(带 BOM),默认按本地编码(GBK)
- //=========================================================
- uo_string ls_s
- uo_string ls_r
- blob lb_u8 //UTF-8 字节流
- boolean lb_ok //操作是否成功
- is_out = ''
- ls_s = create uo_string
- ls_r = create uo_string
- ls_s.fromstring('中A')
- lb_u8 = ls_s.toutf8()
- of_w('K90_utf8_len=' + String(Len(lb_u8)))
- of_w('K91_ansi_len=' + String(Len(ls_s.toansi())))
- of_w('K92_utf16_len=' + String(Len(ls_s.toutf16())))
- of_w('K93_fromutf8=' + ls_s.fromutf8(lb_u8))
- ls_s.fromstring('中文内容ABC')
- lb_ok = ls_s.writefile('demo_u8.txt', true)
- of_w('K94_write_u8=' + String(lb_ok))
- lb_ok = ls_r.readfile('demo_u8.txt')
- of_w('K95_read_u8=' + String(lb_ok) + '/' + ls_r.tostring())
- lb_ok = ls_r.readfile('no_such_file.txt')
- of_w('K96_read_missing=' + String(lb_ok))
- destroy ls_r
- destroy ls_s
- MessageBox('演示结果', is_out)
复制代码
实机输出:
- K90_utf8_len=4
- K91_ansi_len=3
- K92_utf16_len=4
- K93_fromutf8=中A
- K94_write_u8=true
- K95_read_u8=true/中文内容ABC
- 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 行为一致,没有需要分叉的写法。
同一份示例对象源码、同一份测试脚本(66 条硬断言),在两版各跑一遍:
| 验证项 | PB 10 | PB 12.5 | | 对象源码导入 | 2 个对象,0 错 | 2 个对象,0 错 | | 全量重建(Full Rebuild) | 0 错 | 0 错 | | 无界面断言运行 | PASS 66/66 | PASS 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_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 个计时器语义判别函数),与上面的功能验证是两个独立工程,互不干扰。两个工程连同各自的测试脚本已一起打进 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 条,按踩中概率排序)
| # | 坑 | 现象 | 规避 | | 1 | Substr 越界 | 进程 C++ 异常崩溃(0xE06D7363) | 先判 length() | | 2 | Format 类型不匹配 | 进程崩溃,控制台 fmt fail | 实参类型与格式符严格对齐 | | 3 | ChineseMoney 传负数 | 进程崩溃 | 先判正负,负数自己加"负"字 | | 4 | LastPos 带起始位置 | 除 0 外一律返回 0 | 只用单参或第二参写 0 | | 5 | Replace 的 replaceAll | 无效,永远全替换 | 只替换首处就自己算位置 | | 6 | InsertStr 插多字符 | 结果错乱 | 用 of_insert() 或 Replace | | 7 | Split 的分隔符 | 是字符集合不是子串 | 先换成单字符哨兵 | | 8 | GetLine 越界 | 返回最后一行 | 用 GetLineCount() 做上界 | | 9 | Split 的 ignoreNull | 返回值变小但数组不缩容 | 用返回值做循环上界 | | 10 | IsNull() 语义 | 写空串不复位 NULL 标记 | 判空用 Len(tostring())=0 | | 11 | append(double) | 固定 6 位小数;'{}' 截断成整数;'{:.2f}' 变空串 | 小数一律走 Format | | 12 | Format 会清内容 | 原内容被覆盖 | 需要保留就先 FromString(原串 + 格式串) | | 13 | ToAnsi() | 编译器报 C0185 过时(PB 10 与 PB 12.5 都报) | 改用 ToUtf8() | | 14 | 忘 destroy | stringDestroy() 不执行 | 配对 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 片合一) | | 10000 | 578 / 562 ms | 578 / 547 ms | 156 / 172 ms | 62 / 62 ms | | 30000 | 4718 / 4485 ms | 4640 / 4437 ms | 1032 / 1156 ms | 218 / 218 ms | | 50000 | 12922 / 12485 ms | 13234 / 12406 ms | 2688 / 3188 ms | 422 / 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 真正有效的一招:分块累积
既然两种写法都在反复复制整个累积串,那就少复制几次 —— 先把片段攒够一批,再一次性并进主串:
- string ls_buf, ls_part
- long ll_i
- ls_buf = ''
- ls_part = ''
- for ll_i = 1 to 50000
- ls_part = ls_part + String(ll_i) //内层只攒 100 个片段,代价很小
- if Mod(ll_i, 100) = 0 then
- ls_buf = ls_buf + ls_part //每 100 个才并进主串一次
- ls_part = ''
- end if
- next
- 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 带参、Replace 的 replaceAll、InsertStr 多字符三个都是"看起来能用、实测不对"。照着第十三节的坑清单写代码,比事后排查省事得多。
- 性能上别凭直觉 —— 实测原生 + 反而最慢,uo_string 快 4~5 倍但同样是平方复杂度;真要攒几万段文本,分块累积快 30 倍(见 14.2)。
本文全部代码与 66 条功能断言、43 条性能基准断言在 PB 10 与 PB 12.5 双轨跑通,输出逐字节一致。
|