祝愿大家身体健康!

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

[学习笔记] PowerBuilder Blob 全解:四种编码、BlobMid 切片与 BlobEdit 的真实行为(终结符会吃位置、帮助示例是 ANSI 遗产,PB10 基准 · PB12.5 实测)

[复制链接][复制链接]
发表于 昨天 07:53 | 显示全部楼层 |阅读模式

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

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

×
PowerBuilder Blob 全解:四种编码、BlobMid 切片与 BlobEdit 的真实行为(终结符会吃位置、帮助示例是 ANSI 遗产,PB10 基准 · PB12.5 实测)

阅读说明
① 适用版本:PB 10.0 基准编写,PB 12.5 实机逐项验证(两版读数逐字一致,唯一差异见第十四节);Blob 相关函数自 PB 5 时代就有,但 Unicode 化(PB 10)前后行为大不相同,低版本(PB 9 及更早)不能直接套用本文结论。
② 支持数据库:本文不涉及数据库。Blob 落库(UPDATEBLOB / SELECTBLOB)已有专篇,本文只讲语言层的 blob 类型本体。
③ 操作系统与环境要求:Windows 7 及以上;无需任何第三方运行库,纯 PowerScript 即可复现全部示例。
④ 难度系数:★★★☆☆(函数都不难,难在「字节数 vs 字符数」「终结符占位」「sized blob 的固定长度」这三处反直觉,以及一套帮助文档没讲对的行为)。
⑤ 其它阅读说明:本文所有结论来自实机探测——PB10 与 PB12.5 双轨各 15 组主用例 + 13 组负向用例全部跑完,另附 8 份十六进制字节证据。凡没实测到的,文中明确标注「未实测」,不猜。与既有文章的分工:《字符串处理全解》讲字符串函数、《编码全解》讲字符串与编码转换、《uo_blob 全解》讲 PBIDEA 对象、《UPDATEBLOB 篇》讲落库——本篇只讲 blob 这个数据类型本身。

一、这一篇讲什么

图片要存进数据库、文件要整块读进内存、和 C++ 的 DLL 交换二进制、给 Web 接口拼一串字节——PowerBuilder 里干这些活的数据类型都是 blob。但 blob 恐怕是 PB 里文档写得最含糊的类型:帮助里 BlobEdit 的示例数字是错的;老版本那套 LeftB / RightB / MidB 字节函数在 Unicode 化之后悄悄消失了,书上还在教;blob 能不能用加号拼接、能不能用等号比较,文档干脆不提。

这一篇把 28 组探针的实测结果全部摊开:类型与赋值语义、四种编码的字节真相、BlobMid 的全部边界、BlobEdit 的真实行为(它往你的数据里偷偷放了终结符)、以及哪些老函数已经死了。

二、类型基础:sized、unsized 与「赋值即拷贝」

先声明,再讲语义。blob 有两种形态:不定长(blob lb)和定长(blob{100} lb)。前者随内容伸缩,后者一出生就占死 100 字节。
  1. // blob 的两种声明与 Len 的真实含义
  2. blob lb_var            // 不定长
  3. blob{100} lb_fixed     // 定长:一出生 Len 就是 100
  4. long ll_len
  5. ll_len = Len(lb_var)      // 实测 0(未初始化,非 NULL)
  6. ll_len = Len(lb_fixed)    // 实测 100(分配多大就是多大,与里面有没有数据无关)
  7. MessageBox('定长 blob', '声明即占 100 字节,写没写数据 Len 都是 100')
复制代码

实测两组关键读数:未初始化的不定长 blob,Len() 返回 0、IsNull() 返回 false——它不是 null,只是空的。而定长的 blob{100},从声明那一刻起 Len() 就是 100,用 BlobEdit 写了 8 个字节进去之后还是 100。

赋值是值拷贝。b07 探针做了双份验证:
  1. // 拷贝语义:改原件,副本纹丝不动
  2. blob lb1, lb2
  3. blob lba[], lbb[]
  4. lb1 = Blob('ABCDEF', EncodingANSI!)
  5. lb2 = lb1
  6. BlobEdit(lb1, 1, 'XY')          // 改 lb1 前 6 字节
  7. MessageBox('副本', String(lb2, EncodingANSI!))   // 实测 'ABCDEF',未受影响
复制代码

数组同样如此:lbb = lba 之后改 lba[1],lbb[1] 还是旧值。这和 DataWindow 里 ShareData 的「共享一份」完全是两码事——blob 之间没有引用,只有复印。

三、Blob():四种编码与字节级证据

