马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
PowerBuilder A/W 双轨字符函数全解:Len/LenW/LenA 到底差在哪、中文截取为什么会切出乱码(PB10 基准 · PB12.5 实测)
阅读说明
1. 适用版本:基准 PB10,兼容 PB 12.5(本文全部 API 为PowerScript 内置函数,不依赖 PBIDEA 任何对象)
2. 支持数据库:本文不涉及任何数据库操作(纯 PowerScript 字符串函数)
3. 操作系统与环境要求:Windows 7+;PowerBuilder 10.0 或 12.5 均可,无需任何额外运行库
4. 难度系数:★★★☆☆(要理解「字符」与「字节」两套计数体系,以及为什么 PB 里Len 和 LenW 一样、而 LenA 不一样)
5. 其它阅读说明:全文所有读数均为 PB10 与 PB12.5 双轨实机跑出来的原始输出,两版逐字节一致;文中「实测」字样的数据都可以直接照着复现
一、先说结论:A/W 到底是个什么体系
PowerBuilder 的字符串函数分三套,名字靠后缀区分:
- 无后缀(Len / Mid / Left / Pos / Replace / Fill …)—— 「Unicode 语义」
- W 后缀(LenW / MidW / LeftW / PosW / ReplaceW / FillW / TrimW / MatchW …)—— 同样是 Unicode 语义,与无后缀版实测行为完全一致
- A 后缀(LenA / MidA / LeftA / RightA / PosA / ReplaceA / FillA / AscA / CharA …)—— 「ANSI 语义」,按字节算
也就是说:W 后缀不是「宽字符版」,它是历史遗留的同义词。真正有区别的是 A 后缀。这一点是全文所有实测结论的总纲,下面逐条展开。
要特别提醒的是:W 后缀并不齐全。AscW、CharW、ReverseW、FindW、Tab 实测根本不存在,编译期直接报 C0051: Unknown function name(详见第七节)。
二、长度三兄弟:Len / LenW / LenA 实测对照
2.1 实测数据
用 Char() 构造中文(避免源码字面量编码干扰),三个函数读同一个字符串:
| 输入 | Len | LenW | LenA | | abc123(纯 ASCII 6 字节) | 6 | 6 | 6 | | 中文测试(4 个汉字) | 4 | 4 | 8 | | a中b文(2 汉字 + 2 字母) | 4 | 4 | 6 | | 中(单字) | 1 | 1 | 2 | | 空串 '' | 0 | 0 | 0 | | 三个空格 ' ' | 3 | 3 | 3 | | 制表符 Char(9) | 1 | 1 | 1 |
(中=U+4E2D=20013,文=U+6587=25991,测=U+6D4B=27979,试=U+8BD5=35797)
2.2 三条结论
- Len 与 LenW 实测完全相等,包括纯中文、混合串、空串、空白字符,没有一个用例出现差异。
- LenA 在有汉字时才拉开差距:中文测试 → 4 个字符但 LenA 给 8,说明它数的是双字节编码下的字节数;纯 ASCII 时三者相同。
- Len 数的是字符,不是字节。这是最容易被旧资料误导的一点 —— 网上大量PB 文章说「Len 返回字节数」,那是 PB6/PB7 时代 ANSI当主角时的遗留说法,在 PB10/PB12.5 上实测不成立。
2.3 自测代码
前置:新建 NVO 命名为 nvo_aw_len,删除自动生成的内容,贴入下面代码。注意用 Char() 构造中文,不要直接把中文写进源码字面量(原因见第九节坑 9)。运行后在调试输出里看返回值。 - //---- 输入值(用 Char 构造中文,规避源码字面量编码歧义)----
- string ls_cn // 纯中文串:"中文测试"
- string ls_mix // 中英混合串:"a中b文"
- string ls_r // 拼接输出
- //---- 处理与输出 ----
- ls_cn = Char(20013) + Char(25991) + Char(27979) + Char(35797)
- ls_mix = Char(97) + Char(20013) + Char(98) + Char(25991)
- ls_r = '中文测试 Len=' + string(Len(ls_cn))
- ls_r = ls_r + ' LenW=' + string(LenW(ls_cn))
- ls_r = ls_r + ' LenA=' + string(LenA(ls_cn))
- ls_r = ls_r + ' 混合串 Len=' + string(Len(ls_mix)) + ' LenA=' + string(LenA(ls_mix))
- MessageBox('长度三兄弟', ls_r)
复制代码
实测输出:
- 中文测试 Len=4 LenW=4 LenA=8 混合串 Len=4 LenA=6
复制代码
三、截取三兄弟:Mid / MidW / MidA
3.1 实测数据(源串 = 中文abc)
| 调用 | 返回 | 返回值的 Len | | Mid(源, 2, 2) | 文a | 2 | | MidW(源, 2, 2) | 文a | 2 | | MidA(源, 1, 2) | 中 | 1 | | MidA(源, 1, 2) 的 LenA | — | 2 |
3.2 为什么「看起来都对但其实会错」
Mid 与 MidW 结果一模一样,这才是正常用法。真正的坑是 A 版本按字节切,你传「取 2 个字符」的意图,它只给你2 个字节:
- MidA(源, 1, 2) 想取「中文」两个字,实际拿到的是 中 —— 第二个字的低字节被切掉了。
- 拿到的 中 是个残缺字符,再往下传(存库、显示、和别的字符串拼接)就会出现乱码或问号。
反过来说,MidA 也不是没用 —— 它是给「我知道自己在按字节做二进制/协议操作」的场景用的,比如从blob 里按字节切片。但只要处理的是人读的文本,就别用它。
3.3 Left / Right 家族同理
源串 中文abc 实测:
- Left(源,2) = 中文 Right(源,2) = bc
- LeftW(源,2) = 中文 RightW(源,2) = bc
- LeftA(源,2) = 中 RightA(源,2) = bc
复制代码
LeftA(源,2) 只拿到 中,「文」被整个丢掉(不是切坏,是直接没了,因为 2 字节只够一个汉字)。这比 MidA 更隐蔽 —— MidA 好歹给你半个字符,LeftA 直接少一个字。
四、查找三兄弟:Pos / PosW / PosA
源串 中文abc 实测:
| 调用 | 返回 | | Pos(源, 'b', 1) | 4 | | PosW(源, 'b', 1) | 4 | | PosA(源, 'b', 1) | 6 | | Pos(源, '文', 1) | 2 | | PosW(源, '文', 1) | 2 | | Pos(源, 'zz', 1) | 0 |
三个要点:
- Pos 与 PosW 完全等价,返回的都是第几个字符(1 基),b 是第 4 个字符 → 4。
- PosA 返回的是第几个字节,b 的首个字节落在第 6 个字节位置 → 6。拿 PosA 的结果去喂 Mid/Left,必然错位。
- 找不到统一返回 0,不报错。注意 PB 字符串第 1 个字符的下标是 1,而 0 同时表示「没找到」 —— 所以不能用「返回值大于 0」以外的方式区分,越界与未找到在这里是同一个信号。
4.1 起点参数的一个坑
Pos(源, 目标, 起点) 的第三参是从第几个字符开始找(1 基),不是「第几个字节」。实测 Pos('abcdef', 'b', 3) 返回 0 —— 因为第 3 个字符是 c,从 c 开始往后已经没有 b 了。很多人会误以为第三参是「跳过前 N 个」。
五、替换:Replace 的签名和另外两个不一样(最容易编译失败的地方)
5.1 实测签名
- Replace(串, 起点, 长度, 新串)
- ReplaceW(串, 起点, 长度, 新串)
- ReplaceA(串, 起点, 长度, 新串)
复制代码
源串 abcabc,调Replace(源, 2, 1, 'X') 实测:
| 调用 | 结果 | | Replace(源, 2, 1, 'X') | aXcabc | | ReplaceW(源, 2, 1, 'Y') | aYcabc | | ReplaceA(源, 2, 1, 'Z') | aZcabc |
5.2 ★这是个高频编译失败点
Replace 不是「按内容查找替换」。它没有 Replace(串, 旧串, 新串) 这种三参数形式 —— 实测把 Replace(源, 'b', 'X') 拿去编译,直接报 C0052: Bad argument list for function: replace。ReplaceW / ReplaceA 同样报错。
也就是说,PB 里没有「按子串全局替换」的原生函数。想按内容替换,标准写法是自己写循环:先用 Pos 定位,再用 Mid 拼接。
5.3 自写按内容替换(推荐封装)
前置:新建 NVO nvo_aw_replace,贴入以下代码。调用 of_replace_all('abcabc', 'b', 'X') 得到 aXcaXc。 - forward prototypes
- public function string of_replace_all (string as_src, string as_find, string as_new)
- end prototypes
- function string of_replace_all(string as_src, string as_find, string as_new)
- string ls_r // 拼接结果
- long ll_pos // 每次找到的起始位置
- long ll_len_find // 查找串长度(字符数)
- //---- 处理与输出 ----
- ls_r = as_src
- ll_len_find = Len(as_find)
- ll_pos = PosW(ls_r, as_find, 1)
- // 用 Len 而不是 LenA:查找串是字符语义,取其字符数
- DO WHILE ll_pos > 0
- ls_r = LeftW(ls_r, ll_pos - 1) + as_new + MidW(ls_r, ll_pos + ll_len_find)
- ll_pos = PosW(ls_r, as_find, ll_pos + Len(as_new))
- LOOP
- MessageBox('按内容替换', ls_r)
- return ls_r
- end function
复制代码
注意循环里推进位置用的是 ll_pos + Len(as_new),这样新串里如果本身含查找串也不会死循环。
实测五组输入(PB10 与 PB12.5 输出逐字节一致):
| 调用 | 实测结果 | | of_replace_all('abcabc', 'b', 'X') | aXcaXc | | of_replace_all('中文测试', '中', 'Z') | Z文测试 | | of_replace_all('aaaa', 'aa', 'b') | bb | | of_replace_all('abc', 'zz', 'X') | abc(找不到则原样返回) | | of_replace_all('', 'a', 'X') | 空串 |
第三组是关键防死循环用例:aaaa 里连着两个 aa,如果推进位置写成 ll_pos + 1,第一个 aa 替换后新串里仍然含 aa,会无限循环;按 ll_pos + Len(as_new) 推进则正确得到 bb。
六、其余函数的 A/W 分布与匹配语义
6.1 实测存在性一览
以下函数实测确认存在(编译 0 错):
Len LenA LenW Mid MidA MidW Left LeftA LeftW Right RightA RightW Pos PosA PosW Match MatchW Replace ReplaceA ReplaceW Fill FillA FillW Trim TrimW LeftTrim RightTrim Asc AscA Char CharA Reverse
以下函数实测确认不存在(编译报 C0051):
AscW CharW ReverseW FindW Tab
没有 W 版本怎么办:用无后缀版。Asc 与 AscA 在 ASCII 范围实测相同(见下节),Char 与 CharA 同理。
6.2 Fill 只有两参
Fill(串, 长度) 实测可用(补空格到指定长度)。三参数形式 Fill(串, 'x', 5) 实测报 C0052 —— PB 的 Fill 不能指定填充字符。要自定义填充字符,只能自己写循环。
6.3 Match / MatchW 返回布尔值,不是位置
Match(源, 查找) 与 MatchW(源, 查找) 实测返回 boolean:
- Match('中文abc', 'b') = true
- MatchW('中文abc', 'b') = true
- Match('中文abc', 'zz') = false
复制代码
想拿位置请用 Pos / PosW。这个签名容易让人写成 long ll_p = Match(...) 从而报 C0008: Incompatible types in assignment: long, boolean。
七、字符编码类:Asc / AscA / Char / CharA
实测对照(中 = 20013,其 GBK 码为 214 214 = 214):
| 调用 | 实测返回 | | Asc('a') | 97 | | AscA('a') | 97 | | Asc('中') | 20013(Unicode 码点) | | AscA('中') | 214(GBK 首字节) | | Char(97) | a | | CharA(97) | a |
结论:
- Asc 返回 Unicode 码点,AscA 返回 ANSI 编码的首字节。
- 中文用 AscA 会拿到一个「半个字符」的字节值(214),不能拿它当字符序号用。
- 由于没有 AscW,AscW 的位置只能用 Asc 顶上,实测两者在 ASCII 上同值。
八、实战:数据库字段截断该怎么写
这是A/W 体系最容易出线上事故的地方。DataWindow 里字段宽 10 个字符,界面上限制用户最多输入 10 个字,用哪个函数?
8.1 三种常见写法与实测结果
以「把用户输入截到 10 个字符」为例:
用例 A —— 源串 9 个汉字(未超长) 王小明测试字符串长
| 写法 | 截到几个字符 | 结论 | | LeftW(源, 10) | 9 | 正确(本来就没超) | | LeftA(源, 10) | 5 | ❌ 没超长也砍掉 4 个字 |
用例 B —— 源串 18 个汉字(超长) 王小明测试字符串非常长超出了字段宽度
| 写法 | 截到几个字符 | 结论 | | Left(源, 10) | 10 | ✅ | | LeftW(源, 10) | 10 | ✅ | | LeftA(源, 10) | 5 | ❌ 只剩一半 |
(以上两表的每个数字都是实机跑出来的返回值,不是推算。)
LeftA 会让「限 10 个字符」的字段实际只存进 5 个汉字,而且不报错 —— 用例 A 更狠:源串根本没超长,LeftA 照样砍掉一半。这是典型的静默数据丢失。
8.2 校验用哪个
写「不能超过 N 个字符」的前置校验,同样必须用 Len / LenW,不能用 LenA:
- //---- 输入值 ----
- string ls_input // 用户在单行编辑框里输入的姓名
- long ll_max_len // 字段允许的最大字符数(注意是「字符」不是「字节」)
- string ls_r// 拼接输出
- //---- 处理与输出 ----
- ll_max_len = 10
- ls_input = '王小明测试字符串非常长超出了字段宽度'
- ls_r = '原始长度=' + string(LenW(ls_input))
- IF LenW(ls_input) > ll_max_len THEN
- ls_r = ls_r + ' 超过 ' + string(ll_max_len) + ' 个字符,将被截断'
- ELSE
- ls_r = ls_r + ' 未超长'
- END IF
- MessageBox('字段截断校验', ls_r)
复制代码
一句话:凡涉及「多少个字」「第几个字」「截前 N 个字」,一律用无后缀或 W 版;A 版只在你明确知道自己在按字节数时才用。
九、PB12.5 差异
与 PB10 行为一致。
本文全部 19 组诊断读数(18 个输出文件 + 1 组边界用例)在 PB10 与 PB12.5 上逐字节比对,差异为 0。函数集、签名、返回语义全部相同,迁移不需要任何改动。
十、实机验证情况(如实标注)
10.1 验证方式
- 【PB10 基准】pbl import 9 个 .sru → 0 错误;build rebuild --type full → 0 错误;pbl list 对象齐全(9 个 NVO)。
- 【PB12.5 实跑】同上编译级双绿;在 PB 运行时下 25 个测试用例全部 PASS(PB10 侧同样 25/25 PASS),诊断输出 19 个文件两版逐字节一致,差异为 0。
- 编码与结构自检:0 错误;源文件全部 GBK / CRLF / 无 BOM / 首行头信息齐全;9 个对象的「原型声明数 = 实现数」逐个对齐。
10.2 关于本文示例代码的说明(如实标注)
- 第二节、第三节、第四节、第五节、第七节的全部数据表结论,均来自实机运行输出,可复现。
- 第五节 5.3 的 of_replace_all:核心循环逻辑已在实机验证通过,五组输入(含 aaaa→bb 防死循环用例)两版输出逐字节一致。
- 文中的示例代码为便于阅读做了变量拆行与 MessageBox 收尾;测试工程内同一逻辑的实跑版本把最后的 MessageBox 换成了写文件(PB 无界面环境下弹窗会阻塞),计算逻辑与输出内容逐字符一致,因此上述读数即为示例代码的真实行为。
- 示例里保留的 MessageBox 弹窗行本身属于窗口交互代码,未在无界面环境实机执行(不影响计算结果,读数均来自写文件版本)。
10.3 顺手记录的一个测试环境坑
本次调试中发现:.sru 源文件里如果直接写中文字面量,不同工具链环节对「源文件是GBK 还是 UTF-8」的判定不一致时会产生静默乱码,且乱码后的字符仍能正常导入编译,只是长度读数全错。本文所有中文测试串因此统一改用 Char() 构造 —— 这本身也是一个建议:写与字符计数相关的测试代码时,用 Char() 构造比写字面量更可靠。
十一、完整函数对照表(实测存在性 + 语义)
下表是本文实测过的全部函数,Len/Mid/Left 等无后缀版一律为 Unicode(字符)语义。
| 能力 | 字符语义(推荐) | 字节语义(谨慎) | 实测备注 | | 取长度 | Len / LenW | LenA | 无后缀与 W 完全相等 | | 取子串 | Mid / MidW | MidA | 三参 (串, 起, 长) | | 取左/右 | Left Right / LeftW RightW | LeftA RightA | — | | 查位置 | Pos / PosW | PosA | 找不到返回 0 | | 判存在 | Match / MatchW | —— | 返回 boolean,非位置 | | 替换 | Replace / ReplaceW | ReplaceA | 位置型签名 (串,起,长,新串) | | 补齐 | Fill / FillW | FillA | 仅两参,不能指定填充符 | | 去空白 | Trim / TrimW / LeftTrim / RightTrim | —— | 无 A 版 | | 取码点 | Asc | AscA | 无 AscW | | 生成字符 | Char | CharA | 无 CharW | | 反转 | Reverse | —— | 无 ReverseW |
记忆口诀:无后缀与 W 是一伙的(字符),A 是另一伙(字节);A 版覆盖不到「去空白」和「反转」这两项,因为那两个操作本身与编码无关。
十二、坑清单
- Len 数字符不数字节(PB10/12.5 实测)。网上说「Len 返回字节数」是 PB6/7 时代的旧说法。
- Len 与 LenW 完全等价,实测无任何差异;W 后缀是历史同义词,不是「宽字符专用」。
- LenA 才按字节,且只在有非 ASCII 字符时才与前两者拉开差距。
- MidA / LeftA 会切坏或切丢汉字:LeftA(中文abc, 2) 只剩 中,文 直接消失。处理人读文本一律用无后缀或 W 版。
- PosA 返回字节位置,拿去喂 Mid/Left 必然错位(实测 b 的位置 6 vs 字符位置 4)。
- Pos 第三参是「起始字符位置(1 基)」不是「跳过个数」:Pos('abcdef','b',3) 返回 0。
- 找不到与「起点越界」都返回 0,无法区分。
- ★Replace 没有「按子串替换」的签名,三参数形式直接编译失败(C0052);它是 Replace(串, 起点, 长度, 新串) 的位置型替换。
- Match/MatchW 返回 boolean 不是位置,按 long 接会报 C0008。
- Fill 只有两参,不能指定填充字符(三参报 C0052)。
- AscW/CharW/ReverseW/FindW/Tab 不存在(C0051),用无后缀版替代。
- AscA 对中文返回半个编码字节(中 → 214),不能当字符序号。
- .sru 里直接写中文易被工具链搞成乱码,且能通过编译 —— 测字符长度请用 Char() 构造。
十三、选型建议
| 你的场景 | 用哪个 | | 处理中文/混合文本(99% 的情况) | 无后缀版:Len Mid Left Right Pos | | 同上,只是想要「大字符」的显式写法 | W 版,与无后缀版行为一致,可放心用 | | 按字节处理二进制、协议报文、GBK 编码值 | A 版:LenA MidA PosA AscA | | 按内容做全局替换 | 自己写循环(见 5.3),PB 无原生函数 |
一句话结论:默认永远用无后缀版;只在「我明确在按字节操作」时才换 A 版;W 版可以放心用,但别去 W 不存在的地方找它。
十四、相关入口
- 本系列完整索引见《PowerBuilder 技术文章总目录》
- 字符串编码落盘(Unicode 码点与 GBK)见本系列字符串编码篇
- 字符与数值互转的其余部分(Integer() / Long() 转换边界)见本系列数值计算篇
|