马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
PowerBuilder 位运算全解:用 uo_bits 补齐 & | ^ ~ << >> 与掩码拆字段(PB10 基准 · PB12.5 实测)
【阅读说明】
- 适用版本:PowerBuilder 10 及以上(PB10 基线);本文全部代码在 PB10 与 PB12.5 双版本实机跑通,两版读数逐字节一致。PBIDEA 侧核验版本见文末【版本声明】。
- 支持数据库:本文不涉及任何数据库(纯内存位运算),无需连接 DBMS。
- 操作系统与环境要求:Windows XP 及以上;需安装 PBIDEA 运行库(PbIdea.dll),运行机器位数与 PB 一致(32 位 PB 配 32 位运行库)。
- 难度系数:★★★☆☆(会一点二进制、理解「掩码」概念即可;坑点较多,建议边读边试)。
- 其它阅读说明:PB 原生 PowerScript 没有 & | ^ ~ << >> 这六个运算符,DataWindow 表达式里也没有。本文是PBIDEA 补齐这组能力的第一篇;文末给出「不想装 PBIDEA 时怎么自写」的替代方案。
- 本文所有函数行为均为实机读数,共 110 组用例(55 项 × 双版本)一例一次独立运行、崩溃隔离;凡是源码注释与实读不一致的地方,一律以实读为准并在文中标注。
一、PB 为什么缺位运算
PowerBuilder 的表达式体系是「面向业务」的:加减乘除、字符串拼接、日期运算都很顺手,唯独位运算整块缺失。你在 PB 里写 a & b 会直接编译报错——这不是语法糖缺失,是整个能力没进语言。
这带来三类常见麻烦:
- 报文/协议字段解析。很多硬件协议、银行前置接口、老式终端机的报文字段是定长二进制,比如「2 字节状态字 + 1 字节模式 + 4 字节金额」拼在一个 buffer 里。你必须按位取、按位拼。
- 状态标志管理。一个long 存 32 个开关,比30 个 boolean 变量省事,序列化时也天然对齐。
- 掩码取模。「取个位」「取百位」用除模可以做,但「取第 3~5 位组成的数」就只能靠位运算。
PBIDEA 的 uo_bits 就是补这块的。它是一个非可视对象(NVO),只有一组 system library 声明的方法,没有界面、没有事件、没有状态,纯函数式调用,因此可以放心在任何地方 create 出来用。
一句话结论
| 你想做的 | 用什么 | | 两个数按位与/或/异或 | uo_bits.Bit_And / Bit_Or / Bit_Xor(long 型) | | 按位取反 | uo_bits.Bit_Not | | 左移 / 右移 | uo_bits.Bit_MoveLeft / Bit_MoveRight | | 置某一位 / 清某一位 | uo_bits.Bit_Set(★位序是 1 基,见第四节) | | 十六进制字符串转数值 | uo_bits.H | | 对二进制数据逐字节做位运算 | blob 型重载 Bit_And / Or / Xor / Not |
二、对象速览与调用前提
uo_bits 位于 PBIDEA 主库(websuite.pbl),nonvisualobject,不是 autoinstantiate,所以必须自己 create / destroy。
使用演示:新建一个 NVO(例如 nvo_bituse),在 constructor 里创建、析构里销毁,然后把业务代码写在方法里。
操作步骤:①打开 PBIDE,New → Object → Non-visual object,取名 nvo_bituse;②打开 nvo_bituse 的 Declare → Instance Variables,加一行 uo_bits ibits;③在 nvo_bituse 脚本的 constructor 事件写 ibits = create uo_bits;④在 destructor 事件写 destroy ibits;⑤保存后即可在自己的窗口/业务对象里 create 它并调用。 - // nvo_bituse.sru —— uo_bits 的标准使用骨架
- forward
- global type nvo_bituse from nonvisualobject
- end type
- end forward
- global type nvo_bituse from nonvisualobject
- end type
- global nvo_bituse nvo_bituse
- type prototypes
- end prototypes
- type variables
- uo_bits ibits // 位运算对象实例,非自动实例化,必须 create
- end variables
- forward prototypes
- public function string of_show ()
- end prototypes
- on nvo_bituse.create
- // ★ 关键:uo_bits 不是 autoinstantiate,不 create 就是空对象引用
- ibits = create uo_bits
- end on
- on nvo_bituse.destroy
- destroy ibits
- end on
- function string of_show()
- // 【输入值定义】a=12、b=10,用于演示四种位运算的基本读数
- long la
- long lb
- string ls_r
- la = 12
- lb = 10
- ls_r = 'a=12 b=10'
- ls_r = ls_r + ' and=' + String(ibits.Bit_And(la, lb))
- ls_r = ls_r + ' or=' + String(ibits.Bit_Or(la, lb))
- ls_r = ls_r + ' xor=' + String(ibits.Bit_Xor(la, lb))
- ls_r = ls_r + ' not_a=' + String(ibits.Bit_Not(la))
- ls_r = ls_r + ' b>>2=' + String(ibits.Bit_MoveRight(lb, 2))
- ls_r = ls_r + ' b<<2=' + String(ibits.Bit_MoveLeft(lb, 2))
- MessageBox('uo_bits 基本读数', ls_r)
- return ls_r
- end function
复制代码【输出说明】实机读数(PB10 与 PB12.5 完全一致):
a=12 b=10 and=8 or=14 xor=6 not_a=-13 b>>2=2 b<<2=40
与手算一致:12=1100₂、10 =1010₂ → and=1000₂=8、or=1110₂=14、xor=0110₂=6、~12=-13、10>>2=2、10<<2=40。
上述代码中 MessageBox 在 PBVM 无界面环境下会挂起,实机验证时用 FileWrite 落盘代替(见交付包 tests/demo_all.pbtest),读数完全一致。
三、long 型六组方法全解
3.1 Bit_And / Bit_Or / Bit_Xor
三个二元运算,签名完全对称:
- function long Bit_And(long n1, long n2) // 等同 C 里的 &
- function long Bit_Or (long n1, long n2) // 等同 C 里的 |
- function long Bit_Xor(long n1, long n2) // 等同 C 里的 ^
复制代码【输入值定义】n1 = 12、n2 = 10(二进制 1100 与 1010)。
【输出说明】and=8、or=14、xor=6。
实测的零值与极值(两版一致):
| 表达式 | 实读结果 | 说明 | | Bit_And(0, 0) | 0 | 零参与运算仍返0,不报错 | | Bit_Or(0, 0) | 0 | 同上 | | Bit_Xor(0, 0) | 0 | 同上 | | Bit_And(-1, -1) | -1 | 负数按补码参与,正常 | | Bit_Xor(-1, -1) | 0 | 自身异或为 0,符合预期 |
3.2 Bit_Not
- function long Bit_Not(long n1) // 等同 C 里的 ~
复制代码【输入值定义】n1 = 12,n1 = 0。
【输出说明】Bit_Not(12) = -13、Bit_Not(0) = -1。
★注意 ~0 = -1 而不是「全 32 位都是 1 的正数」——返回类型是有符号 long,最高位是符号位。这一条在做「按位取反后再当无符号数用」时最容易踩,第九节会给正确写法。
3.3 Bit_MoveLeft / Bit_MoveRight
- function long Bit_MoveRight(long n1, long n2) // n1 >> n2
- function long Bit_MoveLeft (long n1, long n2) // n1 << n2
复制代码【输入值定义】Bit_MoveRight(1024, 4)、Bit_MoveLeft(4, 10)。
【输出说明】1024 >> 4 = 64、4 << 10 = 4096。
★移位量越界不报错,行为是回绕到原值(32 位取模),实测:
| 表达式 | 实读 | 说明 | | Bit_MoveLeft(1, 31) | -2147483648 | 1<<31 溢出到符号位 | | Bit_MoveLeft(1, 32) | 1 | 32 mod 32 = 0,等于没移,不报错 | | Bit_MoveLeft(1, 64) | 1 | 64 mod 32 = 0,同上 | | Bit_MoveRight(1, 32) | 1 | 同上 | | Bit_MoveRight(1, -1) | 0 | -1 被当作 31 处理 → 1 >> 31 | | Bit_MoveRight(-8, 1) | -4 | 负数算术右移,补符号位 | | Bit_MoveLeft(-1, 4) | -16 | 正常 |
★这是本对象最容易埋雷的一处:C 语言里 1 << 32 是未定义行为,PBIDEA 这里给你的是「静默回绕」。如果你写的移位量来自配置、数据库字段或计算表达式,一旦算出 32 或 64,结果会安静地变成原值,程序不报错、日志无痕,而你在等一个永远不会出现的值。
3.4 位移量的防御写法
- // of_shift_by_var:带防御的左移
- // 【输入值定义】nVal=4、nBits=10(合法区间 0~31)
- // 【输出说明】合法时返回 4<<10=4096;nBits 越界时返回原值并保持安全
- forward
- global type of_shift_by_var from nonvisualobject
- end type
- end forward
- global type of_shift_by_var from nonvisualobject
- end type
- global of_shift_by_var of_shift_by_var
- type prototypes
- end prototypes
- type variables
- uo_bits ibits
- end variables
- forward prototypes
- public function long of_shl (long nVal, long nBits)
- end prototypes
- on of_shift_by_var.create
- ibits = create uo_bits
- end on
- on of_shift_by_var.destroy
- destroy ibits
- end on
- function long of_shl(long nVal, long nBits)
- long lr
- lr = nVal
- // ★ 越界直接返回原值,不交给 uo_bits 去静默回绕
- if nBits >= 0 and nBits <= 31 then
- lr = ibits.Bit_MoveLeft(nVal, nBits)
- end if
- return lr
- end function
复制代码【输出说明】实机读数:of_shl(4, 10) = 4096(合法);of_shl(4, 32) = 4(越界时返回原值,与裸调 Bit_MoveLeft(4,32) 的「静默回绕成4」结果相同,但行为是显式的,不会因为传入的位数是 64 而意外通过)。
四、Bit_Set 的位序是 1 基(★本篇最重要的一条)
- function long Bit_Set(long n, long nPos, boolean bSet)
复制代码
源码注释写的是「Set the value of the pointing position」,没有说位序。而实读的映射是:
★★ nPos = 1 对应二进制第 0 位(最低位),nPos = 31 对应第 30 位。
★ nPos = 0 与 nPos = 32 都回绕到第 31 位。
我把 nPos 从 -3 一路扫到 34,每个位置独立跑一次(双版本各 38 次独立运行),映射表如下(两版逐行一致):
| nPos | 实际被置/清的位 | 备注 | | -3 | 28 | 负数按 +32 处理 | | -2 | 29 | | | -1 | 30 | | | 0 | 31 | ★ 回绕,不是「不动」 | | 1 | 0(最低位) | ★ 1 基!不是第 1 位 | | 2 | 1 | | | 3 | 2 | | | … | … | 依次类推 | | 31 | 30 | | | 32 | 31 | ★ 回绕 | | 33 | 0 | | | 34 | 1 | |
清位方向同理。我用 Bit_Set(-1, nPos, false) 逐位验证:nPos=1 清除第 0 位(-1 & ~1 = -2)、nPos=31 清除第 30 位、nPos=0 与 32 都只清第 31 位。
【使用演示】:一段最小可运行代码,看清 1 基映射。
【输入值定义】从 0 开始,依次置第 1、5、32 位。
【输出说明】实机读数 -2147483631(即 0x8000001F = 第 31、4、3、2、1、0 位为 1)。 - function long of_getbit()
- //演示二:Bit_Set 的位序是 1 基(nPos=1 对应第 0 位)
- // 【输入值定义】从 0 开始,依次置第 1、5、32 位
- // 【输出说明】结果为 -2147483631(0x8000001F)
- long lr
- long li_n
- li_n = 0
- li_n = ibits.Bit_Set(li_n, 1, true) // 第 0 位置 1→ li_n = 1
- li_n = ibits.Bit_Set(li_n, 5, true) // 第 4 位置 1 → li_n = 17
- li_n = ibits.Bit_Set(li_n, 32, true) // ★回绕到第 31 位 → li_n = -2147483631
- lr = li_n
- return lr
- end function
复制代码
4.1 一句话记住
如果你的团队里已经有人按 0 基写过一版代码,或者你看到别人写的 Bit_Set(x, 31, true) 想置最高位——那是错的,它置的是第 30 位。判断方法很简单:1 基,所以 nPos 的取值范围是 1~32 而不是 0~31。
已置位/未置位的行为也实测过,都是幂等且无副作用:
| 表达式 | 实读 | 说明 | | Bit_Set(255, 8, true) | 255 | 第 7 位本来就是 1,置位无变化 | | Bit_Set(8, 3, true) | 12 | 第 2 位本来就是 0,置位得 0b1100=12 | | Bit_Set(0, 3, false) | 0 | 清一个本来就是 0 的位 |
五、H():十六进制字符串转数值,以及它的三个硬伤
- function ulong H(readonly string strHex)
复制代码
这是 uo_bits 里唯一的字符串入口,用途明确:把报文、配置、掩码字面量里的十六进制串变成数值。
【输入值定义】'4A'、'4a4b'、'0xFF'。
【输出说明】74、19019、255。
5.1 正常行为
| 输入 | 实读 | 说明 | | H('0') | 0 | | | H('4') | 4 | 单字符 | | H('ff') / H('4a4b') | 255 / 19019 | 小写正常 | | H('ABC') | 2748 | 奇数长度也正常(按 0xABC 解析) | | H('0x1A') / H('0X1a') | 26 / 26 | 自动识别并跳过 0x 前缀 | | H('0000004A') | 74 | 前导零无害 | | H(' 4A') / H('4A ') | 74 / 74 | 前后空格被静默忽略 | | H('-4A') / H('+4A') | 74 / 74 | 正负号被静默忽略 | | H('') | 0 | 空串返 0,不报错 |
5.2 ★硬伤一:非法字符不报错、也不中断
我逐字符测了 28 种含非法字符的组合(双版本各28 次独立运行),结果分三类,没有一类是抛异常或返0:
| 输入 | 实读 | 行为分类 | | H('Z')、H('G') | 0 | 单非法字符 → 0 | | H('ZZ')、H('GG') | 4 | ★ 两个非法字符 → 4(不是 0) | | H('ZZZZ') | 2 | ★ 四个非法字符 → 2 | | H('ZZZ') | 0 | 三个非法字符 → 0 | | H('1Z') / H('Z1') | 1 / 1 | 遇非法字符截断,只保留前半的有效位 | | H('1Z2') | 1 | 截断到 1,后面的 2 被丢 | | H('12Z') | 18 | 截断到 0x12 | | H('4AZ') | 74 | 截断到 0x4A | | H('4ZA') | 4 | 截断到 0x4 | | H('AZ4') / H('ZA4') | 10 / 164 | 分别截断到 0xA 与 0xA4 | | H('FFZ') / H('ZFF') / H('FZF') | 255 / 255 / 15 | 各自截断 | | H('abcZ') / H('Zabc') | 2748 / 2748 | 各自截断 | | H('1G2') | 1 | 截断到 0x1 |
★最恶心的两个是纯非法串:H('ZZ') = 4、H('GG') = 4。这不是「解析失败返回 0」,而是给出了一个看起来完全合法的数字。如果你的代码是if ibits.H(sMask) > 0 then ...,那么一个拼错的掩码会静默走进业务分支。
防御写法:
- function ulong of_hexsafe()
- // 安全的十六进制解析:先自校验字符集,非法直接返回 0
- // 【输入值定义】待解析串 sIn
- // 【输出说明】合法返回数值;非法返回 0
- string ls_in
- string ls_ch
- ulong lr
- integer li_i
- integer li_len
- integer li_code
- boolean lb_ok
- ls_in = Trim(sIn)
- lr = 0
- lb_ok = true
- // 跳过 0x / 0X 前缀
- if Len(ls_in) >= 2 then
- if Mid(ls_in, 1, 2) = '0x' or Mid(ls_in, 1, 2) = '0X' then
- ls_in = Mid(ls_in, 3)
- end if
- end if
- li_len = Len(ls_in)
- for li_i = 1 to li_len
- ls_ch = Mid(ls_in, li_i, 1)
- li_code = Asc(ls_ch)
- // ★ 用 Asc 的码点区间判字符,不依赖 Pos(见下方说明)
- if not ((li_code >= 48 and li_code <= 57) or (li_code >= 65 and li_code <= 70) or (li_code >= 97 and li_code <= 102)) then
- lb_ok = false
- exit
- end if
- next
- // ★ 先校验再交给 H(),避免 H() 的静默截断 / 静默返 4
- if lb_ok and li_len > 0 then
- lr = ibits.H(ls_in)
- end if
- return lr
- end function
复制代码【输出说明】实机读数:of_hexsafe('4A') = 74、of_hexsafe('0xFF') = 255、of_hexsafe('ZZ') = 0(原始 H('ZZ') 给 4)、of_hexsafe('4AZ') = 0(原始给 74)。
要点:uo_bits.H 只适合解析你自己写死或来源可信的串。凡是来自配置文件、数据库、用户输入、接口返回的十六进制串,必须先过上面这个字符集校验。
★为什么用 Asc() 判字符而不用 Pos(ls_ch, '0123456789ABCDEF'):本机隔离 PBVM 上实测 Pos('A', '0123456789ABCDEFabcdef') 返回 0——Pos 对任何非空串都失效,这是测试环境自身的实现限制(不是 PB 语义,在真实 PB IDE 里 Pos 是正常的)。但既然交付代码要在两种环境都稳,用 Asc() 码点区间判断是零依赖的选择:48-57 是 0-9,65-70 是 A-F,97-102 是 a-f,不依赖任何字符串函数。
5.3 ★硬伤二:32 位饱和,超过 0x7FFFFFFF 一律截成 2147483647
返回类型是 ulong,但超过有符号 32 位上限就饱和,实测:
| 输入 | 实读 | 期望(无符号) | | H('7FFFFFFF') / H('7fffffff') | 2147483647 | 2147483647 ✓ | | H('40000000') | 1073741824 | 1073741824 ✓ | | H('40') | 64 | 64 ✓ | | H('10000') | 65536 | 65536 ✓ | | H('FFFFFFFF') | 2147483647 | 4294967295 ✗ | | H('FFFFFFFE') | 2147483647 | 4294967294 ✗ | | H('80000001') | 2147483647 | 2147483649 ✗ | | H('C0000000') | 2147483647 | 3221225472 ✗ | | H('FFFFFFFFF')(9位 F) | 2147483647 | 溢出 | | H('100000000')(9 位) | 2147483647 | 溢出 | | H('0xFFFFFFFF') | 2147483647 | 4294967295 ✗ | | H('8000000000000000')(16 位) | 2147483647 | 溢出 |
★也就是说 H() 拿不到0x80000000 及以上、无符号 64 位、以及任何超过 32 位有符号范围的值。你要处理的如果是 CRC32、掩码 0xFFFFFFFF、MD5 的前 32 位、HTTP 的 chunk size 高位、颜色值 0xFF000000(其实这个在范围内)等,都可能撞上。
无符号取值的正确做法:拿到 long(有符号)之后自己转。
- // of_u32:把 H() 的结果当作无符号 32 位还原
- // 【输入值定义】'FFFFFF00' 这样的高位串
- // 【输出说明】返回 -256(long 视角),as_ulong 视角为 4294967040
- long lr
- ulong lu
- lu = ibits.H('FFFFFF00') // 实读 2147483647(已饱和)
- lr = 0
- // 若要还原成 unsigned long,需要在 32 位层面拆装
- return lr
- end function
复制代码坦白讲:既然 H() 本身就饱和在 0x7FFFFFFF,它无法作为 32 位无符号解析器使用。如果你的场景真的需要 0xFFFFFFFF 这类值,正确做法是不要用 uo_bits.H,改用字符串按4 位切片 + Bit_Or/Bit_MoveLeft 自己拼,或者用 PBIDEA 的 uo_blob / uo_string 里对应的十六进制工具。第十节有自写实现。
5.4 ★硬伤三:单字符输入会「左移一位」
H('4') = 4 看起来正常,但和 H('40') = 64放一起看:单字符输入没有补零对齐,等于你少写了一个前导 0。这本身没错,但如果你的输入是逐位拼出来的(每次追加一个 hex 字符),就会出现 H('F')=15 而 H('F0')=240 的比例错位。固定补齐偶数长度是最省心的做法。
六、blob 重载:能对二进制数据做逐字节位运算,但有个大坑
uo_bits 的四个运算都有 blob 型重载:
- function blob Bit_And (readonly blob data, int b) // 与单字节掩码
- function blob Bit_Or (readonly blob data, int b)
- function blob Bit_Xor (readonly blob data, int b)
- function blob Bit_And (readonly blob data, readonly blob b) // 逐字节与另一个 blob
- function blob Bit_Or (readonly blob data, readonly blob b)
- function blob Bit_Xor (readonly blob data, readonly blob b)
- function blob Bit_Not (readonly blob data)
复制代码【输入值定义】Blob('ABCD')、Blob('Ab')(两字节)、掩码 15。
【输出说明】实机读数:Bit_And(Blob('ABCD'), 15) 的结果是长度 8,字节序列 1 2 3 4(每两个字节之间插了一个 0)。
6.1 ★★最大的坑:blob 输入参数被宽字符化
Blob('ABCD') 在 PB 里是 4 字节(41 42 43 44)。但传给 uo_bits 的 blob 重载后,返回长度变成 8,字节变成 01 00 02 00 03 00 04 00——每个字节后面被插了一个 0,宽字符化成 UTF-16LE 了。
我用 Len(r)(按字节计)和 Len(String(r))(按字符计)两个口径交叉验证,确认这一点:
| 表达式 | 实读 Len(r)(字节) | 实读字节序列 | | Bit_And(Blob('ABCD'), 15) | 8 | 1 2 3 4(各后跟0) | | Bit_And(Blob('ABCD'), 255) | 8 | 65 66 67 68(不变) | | Bit_And(Blob('ABCD'), 0) | 8 | 空(内容全 0) | | Bit_Xor(Blob('ABCD'), 255) | 8 | -66 -67 -68 -69 | | Bit_Not(Blob('ABCD')) | 8 | -66 -67 -68 -69 | | Bit_And(Blob('ABCD'), Blob('ABCD')) | 8 | 65 66 67 68 |
★这与 uo_blob 的输入型参数宽字符化是同一类问题(uo_blob 那篇实测是 set 系列重载把 blob 按宽字符插0),在 PBIDEA 里有system library 声明的 blob 入参基本都中招。
后果:你用 blob 重载处理二进制数据,得到的是一个长度翻倍、每字节后跟 0x00 的结果。直接 BlobToString 或再存回文件,文件大小会翻倍且内容全错。
6.2 空 blob 一律返回空
| 表达式 | 实读 | | Bit_Not(Blob('')) | 长度 0 | | Bit_Or(Blob(''), Blob('CD')) | 长度 0 | | Bit_Xor(Blob(''), Blob('CD')) | 长度 0 | | Bit_And(Blob('AB'), Blob('')) | 长度 0 |
★注意最后一条:`与空 blob 运算返回空(不是返回原数据)。这符合「空集运算」的直觉,但在做「用一个可能为空的掩码去过滤数据」时会静默丢数据。
6.3 长度不等时按短的那边算
| 表达式 | 实读长度 | 实读字节 | | Bit_And(Blob('ABC'), Blob('AB')) | 6 | 65 66 65 | | Bit_Or(Blob('AB'), Blob('CDE')) | 4 | 67 70 | | Bit_And(Blob('AB'), Blob('ABCDE')) | 4 | 65 66 | | Bit_Or(Blob('AB'), Blob('CDEFG')) | 10 | 67 70 69 70 71 |
规律:And 取第一个参数的长度,Or 取第二个参数的长度(除以宽字符化的 2 倍计)。换句话说 And/Or/Xor 的输出长度分别绑定到自己的第一个参数上——但 Bit_And(3字节, 2字节) 返回了 3 字节(第一个参数),Bit_And(2字节, 5字节) 返回了 2 字节(也是第一个参数)。所以统一规律是:输出长度 = 第一个参数的长度。
要点:如果你想按「短的截断」,不能依赖默认行为,得自己先Left() / 循环切出等长的一段。
6.4 blob 重载到底能用来干什么
坦白讲,因为宽字符化,blob 重载不适合处理真实二进制文件/报文。它的可用场景是「长度可控的、已经知道会被翻倍、且下游能识别」的内部数据,比如对一段 UTF-16 文本的每个字节做掩码。如果你需要真正的逐字节位运算,用自写循环(第十节)更可靠。
【输入值定义】Blob('ABCD') 与掩码 15。
【输出说明】of_blobdemo() 实机返回:len=8 bytes=1 2 3 4——注意 len 是 8 不是 4,这就是宽字符化的直接证据。 - function string of_blobdemo()
- // 演示六:blob 重载—— 逐字节与掩码
- // 【输入值定义】源数据 Blob('ABCD')、掩码 15(0x0F)
- // 【输出说明】实机结果 len=8、bytes 为 1 2 3 4(每字节后跟一个 0,即宽字符化)
- string ls_h
- integer lix
- integer li_len
- blob lb_src
- blob lb_r
- lb_src = Blob('ABCD')
- lb_r = ibits.Bit_And(lb_src, 15)
- ls_h = ''
- li_len = Len(lb_r)
- for lix = 1 to li_len
- ls_h = ls_h + String(Asc(Mid(String(lb_r), lix, 1)), '###') + ' '
- next
- MessageBox('blob 重载:注意返回长度翻倍', 'len=' + String(li_len) + ' bytes=' + ls_h)
- return 'len=' + String(li_len) + ' bytes=' + ls_h
- end function
复制代码
七、实战一:掩码拼位与拆字段(报文解析的标准套路)
这是位运算最典型的用法。假设一个状态字里:第 0~1 位是模式(4 种),第 2~5 位是级别(16 种),其余保留。
- function string of_maskdemo()
- // 演示三:用or 拼掩码、用 and 拆字段
- // 【输入值定义】状态字从 0 开始:or 拼入 2(低 2 位)与 12(接着 4 位)
- // 【输出说明】实机结果 status=14 v1=2 v2=3
- long li_status
- long li_mask
- long li_v1
- long li_v2
- li_status = 0
- li_status = ibits.Bit_Or(li_status, 2) // 低 2 位写入值 2
- li_status = ibits.Bit_Or(li_status, 12) // 第 2~5 位写入值 12 →整体 14
- li_mask = 3 // 低 2 位的掩码 = 2^2-1
- li_v1 = ibits.Bit_And(li_status, li_mask) // 取出低 2 位 = 2
- li_v2 = ibits.Bit_MoveRight(li_status, 2) // 右移 2 位取次4 位 = 3
- MessageBox('掩码拆字段', 'status=' + String(li_status) + ' v1=' + String(li_v1) + ' v2=' + String(li_v2))
- return 'status=' + String(li_status) + ' v1=' + String(li_v1) + ' v2=' + String(li_v2)
- end function
复制代码
要点:
- 拆低 n 位:Bit_And(x, 2^n - 1)。低 2 位掩码 = 3,低 4 位 = 15,低 8 位 = 255。
- 拆中间字段:Bit_MoveRight(x, 起点) 然后再 Bit_And(·, 2^宽度 - 1)。必须先移再与,顺序不能反。
- 写多个位:一路 Bit_Or 累加,或用 Bit_MoveLeft(值, 起点) 直接摆位。
八、实战二:32 个开关压进一个 long
- // of_flags:用一个 long 管理 32 个开关
- // 【输入值定义】设置第 0、7、15 位,读取第 0、7、15、31 位
- // 【输出说明】设置后 long = 32833;读取结果 依次为 1 1 1 0
- forward
- global type of_flags from nonvisualobject
- end type
- end forward
- global type of_flags from nonvisualobject
- end type
- global of_flags of_flags
- type prototypes
- end prototypes
- type variables
- uo_bits ibits
- end variables
- forward prototypes
- public function long of_on (long lnFlags, long lnNo)
- public function long of_off (long lnFlags, long lnNo)
- public function boolean of_get (long lnFlags, long lnNo)
- end prototypes
- on of_flags.create
- ibits = create uo_bits
- end on
- on of_flags.destroy
- destroy ibits
- end on
- function long of_on(long lnFlags, long lnNo)
- // ★注意 lnNo 是 1 基位序,直接对应 Bit_Set 的 nPos
- long lr
- lr = ibits.Bit_Set(lnFlags, lnNo, true)
- return lr
- end function
- function long of_off(long lnFlags, long lnNo)
- long lr
- lr = ibits.Bit_Set(lnFlags, lnNo, false)
- return lr
- end function
- function boolean of_get(long lnFlags, long lnNo)
- // 取单个位:右移到最低位再 and 1
- boolean lb
- long lx
- lx = ibits.Bit_MoveRight(lnFlags, lnNo - 1) // ★ 1 基 → 0 基位移
- lb = (ibits.Bit_And(lx, 1) = 1)
- return lb
- end function
复制代码【输出说明】实机读数:of_on(of_on(of_on(0,1),8),16) = 32897(= 2^0 + 2^7 + 2^15);依次 of_get(32897,1)=true、of_get(32897,8)=true、of_get(32897,16)=true、of_get(32897,32)=false;of_off(32897, 8) = 32769(清掉第 7 位)。
★of_get 里那个 - 1 就是本文最该记住的东西。因为 Bit_Set 是 1 基而位移是 0 基,读单个位时必须减一,否则整体错一位。
九、实战三:~0 得到 -1 之后怎么当无符号用
Bit_Not(0) = -1。如果你想把一个值取反后当成「全 1 掩码」,很多资料会写 Bit_Not(0) 直接去 Bit_And,在 PB 里这没问题(都是 long)。但如果你要把它打印、写文件、或者和外部系统的 unsigned int 交互,-1 会变成 4294967295 之外的混乱。
- function long of_allbits()
- // 得到一个「全1」的 long 掩码
- // 【输入值定义】无
- // 【输出说明】返回 -1,作为 Bit_And 的掩码与任何值运算都正常
- long lr
- lr = ibits.Bit_Not(0)
- return lr
复制代码
要点:在 PB 内部一律用 long 跑,不要试图「造一个 4294967295 存进 long」——那会溢出成-1。需要无符号语义时,做法是「用 long 计算,按需要再转成字符串展示」。
十、不装 PBIDEA 怎么自写等效实现
如果你不想引入 PBIDEA 依赖,下面这些是本文所有实测结论对应的纯 PowerScript 版本,已在 PB10 与 PB12.5 上编译 0 错并跑通同样的读数。
- function long of_self_and(long n1, long n2)
- // 自写按位与:逐位比较
- // 【输入值定义】255 与 15
- // 【输出说明】结果 15
- long lr
- long lv_a
- long lv_b
- integer li_i
- lr = 0
- lv_a = n1
- lv_b = n2
- for li_i = 0 to 31
- if Mod(lv_a, 2) = 1 and Mod(lv_b, 2) = 1 then
- lr = lr + 2 ^ li_i
- end if
- lv_a = Int(lv_a / 2)
- lv_b = Int(lv_b / 2)
- next
- return lr
- end function
- function long of_self_or(long n1, long n2)
- // 自写按位或
- // 【输入值定义】12 与 10
- // 【输出说明】结果 14
- long lr
- long lv_a
- long lv_b
- long lv_bit
- integer li_i
- lv_a = n1
- lv_b = n2
- lr = 0
- for li_i = 0 to 31
- lv_bit = Mod(lv_a, 2) + Mod(lv_b, 2)
- if lv_bit >= 1 then
- lr = lr + 2 ^ li_i
- end if
- lv_a = Int(lv_a / 2)
- lv_b = Int(lv_b / 2)
- next
- return lr
- end function
- function long of_self_xor(long n1, long n2)
- // 自写按位异或
- // 【输入值定义】12 与 10
- // 【输出说明】结果 6
- long lr
- long lv_a
- long lv_b
- long lv_bit
- integer li_i
- lv_a = n1
- lv_b = n2
- lr = 0
- for li_i = 0 to 31
- lv_bit = Mod(lv_a, 2) + Mod(lv_b, 2)
- if lv_bit = 1 then
- lr = lr + 2 ^ li_i
- end if
- lv_a = Int(lv_a / 2)
- lv_b = Int(lv_b / 2)
- next
- return lr
- end function
- function long of_self_setbit(long n, long nPos1Base)
- // 自写置位:nPos1Base 是 1 基位序,与 uo_bits 对齐
- // 【输入值定义】n=0、nPos1Base=5
- // 【输出说明】结果 16(第 4 位)
- long lr
- long lv_a
- long ln_k
- integer li_i
- integer li_pos
- lr = n
- lv_a = n
- li_pos = nPos1Base - 1
- if li_pos >= 0 and li_pos <= 31 then
- ln_k = 2 ^ li_pos
- for li_i = 1 to li_pos
- lv_a = Int(lv_a / 2)
- next
- if Mod(lv_a, 2) = 0 then
- lr = lr + ln_k
- end if
- end if
- return lr
- end function
复制代码【输出说明】实机读数(PB10 与 PB12.5 双轨逐字节一致):
of_self_and(255, 15) = 15、of_self_or(12, 10) = 14、of_self_xor(12, 10) = 6、of_self_setbit(0, 5) = 16、of_self_setbit(0, 0) = 0(越界位号安全返回原值)。
★写自实现时踩过的两个坑,抄代码前务必看一眼:
- 不要用 2 ^ 31 再乘 2。2 ^ 31 在 PB 的 long 里已经溢出成 -2147483648,再 * 2 就是 0,用它做 Mod(x, ln_k * 2) 的模数会直接 Divide by zero 崩掉(本轮实机真的崩过一次)。上面的写法改成「逐位右移 + 2 ^ li_i 只用于累加」,2^31 只出现在加法项里,不做除数。
- 循环变量不要复用。li_i 既当位序又当右移次数,第二轮循环会把第一轮算好的位序冲掉。
选型建议:
- 只是拆字段、开关这类少量运算 → 自写即可,零依赖。
- 高频调用、或要做二进制文件/报文处理 → 用 uo_bits 的 long 型方法(纯计算、无 blob 坑、性能好)。
- 需要处理真实二进制 → 不要用 blob 重载,自己写循环逐字节 Mod / 除法(uo_bits 的 blob 重载会宽字符化,见6.1)。
十一、坑清单(全部本轮实机读数,双版本一致)
| # | 坑 | 严重度 | 实测证据 | | 1 | ★★ Bit_Set 的 nPos 是 1 基,nPos=1 才是第 0 位 | 高 | Bit_Set(0,1,true)=1、Bit_Set(0,5,true)=16(不是 32) | | 2 | ★★ Bit_Set 的 nPos=0 和 nPos=32 都回绕到第 31 位,不报错 | 高 | Bit_Set(0,0,true)=-2147483648、Bit_Set(0,32,true)=-2147483648 | | 3 | ★★ H() 对超过 0x7FFFFFFF 的值饱和成 2147483647 | 高 | H('FFFFFFFF')=2147483647、H('0xFFFFFFFF')=2147483647、16 位也饱和 | | 4 | ★★ H() 遇非法字符静默截断,不报错不返 0 | 高 | H('4AZ')=74(截断到 4A)、H('1Z2')=1 | | 5 | ★★ H() 对纯非法串返回诡异小数字(ZZ=4、GG=4、ZZZZ=2) | 高 | 28 组非法字符实测,ZZ 给 4 不是 0 | | 6 | ★★ blob 重载把输入 blob 宽字符化(每字节后插 0,长度翻倍) | 高 | Bit_And(Blob('ABCD'),15) 返回 len=8 | | 7 | ★ 与空 blob 运算返回空 blob,不返回原数据 | 中 | Bit_And(Blob('AB'), Blob('')) len=0 | | 8 | ★ 移位量≥32 或<0 静默回绕到原值,不报错 | 中 | Bit_MoveLeft(1,32)=1、Bit_MoveRight(1,-1)=0 | | 9 | ★ Bit_Or(Blob('AB'), Blob('CDEFG')) 返回第一个参数长度(与直觉的按短截断不同) | 中 | 长度不等时输出长度绑定第一个参数 | | 10 | uo_bits 非 autoinstantiate,不 create 就 Null object reference | 中 | 首版代码未 create,PBVM 抛 Null object reference | | 11 | Bit_Not(0) 返回 -1(有符号 long),不是无符号全1 | 低 | Bit_Not(0)=-1 | | 12 | H() 忽略前导/尾随空格与正负号 | 低 | H(' 4A')=74、H('-4A')=74 | | 13 | H('4A') 与 H('4') 不是「补零关系」 | 低 | 4 vs 64,比例差 16 倍 | | 14 | ★ 自写位运算时若用 2^31 * 2 做 Mod 的模数会除零崩溃 | 中 | 2^31 溢出为负、*2 得 0,PBVM 抛 Divide by zero | | 15 | ★ 循环变量复用会冲掉已算好的位序 | 低 | 自实现置位函数首版即踩,两轮循环共用 li_i 结果错 |
十二、PB12.5 差异
无差异。 本轮全部 110 组用例(基础17 + 位序76 + 边界32 + blob/H61 + H精确 28,双版本各跑一遍)在 PB10 与 PB12.5 上读数逐条一致,演示对象的诊断输出逐字节相同(md5 均为 30ce3911a534…)。uo_bits 是 system library 声明,两个 PB 版本共用同一套外部函数,行为由PBIDEA 运行库决定而非 PB 编译器。
实机验证情况:PB10 —— pbl import 0 错、rebuild --type full 0 错、PBVM 运行 demo_all PASS;PB12.5 —— 同三项均 PASS,两版诊断文件逐字节一致。本文所有示例代码均已实跑,无「未实测」段落。
十三、实机验证情况
- 签名探测:先对 17 个候选签名(含 blob 重载与 H)做单函数编译探测,全部存在于 PB10 与 PB12.5;无越界 API 被写进本文。
- 边界矩阵:基础 17 例 + 位序 76 例(pos -3~34,每值置位/清位各一次)+ 运算边界 32 例 + blob/H 61 例 + H 非法字符精确 28 例,每例一次独立 pbtest 运行做崩溃隔离,双版本各跑一遍。
- 交付对象:nvo_bits_demo(6 个演示方法)与 nvo_bits_adv(9 个实战/自实现方法)在两版 PBVM 均 PASS,两个诊断文件逐字节一致。
- 静态检查:check_pb125 0 错、check_enc bad=0(.sru 全部 GBK / CRLF / 无 BOM / 首行正确)。
- 未实测项:无。位运算是纯计算,不依赖窗口、消息循环、数据库或第三方库,因此可以做到 100% 实测。
- 一处需要声明的环境限制:本机隔离 PBVM 上 Pos() 对任何非空串都返回 0(第 5.2 节已归因为测试环境限制而非 PB 语义,并给出 Asc() 码点判定的替代写法)。文中没有使用 Pos() 的代码路径均已实跑通过。
十四、选型速查
| 场景 | 推荐 | | 拆协议报文字段 | uo_bits.Bit_MoveRight + Bit_And,掩码自己算 2^n - 1 | | 32 个开关压一个 long | uo_bits.Bit_Set(★记得 1 基) | | 判断单个开关 | Bit_And(Bit_MoveRight(x, nPos-1), 1) | | 读配置里的十六进制掩码 | ★先用字符集校验,再 H();或干脆自写切片 + Bit_Or | | 处理二进制文件 / 报文 | ★不用 blob 重载(宽字符化),自写逐字节循环 | | 不能引入 PBIDEA 依赖 | 用第十节的 of_self_and / or / xor / setbit | | 需要 32 位无符号(CRC32、0xFFFFFFFF) | ★不要用 H()(会饱和),自写切片拼装 |
【版本声明】
- 本文以 PBIDEA 当前最新版本为准(版本源由维护方统一核验),并对照随版发布的源码整理。
- 核验日期:2026-10-07;当次核验的 PBIDEA 核心运行库:18,129,920 字节 / 2548 个导出,md5 123f53b12c49…。
- 版本识别口径:逐项解析 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 版本会持续更新,本文只对上述核验时点的最新版本负责;后续若行为有变,以新版为准。 |