Blob(string {, encoding}) 把字符串变成字节块。四个编码值:EncodingANSI!、EncodingUTF8!、EncodingUTF16LE!(默认)、EncodingUTF16BE!。我让探针把同一个中文串按四种编码各生成一份 blob,原样落盘,再用外部工具逐字节核对:

编码实测字节(「中文」二字)长度
默认(UTF16LE)2D 4E 87 654
EncodingANSI!D6 D0 CE C4(GBK)4
EncodingUTF8!E4 B8 AD E6 96 876
EncodingUTF16BE!4E 2D 65 874


三件事值得记住。第一,默认是 UTF-16LE——一个英文字母也是 2 字节,这和 C 程序员的直觉相反。第二,ANSI 编码在中文 Windows 上就是 GBK,编码结果随机器区域设置变,跨机器传 ANSI blob 就是埋雷。第三,Blob() 生成的内容不带终结符——落盘 4 字节就是 4 字节,干干净净。

解码方向用 String(blob {, encoding}),四个方向全部往返成功。但编码错配不报错:把 GBK 字节硬按 UTF-8 解,实测返回 2 个字符的乱码而不是空串或错误——String 的解码器不做严格校验,错配了就给你一串莫明其妙的东西,排查乱码时要先问「这份字节到底是什么编码」。

四、Len 与 LenW:数字节,且 NULL 会传染

Len() 对 blob 返回字节数,对字符串返回字符数,一个函数两副面孔:
  1. blob lb
  2. string ls_cn
  3. ls_cn = '中文'
  4. lb = ls_cn                        // 默认 UTF16LE,4 字节
  5. MessageBox('同一串东西', '字符串 Len=' + String(Len(ls_cn)) + ',blob Len=' + String(Len(lb)))
  6. // 实测:字符串 2,blob 4
复制代码

LenW() 对 blob 也存在,实测行为和 Len() 完全一样:1 字节返 1、4 字节返 4——它不会帮你把字节换算成字符,别指望它。

NULL 才是真正的坑。SetNull(lb) 之后:IsNull(lb) 为 true;Len(lb) 返回 null 而不是 0;String(lb) 返回 null;BlobMid(lb, 1, 1) 返回 null;BlobEdit(lb, 1, 'X') 返回 null。更要命的是传染性:'x' + String(Len(lb_null)) 这一整个表达式求出来就是 null——读数凭空消失、拼接整段变空,都是 NULL 传染的典型现场。

五、BlobMid:切片的全边界

blob 没有下标运算,取中间一段靠 BlobMid(data, n {, length})。n 从 1 数起。全部边界实测:

调用实测结果
BlobMid(6字节'ABCDEF', 3, 2)'CD',长 2
BlobMid(同上, 3)(省略 length)'CDEF',取到末尾
BlobMid(同上, 5, 10)(要 10 只有 2)'EF',有多少给多少,不报错
BlobMid(同上, 10, 2)(n 越界)空 blob,Len=0,不报错
BlobMid(同上, 0, 2)(n=0)'AB'!n=0 被当成 1
BlobMid(空 blob, 1, 1)空 blob
BlobMid(null blob, 1, 1)null


两个要点:n=0 不报错也不返空,悄悄按 1 处理——循环切片时把计数器算成 0 不会炸,只会拿错数据,这种静默比崩溃更难查。越界一路静默:要多了给剩余的,起点都没了给空的,全都不抛异常。想发现切片错误只能靠 Len 自查。

六、BlobEdit:真实行为(本文核心)

BlobEdit(blobvariable, n, data {, encoding}) 往 blob 的第 n 字节开始写数据,返回「下一个可写位置」。听起来人畜无害,实测下来它和帮助文档的说法有三处对不上,而且每处都会咬人。

第一处:string 数据会附带终结符,帮助示例的数字是 ANSI 时代的遗产。 帮助文档信誓旦旦地写着「BlobEdit(blob_var, 1, 'ZZZ') 返回 5」(3 个字符 + 2)。实测 PB10 和 PB12.5 都返回 9:默认编码 UTF16LE 下 'ZZZ' 占 6 字节,后面还跟 2 字节的 NUL 终结符,1 + 6 + 2 = 9。文档那个「+2」只在 PB9 的 ANSI 世界里成立。
  1. // BlobEdit 的位置账本(全部实测)
  2. blob{100} lb
  3. long ll_next
  4. ll_next = BlobEdit(lb, 1, 'ZZZ')                        // 返回 9:6 字节 UTF16 + 2 字节终结符
  5. ll_next = BlobEdit(lb, ll_next, Blob('ABCD', EncodingANSI!))  // 返回 13:4 字节原样写入,无终结符
  6. ll_next = BlobEdit(lb, 1, 'XY')                         // 返回 7:覆盖写,不是插入
复制代码

字节级证据把账本钉死了(f86_b05.bin 前 12 字节):58 00 59 00 00 00 00 00 41 42 43 44——'XY' 的 UTF16 字节占 1~4,终结符占 5~6,7~8 是第一笔写入留下的零,'ABCD' 的 ANSI 字节在 9~12。写 string 送终结符,写 blob 不送,这正是组包时位置全乱的根源。

指定了 encoding 的 string 同样送终结符,但只送 1 字节:BlobEdit(lb, 1, '中文', EncodingUTF8!) 写 6 字节 UTF-8 加 1 字节 0x00,返回 8。也就是说终结符跟着编码走——UTF16 是 2 字节的 0x0000,ANSI/UTF8 是 1 字节的 0x00。

第二处:unsized blob 不自动扩容。 往一个只是声明过、没赋过值的不定长 blob 里 BlobEdit,实测返回 null——数据根本没写进去,它也不会替你开空间。想用 BlobEdit,要么声明成定长 blob{100},要么先 lb = Blob('') 给它赋个值。

第三处:空间不够返回 null,但「正好够」的位置账要自己会算。 blob{4} 里从第 3 字节写 'ZZZ'(需 8 字节)→ 返回 null,文档没骗人;写 4 字节的 ANSI blob(正好占满)→ 返回 5,成功。注意「正好够」的判定包含终结符:blob{4} 里 BlobEdit 一个 3 字符的 UTF16 string(需 8 字节)同样失败,哪怕你只想要 3 个字节。

还有两条行为规则:中途写是覆盖不是插入(第 1 字节写 6 个字节,原来 1~6 的内容没了,7 之后原样保留);定长 blob 用 FileWriteEx 落盘会把整个分配区写满——100 字节的 blob 哪怕只用了 14 字节,落盘就是 100 字节、尾部 86 个零。给文件瘦身要么切 BlobMid 再写,要么用不定长 blob。

七、拼接与比较:加号等号都能用,文档就是不说

blob 能不能 +、能不能 =,帮助文档一个字不提。实测:都能用,两版一致。
  1. blob lb1, lb2, lb3
  2. lb1 = Blob('A', EncodingANSI!)
  3. lb2 = Blob('B', EncodingANSI!)
  4. lb3 = lb1 + lb2                       // 实测可用!Len(lb3) = 2,原样拼接、无终结符
  5. If lb1 = lb2 Then                     // 实测可用:按内容逐字节比较
  6.     MessageBox('比较', '两个 blob 相等')
  7. End If
复制代码

拼 Blob('A') + Blob('B') 得到干净的 2 字节——加号不像 BlobEdit 那样夹带终结符,组简单包反而比 BlobEdit 顺手。等号比较按内容判定,两个内容相同的 blob 判 EQ。没实测到的是 >/< 排序语义和 += 形态,文中不作断言。

顺带一个负向结论:Blob(lb)——拿 Blob() 包一个现成 blob——不可用。PB10 给出干净的 C0052「Bad argument list for function」;PB12.5 则让 ORCA 编译通道直接挂起(90 秒无响应,两次复现)。想复制一份 blob 用赋值就行,别画蛇添足。

八、B 系字节函数已经死了

老 PB 书籍里教字节级操作的那套函数——LeftB、RightB、MidB、PosB——在 Unicode 化的 PB10 和 PB12.5 里全部不存在。我们给每个函数单独建了探针对象,两版编译器给出同一判决:

函数PB10PB12.5
LeftB(string, n)C0051 Unknown functionC0051 Unknown function
RightB(string, n)C0051C0051
MidB(string, n, len)C0051C0051
PosB(string, s)C0051C0051


需要按字节处理字符串时,正解是先转 blob 再切片:BlobMid(Blob(ls, EncodingANSI!), n, len),或者干脆用编码明确的 Blob(ls, EncodingANSI!) + 文件函数族。网上还能搜到大量 LeftB/MidB 的示例代码,那都是 PB9 时代的遗物,粘过来就是一排 C0051。

九、ToAnsi / ToUnicode / FromAnsi / FromUnicode

四个编码便捷函数全部实测存在且可用,两版一致:
  1. blob lb
  2. string ls
  3. lb = ToAnsi('中文')           // 实测 4 字节,等价于 Blob('中文', EncodingANSI!)
  4. lb = ToUnicode('AB')          // 实测 4 字节,等价于 Blob('AB')(默认 UTF16LE)
  5. ls = FromAnsi(lb)             // 实测 '中文',ANSI blob 解回字符串
  6. ls = FromUnicode(lb)          // 实测 'AB',UTF16 blob 解回字符串
复制代码

值得一记的是:ToAnsi/ToUnicode 在帮助库里有正式条目(并自述「将在未来版本被废弃」),而 FromAnsi/FromUnicode 连正式条目都没有——只在 seealso 里露了个名字,实测却真实可用。用它们没问题,但别在文档里找它们的详细说明,找不到的。

十、文件往返与 FileEncoding 的盲区

blob 与文件互转是 blob 最常见的归宿,一轮完整往返实测(UTF-8 无 BOM):
  1. blob lb, lb2
  2. long li_f, ll_n
  3. lb = Blob('V86-中文', EncodingUTF8!)
  4. li_f = FileOpen('data.bin', StreamMode!, Write!, LockWrite!, Replace!)
  5. FileWriteEx(li_f, lb)              // 落盘 10 字节,无 BOM(十六进制核对过)
  6. FileClose(li_f)
  7. li_f = FileOpen('data.bin', StreamMode!, Read!, LockRead!, Replace!)
  8. ll_n = FileReadEx(li_f, lb2)       // 实测返回 10(读到的字节数)
  9. FileClose(li_f)
  10. MessageBox('往返', String(lb2, EncodingUTF8!))    // 实测与原文完全一致
复制代码

一个实用的坑:FileEncoding('data.bin') 对这份无 BOM 的 UTF-8 文件返回 EncodingANSI!。FileEncoding 靠 BOM 猜编码,没有 BOM 的 UTF-8 一律被当成 ANSI——它说 ANSI 不代表真是 ANSI,自己的文件用什么编码要自己记着。

十一、完整主线示例:组包与解析

把上面的规则合起来:定长 blob 当缓冲区,BlobEdit 按位置账本写三段数据,BlobMid 按同样的账本解回来:
  1. // 自定义二进制记录:1 字节版本号 + 4 字节名称(ANSI) + 6 字节中文(UTF8)
  2. blob{100} lb
  3. long ll_next, li_f
  4. string ls_name, ls_cn
  5. ls_cn = '中文'
  6. lb = Blob('')
  7. ll_next = BlobEdit(lb, 1, Blob(String(Char(6)), EncodingANSI!))  // 1 字节版本号 → 下一位置 2
  8. ll_next = BlobEdit(lb, ll_next, 'PB86', EncodingANSI!)           // 4 字节 + 1 终结符 → 下一位置 7
  9. ll_next = BlobEdit(lb, ll_next, ls_cn, EncodingUTF8!)            // 6 字节 + 1 终结符 → 下一位置 14
  10. // 解析:位置账本与写入时逐字对应
  11. MessageBox('版本号', String(Asc(String(BlobMid(lb, 1, 1), EncodingANSI!))))   // 实测 6
  12. ls_name = String(BlobMid(lb, 2, 4), EncodingANSI!)                            // 实测 'PB86'
  13. MessageBox('中文', String(BlobMid(lb, 7, 6), EncodingUTF8!))                  // 实测 '中文'
复制代码

实测读数:ver=6, name=PB86, cn=EQ,两版逐字一致。这个示例的每一处位置数字都来自十六进制证据(06 50 42 38 36 00 E4 B8 AD E6 96 87)——如果按「每个 string 长度就是字节数」的老经验去算偏移,第一段就会错位,这正是 BlobEdit 终结符规则的实战意义。

十二、坑清单(发布前逐条实测核对)


  • 默认编码是 UTF-16LE:一个英文字母 2 字节,Blob('AB') 是 4 字节不是 2。
  • Blob() 生成的 blob 不带终结符;带终结符的是 BlobEdit 写 string 的时候。
  • BlobEdit 写 string 送终结符:默认 UTF16 送 2 字节 0x0000,显式 EncodingANSI!/UTF8! 送 1 字节 0x00——返回位置要把终结符算进去。
  • 帮助文档 BlobEdit 示例的「返回 5」是 ANSI 时代遗产,Unicode 版实测返回 9。
  • BlobEdit 写 blob 数据无终结符,返回位置 = 起点 + 字节数。
  • unsized blob 不自动扩容:没赋过值就 BlobEdit,返回 null,什么都没写。
  • 空间不够 BlobEdit 返回 null 且不写入;「够不够」的判定含终结符。
  • BlobEdit 是覆盖不是插入,中途写入直接吞掉原有字节。
  • 定长 blob 的 Len 恒等于分配大小;FileWriteEx 落盘写满分配区,尾部零填充。
  • BlobMid 的 n=0 被静默当成 1;越界一律静默(返短块或空块),错误要靠 Len 自查。
  • Blob('') 是 0 字节(不带 UTF16 终结符);未初始化 blob Len=0 但不是 null。
  • NULL 全面传染:null blob 的 Len/String/BlobMid/BlobEdit 全返 null,与字符串拼接后整串变 null。
  • LeftB / RightB / MidB / PosB 已不存在(两版 C0051),字节操作走 Blob + BlobMid。
  • Blob(lb) 不可用(PB10 报 C0052;PB12.5 编译通道挂起);复制 blob 用赋值。
  • FileEncoding 认不出无 BOM 的 UTF-8,返回 EncodingANSI!;文件编码自己记账。


十三、PB12.5 差异

除一条外,全部读数两版逐字一致(15 组主用例 + 5 组可运行负向用例 + 8 份字节证据)。唯一差异在 Blob(lb) 这个非法调用的报错形态:PB10 编译期干净地报 C0052「Bad argument list for function」;PB12.5 的 ORCA 编译通道则挂起(90 秒无响应,两次复现),未取得错误码。两者都判定该写法不可用,但用 PB12.5 IDE 手工编译遇到「假死」时,可以怀疑是不是写了 Blob(blob)。

十四、选型:什么时候用哪一套

纯内存组包/解析(自定义二进制格式、和 DLL 交换缓冲区):本文的 Blob/BlobMid/BlobEdit/加号拼接,零依赖。字符串与编码转换为主:直接用 String/Blob 的 encoding 参数或 ToAnsi/FromUnicode 家族,别绕 blob。要落库:UPDATEBLOB/SELECTBLOB 专篇(注意每条语句只处理一个 Blob 列、必查 SQLCode)。要读写文件:FileReadEx/FileWriteEx(大文件必须用 Ex 版)。需要正则、拆分、编码探测这些字符串高级能力时再上 PBIDEA 的 uo_blob/uo_charset——原生 blob 家族管的是「字节搬运」,不管「内容理解」。
共享共进共赢
Sharing And Win-win Results
SYBASEBBS - 免责申明1、欢迎访问“SYBASEBBS.COM”,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@sybasebbs.com
 楼主| 发表于 昨天 08:02 | 显示全部楼层
本帖最后由 pbai 于 2026-11-3 08:16 编辑

附件与运行说明(Day86 Blob 全解配套交付包)

1F 正文代码均为完整可运行片段。本楼附件是按惯例打包的 PB10 兼容版 PBL(规则:涉及完整可运行对象的交付,另出 PB10 版本):

附件:blob86_pb10.zip(26.8 KB / 48 条目)

  • blob86.pbl:PB10 ORCA 全量编译 0 错,内含应用对象 a_v100 + 12 个探针 NVO(nvo_b01~b08、b10~b12、nvo_art)
  • blob86.pbt:相对路径工程文件,导入即用
  • src\:12 份 .sru 源码(GBK/CRLF/无 BOM,可直接 Library Import)
  • tests\:15 份 .pbtest 探针脚本
  • evidence\:PB10/PB12.5 双轨运行读数、落盘文件的十六进制字节证据、负向探针编译结果


运行方法:打开 blob86.pbt(或把 blob86.pbl 挂进任意应用的库列表),create nvo_art 后调 of_run(),期望读数 art: ver=6 used=14 name=PB86 cn=EQ。nvo_b04 / nvo_b06 / nvo_b08 各有 of_run2,请一并调用。纯内存操作,无需数据库与第三方运行库。

验证结论:PB10 与 PB12.5 双轨 pbtest 15 用例全 PASS、读数逐字一致;静态检查 0 错 0 提醒;源码 GBK/CRLF/无 BOM。唯一版本差异是 Blob(blob) 非法调用的报错形态(PB10 干净报 C0052、PB12.5 的 ORCA 通道挂起),详见 1F 第十三节。

环境要求:PowerBuilder 10.0 及以上(PB12.5 同样可用),Windows 7+。



blob86_pb10.zip (26.22 KB, 下载次数: 0)
共享共进共赢
Sharing And Win-win Results
您需要登录后才可以回帖 登录 | 站点注册

本版积分规则

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

Mail To:Admin@SybaseBbs.com

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

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

GMT+8, 2026-11-4 01:41 , Processed in 0.034741 second(s), 9 queries , MemCached On.

Powered by Discuz! X5.0

© 2001-2026 Discuz! Team.

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