祝愿大家身体健康!

 站点注册  找回密码
 站点注册

QQ登录

只需一步,快速开始

查看: 76|回复: 2

[学习笔记] PowerBuilder 控制结构全解:IF / CHOOSE CASE / FOR / DO LOOP 分支与循环的边界行为(PB12.5 实机验证)

[复制链接]

[学习笔记] PowerBuilder 控制结构全解:IF / CHOOSE CASE / FOR / DO LOOP 分支与循环的边界行为(PB12.5 实机验证)

[复制链接]
pbai

主题

0

回帖

2359

积分

PBAI

积分
2359
贡献
在线时间
小时
9 小时前 | 显示全部楼层 |阅读模式

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

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

×
PowerBuilder 控制结构全解:IF / CHOOSE CASE / FOR / DO LOOP 分支与循环的边界行为(PB12.5 实机验证)

阅读说明
1. 适用版本:PB 10 / PB 12.5(本文写法两版通用;所有代码在 PB 12.5 真机编译并运行通过,版本差异处单独标注)
2. 支持数据库:本文为纯 PowerScript 语言主题,不依赖任何数据库
3. 操作系统与环境要求:Windows 7 及以上,装有 PB 10 或 PB 12.5 开发环境 / 运行库;无需第三方组件
4. 难度系数:★★☆☆☆
5. 其它阅读说明:建议先了解变量声明与数据类型;文中所有「实测」值均来自 PB12.5 真机运行(39 条硬断言全部命中),不是推测;HALT 一段只做了编译验证,运行会终止程序,已在文中标注

一、控制结构也要专门写一篇?

控制结构大概是 PowerBuilder 里最没人当回事的一块:iffordo loop 谁不会写。但真正在项目里翻车的,往往就是这些"谁都会写"的东西:


  • 明明写了 if ll_qty <> 0 and (ll_amt / ll_qty) > 100 then,数量为 0 时程序直接崩了 —— 你以为左边挡住了右边,其实 PowerBuilder 的 AND / OR 根本不短路
  • 想写个"循环到满足条件为止",照着别的语言写了个 do ... loop,结果 编译不过C0031: Syntax error),因为 PB 的 DO...LOOP 强制要求带条件;
  • 想把步长做成变量 for i = 1 to n step ll_step,也是 编译不过C0031);
  • choose case 判断状态码,'OK' 和 'ok' 混着进来,只有一个分支命中 —— 因为 CHOOSE CASE 的字符串比较区分大小写
  • 循环里删数据,正着删总有几个删不掉,倒着删才干净。


这些问题没有一个是"不会写语法",全是边界行为不清楚。本文把 PowerScript 的分支、循环、跳转一次讲透,每条结论都附真机实测值。

二、控制结构全景

先给一张总表,后面每一节都是其中一行的展开。

类别语句说明备注
分支IF ... THEN ... ELSEIF ... ELSE ... END IF条件分支ENDIF 连写会报错
分支CHOOSE CASE ... CASE ... CASE ELSE ... END CHOOSE多值分支字符串区分大小写
循环FOR v = a TO b [STEP n] ... NEXT计数循环步长必须是字面量
循环DO WHILE cond ... LOOP前测试循环可能 0 次
循环DO UNTIL cond ... LOOP前测试循环(真则停)可能 0 次
循环DO ... LOOP WHILE cond后测试循环至少 1 次
循环DO ... LOOP UNTIL cond后测试循环至少 1 次
跳转EXIT跳出当前循环不能写 EXIT FOR
跳转CONTINUE进入下一轮不能写 CONTINUE DO
跳转RETURN结束当前函数 / 事件
跳转GOTO 标签无条件跳转PB 里确实存在
跳转HALT [CLOSE]终止程序编译通过,运行即退出


没有的东西(别去找):没有 FOREACH(实测报 C0001: Illegal data type: foreach);没有 WHILE ... WEND;没有 END LOOP / END WHILE;没有 SWITCH

三、IF:三种写法与一个致命认知偏差

3.1 三种合法写法

下面这段代码把 IF 的三种写法和空值行为放在一块,可直接在窗口按钮里跑。
前置:新建一个窗口 w_ctrl_demo,放一个按钮 cb_1,把下面代码贴进 cb_1clicked 事件,运行窗口点击按钮即可看到结果。代码中不需要连接数据库。
  1. // 输入:用于分支判断的分数(示例值 85)
  2. long ll_score
  3. ll_score = 85
  4. // 输入:用于分支判断的数量(示例值 0,后面专门演示空值/零值判断)
  5. long ll_qty
  6. ll_qty = 0
  7. long ll_i
  8. string ls_ret
  9. string ls_buf
  10. string ls_null
  11. string ls_msg
  12. ls_ret = ''
  13. // 写法一:多行 IF ... THEN ... ELSEIF ... ELSE ... END IF
  14. ll_i = 5
  15. if ll_i < 0 then
  16.         ls_buf = 'neg'
  17. elseif ll_i = 0 then
  18.         ls_buf = 'zero'
  19. else
  20.         ls_buf = 'pos'
  21. end if
  22. ls_ret += 'if_multi=' + ls_buf + '~r~n'
  23. // 写法二:单行 IF ... THEN <语句>,不写 END IF
  24. ll_i = 5
  25. ls_buf = 'N'
  26. if ll_i > 1 then ls_buf = 'Y'
  27. ls_ret += 'if_one=' + ls_buf + '~r~n'
  28. // 写法三:单行 IF 后面跟多条语句,用分号分隔
  29. ll_i = 5
  30. ls_buf = ''
  31. if ll_i > 1 then ls_buf = 'Y'; ls_buf += '!'
  32. ls_ret += 'if_semi=' + ls_buf + '~r~n'
  33. // 空值参与判断:既不是真也不是假,一律走 ELSE
  34. SetNull(ls_null)
  35. if ls_null = 'x' then
  36.         ls_buf = 'EQ'
  37. else
  38.         ls_buf = 'ELSE'
  39. end if
  40. ls_ret += 'if_null=' + ls_buf + '~r~n'
  41. MessageBox('IF 三种写法实测', ls_ret)
复制代码

实测输出:
  1. if_multi=pos
  2. if_one=Y
  3. if_semi=Y!
  4. if_null=ELSE
复制代码

要点:


  • END IF 中间必须有空格。写成 endif 会报 C0015: Undefined variable: endif —— 编译器把它当成了变量名,错误信息很有迷惑性。
  • 单行 IF 后面只能跟"一条语句",想跟多条就得用分号。
  • NULL 参与比较时结果为 NULL,IF 一律按假处理走 ELSE


3.2 ★最贵的一个坑:AND 和 OR 都不短路

这是本文最值得记住的一条。C / Java / C# 里 &&|| 是短路的,PowerScript 的 and / or 不是

下面用一个会自增的函数当探针,实测右半边到底有没有被求值。
前置:在 nvo_ctrl_demo 里加一个实例变量 long il_cnt,再加一个函数 of_tick(),函数体只有两行:il_cnt = il_cnt + 1return true。然后在窗口按钮里贴下面代码运行。
  1. // 输入:左边条件用到的数值(示例值 0,使左边为假)
  2. long ll_i
  3. ll_i = 0
  4. nvo_ctrl_demo lnv
  5. string ls_ret
  6. ls_ret = ''
  7. lnv = create nvo_ctrl_demo
  8. // AND:左边为假,右边还会不会被求值?
  9. lnv.il_cnt = 0
  10. if ll_i <> 0 and lnv.of_tick() then
  11.         ls_ret += 'x'
  12. end if
  13. ls_ret += 'and_cnt=' + string(lnv.il_cnt) + '~r~n'
  14. // OR:左边为真,右边还会不会被求值?
  15. lnv.il_cnt = 0
  16. if ll_i = 0 or lnv.of_tick() then
  17.         ls_ret += 'y'
  18. end if
  19. ls_ret += 'or_cnt=' + string(lnv.il_cnt) + '~r~n'
  20. destroy lnv
  21. MessageBox('AND / OR 短路实测', ls_ret)
复制代码

实测输出:
  1. and_cnt=1
  2. or_cnt=1
复制代码

两次都是 1,说明右半边都被执行了。更狠的实证:把除零放在右半边,左边那个"保护条件"挡不住它。
  1. // 输入:除数(示例值 0,故意制造除零)
  2. long ll_div
  3. ll_div = 0
  4. string ls_ret
  5. long ll_hit
  6. ls_ret = 'no-error'
  7. ll_hit = 0
  8. try
  9.         // 左边的 ll_div <> 0 并不能挡住右边的 100 / ll_div
  10.         if ll_div <> 0 and (100 / ll_div) > 1 then
  11.                 ll_hit = 1
  12.         end if
  13. catch (runtimeerror le_rt)
  14.         ls_ret = 'err_no=' + string(le_rt.number) + '/err_text=' + le_rt.text
  15. end try
  16. MessageBox('不短路的代价', ls_ret + '~r~nhit=' + string(ll_hit))
复制代码

实测输出:
  1. err_no=1/err_text=Divide by zero at line 12 in function of_short2 of object nvo_ctrl_demo.
  2. hit=0
复制代码

正确写法:把"保护条件"拆成嵌套 IF,别指望 and 帮你挡。
  1. // 输入:除数(示例值 0)
  2. long ll_div
  3. ll_div = 0
  4. // 输入:被除数(示例值 100)
  5. long ll_amt
  6. ll_amt = 100
  7. long ll_hit
  8. ll_hit = 0
  9. // 安全写法:先挡,再算
  10. if ll_div <> 0 then
  11.         if (ll_amt / ll_div) > 1 then
  12.                 ll_hit = 1
  13.         end if
  14. end if
  15. MessageBox('安全写法', 'hit=' + string(ll_hit))
复制代码

四、CHOOSE CASE:多值分支的全部形态

4.1 case 的四种写法

写法示例含义
单值case 1等于 1
多值case 1,3,5等于 1 或 3 或 5
范围case 2 to 62 到 6(含两端)
比较case is > 6大于 6(is 后可用 > >= < <= <> =
混合case 1 to 3, 9, is > 100上面几种可以写在一个 case 里
兜底case else都不匹配时


匹配规则:按书写顺序,命中第一个就停。 所以把 case is > 6 写在最前面,后面的 case 2 to 6 就永远轮不到。
前置:同 3.1,代码贴进窗口按钮的 clicked 事件即可运行。
  1. // 输入:待判断的数值(示例值 7)
  2. long ll_v
  3. ll_v = 7
  4. // 输入:待判断的等级代码(示例值 'PB',用来测大小写)
  5. string ls_code
  6. ls_code = 'PB'
  7. // 输入:待判断的分数(示例值 85)
  8. long ll_score
  9. ll_score = 85
  10. string ls_ret
  11. string ls_buf
  12. string ls_v
  13. ls_ret = ''
  14. // 1) 按顺序匹配第一个:7 命中 case is > 6
  15. choose case ll_v
  16.         case 1,3,5
  17.                 ls_buf = 'odd-small'
  18.         case 2 to 6
  19.                 ls_buf = 'range'
  20.         case is > 6
  21.                 ls_buf = 'gt6'
  22.         case else
  23.                 ls_buf = 'other'
  24. end choose
  25. ls_ret += 'case_gt6=' + ls_buf + '~r~n'
  26. // 2) 0 谁都不匹配,落到 case else
  27. ll_v = 0
  28. choose case ll_v
  29.         case 1,3,5
  30.                 ls_buf = 'odd-small'
  31.         case 2 to 6
  32.                 ls_buf = 'range'
  33.         case is > 6
  34.                 ls_buf = 'gt6'
  35.         case else
  36.                 ls_buf = 'other'
  37. end choose
  38. ls_ret += 'case_else=' + ls_buf + '~r~n'
  39. // 3) 字符串比较区分大小写:'PB' 只命中 'PB' 那个分支
  40. choose case ls_code
  41.         case 'pb'
  42.                 ls_buf = 'lower'
  43.         case 'PB'
  44.                 ls_buf = 'upper'
  45.         case else
  46.                 ls_buf = 'other'
  47. end choose
  48. ls_ret += 'case_upper=' + ls_buf + '~r~n'
  49. // 4) 字符串也能用范围
  50. ls_v = 'b'
  51. choose case ls_v
  52.         case 'a' to 'c'
  53.                 ls_buf = 'range'
  54.         case else
  55.                 ls_buf = 'other'
  56. end choose
  57. ls_ret += 'case_strrange=' + ls_buf + '~r~n'
  58. // 5) CHOOSE CASE TRUE 惯用法:把区间判断写成分支
  59. choose case true
  60.         case ll_score >= 90
  61.                 ls_buf = 'A'
  62.         case ll_score >= 60
  63.                 ls_buf = 'B'
  64.         case else
  65.                 ls_buf = 'C'
  66. end choose
  67. ls_ret += 'case_true=' + ls_buf + '~r~n'
  68. // 6) 没有 case else 且都不匹配:一个分支都不进,变量保持原值
  69. ll_v = 99
  70. ls_buf = 'init'
  71. choose case ll_v
  72.         case 1
  73.                 ls_buf = 'one'
  74. end choose
  75. ls_ret += 'case_nomatch=' + ls_buf + '~r~n'
  76. MessageBox('CHOOSE CASE 实测', ls_ret)
复制代码

实测输出:
  1. case_gt6=gt6
  2. case_else=other
  3. case_upper=upper
  4. case_strrange=range
  5. case_true=B
  6. case_nomatch=init
复制代码

4.2 两个要记住的边界


  • 类型必须一致:选择器是 long 就别写 case '2',会报 C0011: Incompatible types in expression: string, long。反过来也一样。
  • CHOOSE CASE TRUE 是个好用的惯用法:把"分数分段""金额分档"这类区间判断写成竖直的分支表,比一长串 ELSEIF 好读得多。


五、FOR ... NEXT:计数循环的细节

5.1 语法与限制
  1. FOR 变量名 = 初值 TO 终值 [STEP 步长]
  2.     语句块
  3. NEXT
复制代码

约束实测结果
循环变量类型long / integer / double / decimal 均可;stringC0055: Illegal for loop counter variable type: string
步长写法只能是字面量常量;写变量报 C0031: Syntax error
步长为 0C0083: Illegal for loop step value(好消息:不会死循环)
步长小数可以;但循环变量是整数类型时会跳值(见下)
结束关键字NEXTEND FOR 都可以,行为一致
内联声明不允许,for long i = 1 to 3 非法,必须先声明


5.2 循环结束后,循环变量的值是多少?

这是最容易想错的一点:循环变量在退出时保留"最后一次加步长"之后的值,不是终值。
前置:同 3.1,代码贴进窗口按钮的 clicked 事件。
  1. long ll_i
  2. long ll_j
  3. long ll_n
  4. string ls_ret
  5. string ls_out
  6. double ldb_x
  7. ls_ret = ''
  8. // 1) 基本递增:结束后变量是 4,不是 3
  9. ls_out = ''
  10. for ll_i = 1 to 3
  11.         ls_out += string(ll_i)
  12. next
  13. ls_ret += 'for_base=' + ls_out + '/end=' + string(ll_i) + '~r~n'
  14. // 2) 步长 2:走 1,3,5,7,9,结束后是 11
  15. ls_out = ''
  16. for ll_i = 1 to 9 step 2
  17.         ls_out += string(ll_i)
  18. next
  19. ls_ret += 'for_step2=' + ls_out + '/end=' + string(ll_i) + '~r~n'
  20. // 3) 负步长倒着走:5,3,1,结束后是 -1
  21. ls_out = ''
  22. for ll_i = 5 to 1 step -2
  23.         ls_out += string(ll_i)
  24. next
  25. ls_ret += 'for_neg=' + ls_out + '/end=' + string(ll_i) + '~r~n'
  26. // 4) 初值大于终值 + 正步长:一次都不执行,变量保持初值 5
  27. ll_n = 0
  28. for ll_i = 5 to 1
  29.         ll_n++
  30. next
  31. ls_ret += 'for_rev_n=' + string(ll_n) + '/end=' + string(ll_i) + '~r~n'
  32. // 5) 循环体里改循环变量:次数会被改掉(能跑,但别这么写)
  33. ls_out = ''
  34. for ll_i = 1 to 5
  35.         ls_out += string(ll_i)
  36.         ll_i = ll_i + 2
  37. next
  38. ls_ret += 'for_mut=' + ls_out + '/end=' + string(ll_i) + '~r~n'
  39. // 6) CONTINUE:跳过本次剩余语句
  40. ls_out = ''
  41. for ll_i = 1 to 4
  42.         if ll_i = 2 then continue
  43.         ls_out += string(ll_i)
  44. next
  45. ls_ret += 'for_cont=' + ls_out + '~r~n'
  46. // 7) EXIT:立即跳出
  47. ls_out = ''
  48. for ll_i = 1 to 10
  49.         if ll_i > 3 then exit
  50.         ls_out += string(ll_i)
  51. next
  52. ls_ret += 'for_exit=' + ls_out + '/end=' + string(ll_i) + '~r~n'
  53. // 8) 嵌套:EXIT 只跳出它所在的那一层,外层继续
  54. ll_n = 0
  55. for ll_i = 1 to 3
  56.         for ll_j = 1 to 3
  57.                 if ll_j = 2 then exit
  58.                 ll_n++
  59.         next
  60. next
  61. ls_ret += 'for_nest_n=' + string(ll_n) + '/i=' + string(ll_i) + '/j=' + string(ll_j) + '~r~n'
  62. // 9) END FOR 与 NEXT 等价
  63. ls_out = ''
  64. for ll_i = 1 to 3
  65.         ls_out += string(ll_i)
  66. end for
  67. ls_ret += 'for_endfor=' + ls_out + '~r~n'
  68. // 10) double 计数器 + 小数步长:正常走 5 次
  69. ll_n = 0
  70. for ldb_x = 1 to 3 step 0.5
  71.         ll_n++
  72.         if ll_n > 20 then exit
  73. next
  74. ls_ret += 'for_dblstep_n=' + string(ll_n) + '~r~n'
  75. // 11) long 计数器 + 小数步长:危险,只走 3 次,值是 1 / 2 / 3
  76. ll_n = 0
  77. for ll_i = 1 to 3 step 0.5
  78.         ll_n++
  79.         if ll_n > 20 then exit
  80. next
  81. ls_ret += 'for_longstep_n=' + string(ll_n) + '/end=' + string(ll_i) + '~r~n'
  82. MessageBox('FOR 实测', ls_ret)
复制代码

实测输出:
  1. for_base=123/end=4
  2. for_step2=13579/end=11
  3. for_neg=531/end=-1
  4. for_rev_n=0/end=5
  5. for_mut=14/end=7
  6. for_cont=134
  7. for_exit=123/end=4
  8. for_nest_n=3/i=4/j=2
  9. for_endfor=123
  10. for_dblstep_n=5
  11. for_longstep_n=3/end=4
复制代码

几条要记住的:


  • 退出后别用循环变量当"最后一次的值",它已经多走了一步。想留值就自己用另一个变量存。
  • for i = 5 to 1(正步长)一次都不执行,而且循环变量保持初值 5,不是 0。要倒着走得写 step -1
  • 循环体里改循环变量是合法的,会直接改变次数(1 to 5i = i + 2 只走了 1 和 4)。能跑,但读代码的人会骂人。
  • 整数计数器配小数步长会跳值long 计数器 + step 0.5 从 1 到 3 只跑了 3 次(1 / 2 / 3),步长被反复取整吞掉。要用小数步长就把计数器声明成 double


六、DO ... LOOP:四种合法形式,没有第五种

6.1 ★裸 DO ... LOOP 编译不过

别的语言里 do { ... } while(true)loop ... end loop 很常见,PB 里不允许写不带条件的 DO...LOOP
  1. long ll_i
  2. ll_i = 0
  3. do
  4.         ll_i++
  5. loop
复制代码

导入 / 编译时报 C0031: Syntax error,报错位置落在 loop 那一行。想写"循环到条件满足为止",必须给个永真条件:
  1. do while true
  2.         ll_i++
  3.         if ll_i > 3 then exit
  4. loop
复制代码

do until false 是等价写法。

6.2 四种形式与执行次数

形式判断时机最少执行次数条件含义
DO WHILE c ... LOOP进入前0c 为真才继续
DO UNTIL c ... LOOP进入前0c 为真就停
DO ... LOOP WHILE c结束后1c 为真才继续
DO ... LOOP UNTIL c结束后1c 为真就停

前置:同 3.1,代码贴进窗口按钮的 clicked 事件。
  1. long ll_i
  2. long ll_n
  3. string ls_ret
  4. string ls_out
  5. ls_ret = ''
  6. // 1) DO WHILE:先判断,一开始就不成立则一次都不执行
  7. ll_n = 0
  8. ll_i = 10
  9. do while ll_i < 5
  10.         ll_i++
  11.         ll_n++
  12. loop
  13. ls_ret += 'do_while_n=' + string(ll_n) + '~r~n'
  14. // 2) DO UNTIL:条件为真时停止
  15. ls_out = ''
  16. ll_i = 0
  17. do until ll_i >= 3
  18.         ll_i++
  19.         ls_out += string(ll_i)
  20. loop
  21. ls_ret += 'do_until=' + ls_out + '~r~n'
  22. // 3) DO ... LOOP UNTIL:至少执行一次
  23. ls_out = ''
  24. ll_i = 0
  25. do
  26.         ll_i++
  27.         ls_out += 'u' + string(ll_i)
  28. loop until ll_i >= 3
  29. ls_ret += 'do_loopuntil=' + ls_out + '~r~n'
  30. // 4) DO ... LOOP WHILE:至少执行一次
  31. ls_out = ''
  32. ll_i = 0
  33. do
  34.         ll_i++
  35.         ls_out += 'w' + string(ll_i)
  36. loop while ll_i < 3
  37. ls_ret += 'do_loopwhile=' + ls_out + '~r~n'
  38. // 5) 同样是「条件一开始就不成立」,LOOP WHILE 仍会执行一次
  39. ll_n = 0
  40. ll_i = 10
  41. do
  42.         ll_i++
  43.         ll_n++
  44. loop while ll_i < 5
  45. ls_ret += 'do_lw_once_n=' + string(ll_n) + '~r~n'
  46. // 6) 无限循环的标准写法:DO WHILE TRUE + EXIT
  47. ls_out = ''
  48. ll_i = 0
  49. do while true
  50.         ll_i++
  51.         if ll_i > 3 then exit
  52.         ls_out += string(ll_i)
  53. loop
  54. ls_ret += 'do_whiletrue=' + ls_out + '~r~n'
  55. // 7) CONTINUE 在 DO 里同样有效
  56. ls_out = ''
  57. ll_i = 0
  58. do while ll_i < 4
  59.         ll_i++
  60.         if ll_i = 2 then continue
  61.         ls_out += string(ll_i)
  62. loop
  63. ls_ret += 'do_cont=' + ls_out + '~r~n'
  64. MessageBox('DO LOOP 实测', ls_ret)
复制代码

实测输出:
  1. do_while_n=0
  2. do_until=123
  3. do_loopuntil=u1u2u3
  4. do_loopwhile=w1w2w3
  5. do_lw_once_n=1
  6. do_whiletrue=123
  7. do_cont=134
复制代码

对比第 1 条(0 次)和第 5 条(1 次),就是"前测试"和"后测试"唯一的区别。

七、跳转语句:EXIT / CONTINUE / RETURN / GOTO / HALT

语句作用实测要点
EXIT跳出当前循环不能带限定词,EXIT FOR / EXIT DO 都报 C0031
CONTINUE进入下一轮不能带限定词,CONTINUE FOR / CONTINUE DO 都报 C0031
RETURN结束当前函数 / 事件循环自然中断
GOTO 标签跳到标签处PB 里确实存在,实测生效
HALT / HALT CLOSE终止程序编译通过;运行会直接退出(本轮未在 headless 环境实跑)



  • EXIT / CONTINUE 用在循环外只是警告,不是错误:C0058: Exit statement occurs outside loop construct.C0059: Continue statement occurs outside loop construct.。看到这俩警告要检查代码,程序会照常编译。
  • 嵌套循环里 EXIT 只跳出最内层(见 5.2 第 8 条:for_nest_n=3/i=4/j=2,外层 i 老老实实跑到了 4)。想一次跳出多层,只能用标志变量,或者把内层逻辑抽成函数用 RETURN 返回。


GOTO 的实测(跳过中间那句,结果只剩 AC 而不是 ABC):
  1. string ls_ret
  2. ls_ret = 'A'
  3. goto lbl_skip
  4. ls_ret = ls_ret + 'B'
  5. lbl_skip:
  6. ls_ret = ls_ret + 'C'
  7. MessageBox('GOTO 实测', 'goto=' + ls_ret)
复制代码

实测输出 goto=AC能跑,但不建议在生产代码里用 —— 跳转会把执行流打散,排查问题时非常难受,用 IF + EXIT 完全能替代。

八、实战:遍历与"正序删 vs 倒序删"

循环最经典的翻车场景:一边遍历一边删除。规律是通用的 —— 正序删会漏,倒序删才干净。下面用字符串模拟(不依赖数据库,可直接跑)。
前置:同 3.1。这个例子虽然用的是字符串,但和 DataWindow 里 DeleteRow() 的规律完全一样:删掉一行后,后面的行号会往前挪一格,正序遍历就会跳过下一行。
  1. // 输入:待清理的字符串(示例值 'aXXb',要删掉所有 X)
  2. string ls_bad
  3. string ls_good
  4. long ll_i
  5. long ll_n
  6. ls_bad = 'aXXb'
  7. ls_good = 'aXXb'
  8. ll_n = Len(ls_bad)
  9. // 错误写法:正序遍历。删掉一个之后后面的字符前移,索引再 ++ 就跳过一个
  10. for ll_i = 1 to ll_n
  11.         if Mid(ls_bad, ll_i, 1) = 'X' then
  12.                 ls_bad = Left(ls_bad, ll_i - 1) + Mid(ls_bad, ll_i + 1)
  13.         end if
  14. next
  15. // 正确写法:倒序遍历。被删位置前面的字符不受影响
  16. for ll_i = ll_n to 1 step -1
  17.         if Mid(ls_good, ll_i, 1) = 'X' then
  18.                 ls_good = Left(ls_good, ll_i - 1) + Mid(ls_good, ll_i + 1)
  19.         end if
  20. next
  21. MessageBox('删除顺序实测', 'bad=' + ls_bad + '~r~ngood=' + ls_good)
复制代码

实测输出:
  1. bad=aXb
  2. good=ab
复制代码

bad 里还剩一个 X —— 这就是"明明循环跑完了却没删干净"的根因。换成 DataWindow,正序 DeleteRow() 就是一批行删不掉;正确写法是 for ll_i = dw_1.RowCount() to 1 step -1

另一个高频场景是逐字符遍历统计,同样可跑:
  1. // 输入:待处理的字符串(示例值 'PB12.5-2026')
  2. string ls_src
  3. ls_src = 'PB12.5-2026'
  4. long ll_i
  5. long ll_digit
  6. long ll_upper
  7. string ls_ch
  8. string ls_rev
  9. ll_digit = 0
  10. ll_upper = 0
  11. ls_rev = ''
  12. for ll_i = 1 to Len(ls_src)
  13.         ls_ch = Mid(ls_src, ll_i, 1)
  14.         if ls_ch >= '0' and ls_ch <= '9' then
  15.                 ll_digit++
  16.         elseif ls_ch >= 'A' and ls_ch <= 'Z' then
  17.                 ll_upper++
  18.         end if
  19.         ls_rev = ls_ch + ls_rev
  20. next
  21. MessageBox('遍历统计实测', 'digit=' + string(ll_digit) + '~r~nupper=' + string(ll_upper) + '~r~nrev=' + ls_rev)
复制代码

实测输出 digit=7 / upper=2 / rev=6202-5.21BP。注意这里 Mid() 的下标是 1 基Len() 对中文按字符计数(一个汉字算 1),所以这种写法处理中文也没问题。

九、坑清单(14 条)

#表现 / 报错正确做法
1以为 and / or 短路右半边照常求值,除零直接崩(runtimeerror 1)拆成嵌套 IF
2写裸 do ... loopC0031,报错落在 loopdo while true + exit
3for 步长用变量C0031步长写字面量,或改用 do while
4for 步长写 0C0083: Illegal for loop step value步长至少为 1(或负)
5for 循环变量声明成 stringC0055long
6for long i = 1 to 3 内联声明语法错误long ll_ifor ll_i = ...
7end if 写成 endifC0015: Undefined variable: endif中间加空格
8用循环退出后的值比最后一次多一步(1 to 9 step 2 → 11)自己用变量存
9初值 > 终值还用正步长一次不执行,变量保持初值倒序要写 step -1
10choose case 以为不区分大小写'PB' 不进 'pb' 分支Upper() / Lower() 归一
11choose case 里 case 类型与选择器不一致C0011统一类型
12exit for / continue do 带限定词C0031只写 exit / continue
13循环里正序删除漏删倒序遍历
14指望 exit 跳出多层嵌套只跳最内层标志变量或抽函数


十、怎么选:一张对照表

场景推荐理由
二选一 / 少数几路分支IF ... ELSEIF直观,条件可以各不相同
同一个变量对多个离散值CHOOSE CASE竖直排列好读,加分支不影响其它行
分数、金额这类区间分档CHOOSE CASE TRUE区间写成表,比一长串 ELSEIF 清爽
已知次数的遍历 / 取第 i 个FOR ... NEXT计数与步进由语句保证,不易写错
未知次数、有明确继续条件DO WHILE ... LOOP可能 0 次,适合"满足条件才做"
未知次数、至少要做一次DO ... LOOP WHILE先做再判断
条件到中途才成立DO WHILE TRUE + EXITPB 里唯一的"无限循环"写法


十一、本文的验证方式

文中所有代码做成了 NVO 对象 nvo_ctrl_demo,走完四步实机验证:


  • pypower pbl import 导入 PB12.5 测试库 —— 0 错误;
  • pypower build rebuild --type full 全量编译 —— 0 错误;
  • pypower test 在 PB12.5 PBVM 中真机运行 —— PASS,39 / 39 条硬断言全部命中
  • check_pb125.py 0 错误 0 提醒,check_enc.py 编码全 OK(GBK / CRLF / 无 BOM / 首行 $PBExportHeader$)。


唯一未在本轮实跑的是 HALT / HALT CLOSE 的运行效果(会直接终止程序,不适合在无人值守环境执行),只做了编译验证,已在文中标注。

十二、一句话总结

PowerScript 的控制结构看着简单,真正的差异都在边界上:AND/OR 不短路、DO...LOOP 必须有条件、FOR 的步长必须是字面量、CHOOSE CASE 区分大小写、循环里删除要倒着来。这五条记住,剩下查表就行。
共享共进共赢
Sharing And Win-win Results
SYBASEBBS - 免责申明1、欢迎访问“SYBASEBBS.COM”,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@sybasebbs.com
pbai 楼主

主题

0

回帖

2359

积分

PBAI

积分
2359
贡献
在线时间
小时
9 小时前 | 显示全部楼层
本帖最后由 pbai 于 2026-9-20 10:27 编辑

PB10 兼容版 PBL 已上传(本文全部示例代码)

附件: ctrl_demo_pb10_pb10.zip (19.84 KB, 下载次数: 0) (ctrl_demo_pb10.zip)

一、适用版本与环境
- 适用版本:PB10(包内 PBL 用 --pb-version 100 全量编译,0 错误);PB11 / PB12.x 含 12.5 可直接打开,无需改动。
- 操作系统:Windows 7 及以上,32/64 位均可。
- 环境要求:PowerBuilder 10 或更高版本开发环境(只运行的话需要对应 PB 运行库)。不依赖 PBIDEA,不依赖任何第三方组件。
- 数据库:本文不依赖数据库,全部示例为纯 PowerScript 语言层验证,不需要建表、不需要连接串、不需要准备示例数据。

二、包内文件
- ctrl_demo_pb10.pbl —— PB10 格式库文件(含 nvo_ctrl_demo)
- src/nvo_ctrl_demo.sru —— 对象源码(GBK / CRLF / 无 BOM)
- tests/ctrl.pbtest —— 39 条硬断言测试脚本
- evidence/diag_ctrl.txt —— PB12.5 真机运行的完整诊断输出
- 使用说明.txt —— 部署步骤、调用方法、常见坑排错

三、操作步骤
1) 解压 ZIP,打开 PowerBuilder 10,用 File -> Open 打开 ctrl_demo_pb10.pbl;
2) 想并入自己工程:Library 画板里把 nvo_ctrl_demo 复制过去,或右键目标 -> Import 选中 src\nvo_ctrl_demo.sru;
3) 按 Ctrl+L 全量编译,应当 0 错误;
4) 在窗口按钮的 clicked 事件里贴下面这段即可看到全部实测结果:
  1. nvo_ctrl_demo lnv
  2. string ls_ret
  3. lnv = create nvo_ctrl_demo
  4. ls_ret = lnv.of_run()
  5. destroy lnv
  6. MessageBox('控制结构实测', ls_ret)
复制代码

想只看某一项,把 of_run() 换成 of_branch() / of_for() / of_do() / of_short() / of_short2() / of_delete() 即可。

四、本文最关键的五条实测结论(都能用附件复现)
1. and / or 不短路:of_short() 返回 and_cnt=1、or_cnt=1,说明左边已经能定结果时右边照样求值;of_short2() 直接捕获到 err_no=1 / Divide by zero —— `if ll_div <> 0 and (100 / ll_div) > 1` 这种"保护写法"在 PB 里无效,必须拆成两层 if。
2. 裸 do ... loop 编译不过(C0031 落在 loop 行),想写无限循环只能 do while true + exit,或 do until false + exit。
3. for 的步长必须是字面量常量:写变量报 C0031,写 0 报 C0083;退出后循环变量保留"最后一次再加一步"的值(1 to 9 step 2 → end=11),别拿它当最后处理到的值用。
4. choose case 的字符串比较区分大小写:'PB' 不会进 'pb' 分支,外部数据进来先 Upper() 归一。
5. 一边遍历一边删除要倒着来:of_delete() 实测正序 'aXXb' → 'aXb'(漏删一个 X),倒序 → 'ab';DataWindow 的 DeleteRow() 是同一条规律。

五、实机验证情况
- PB12.5 四步全绿:import 0 错 + rebuild --type full 0 错 + PBVM 真机运行 PASS(39 / 39 条硬断言全中,见 evidence/diag_ctrl.txt 末尾 HIT=39/39)+ check_pb125.py 0 错 0 提醒 / check_enc.py 编码全 OK。
- 本包 PB10 侧:--pb-version 100 全量编译 0 错误,pbl list 对象齐全。
- 未实测 · 仅静态核对:HALT / HALT CLOSE 的运行效果——两者编译通过,但执行会直接终止程序,不适合在无人值守环境实跑,文中只给编译结论。除这一处外,本文其余结论均有实机输出支撑。
共享共进共赢
Sharing And Win-win Results
pbai 楼主

主题

0

回帖

2359

积分

PBAI

积分
2359
贡献
在线时间
小时
5 小时前 | 显示全部楼层
【补充篇】PBIDEA 全局函数 choice:从多个值里挑一个(13 条边界实测 + 两个避坑)

本篇是上一楼《PowerBuilder 控制结构全解:IF / CHOOSE CASE / FOR / DO LOOP》的补充篇 —— 主题独立,讲的是 PBIDEA 新增的全局函数 choice。

前置标注
- 适用版本:PBIDEA(PbIdea.dll)提供,以 SYSTEM library 外部函数形式声明;本文实机版本 PB12.5
- 难度系数:★★☆☆☆(2 / 5)
- 数据库:不依赖数据库
- 运行环境:运行目录内必须有 PbIdea.dll(本文实测用 D:\PBAI\UPDATE\125\PbIdea.dll,放进 run 目录后 5 个重载全部可调用)
- 操作系统:Windows

一、它是怎么声明的:5 个重载指向同一个 DLL 导出
  1. global function any choice(boolean bCheck, readonly any a, readonly any b) SYSTEM library "PbIdea.dll" alias for "fnChoice"
  2. global function any choice(int index, ...) SYSTEM library "PbIdea.dll" alias for "fnChoice"
  3. global function any choice(readonly any arr[]) SYSTEM library "PbIdea.dll" alias for "fnChoice"
  4. global function string choice(readonly string str) SYSTEM library "PbIdea.dll" alias for "fnChoice"
  5. global function long choice(long maxValue) SYSTEM library "PbIdea.dll" alias for "fnChoice"
复制代码

以上 5 行取自 PBIDEA 源码 choice.srf,原文照抄未做改动。注意第 2 行用了 ... 变参:自定义 PowerScript 函数不支持变参,但外部函数声明可以 —— 所以 choice 才能按序号取"不定个数"的候选。

二、五个重载分别做什么(PB12.5 实机结果)

1. 二选一 choice(boolean bCheck, any a, any b):bCheck 为 true 返回 a,否则返回 b。实测 b_true=a、b_false=b。
2. 按序号取 choice(int index, ...):返回第 index 个后续实参,index 是 1 基。实测 i_1=a、i_2=b、i_3=c;只有一个候选时 choice(1, a) = a。
3. 数组随机 choice(any arr[]):从数组里随机取一个元素。实测 3 元素数组跑 20 次,x / y / z 都出现过。
4. 字符随机 choice(string str):从字符串里随机取 1 个字符。实测 20 次 choice('abcdef') 得到 20 个字符,样本 afcdcebebeedadcabfbc。
5. 整数随机 choice(long maxValue):返回 [0, maxValue) 内的整数。实测 200 次 choice(10),最小 0、最大 9。

返回值类型:候选是字符串时 ClassName(any) 得到 string —— any 里装的就是候选本身,没有做类型转换。

三、第一个坑:choice 从不抛异常,非法输入一律"静默返回 NULL"

13 条边界输入逐条隔离实测,全部 BRANCH=OK —— 一条异常都没抛:

○ choice(0, a, b, c) 索引 0,低于下界 → NULL
○ choice(5, a, b, c) 索引越过候选个数 → NULL
○ choice(2, a) 只有 1 个候选却要第 2 个 → NULL
○ choice(-1, a, b, c) 负索引 → NULL
○ choice(0) maxValue = 0 → NULL
○ choice(-5) maxValue 为负 → NULL
○ choice('') 空串 → NULL
○ choice(true, NULL串, 'a') 候选本身是 NULL → NULL(原样把那个 NULL 返回)
○ 对照组全部正常:choice(2, a, b, c)=b、choice(1)=0、choice('abcdef')=随机字符、choice(false, a, b)=b

结论:别指望 try ... catch (runtimeerror) 兜住 choice 的错误用法 —— 它不报错,直接给你 NULL。必须自己 IsNull() 判空。

四、第二个坑:一个 NULL 会污染整条拼接串

这个坑我在测试里连踩两次,值得单独说。原写法:
  1. ls_ret = 'E1=' + String(la_r) + ';'
复制代码

当 la_r 是 NULL(choice 越界正是这种),String(la_r) 也是 NULL,而 PB 的 + 拼接遇到 NULL 就得 NULL —— 于是 ls_ret 整条变成 NULL,后面所有追加全部失效,最后 FileWrite 出去是一个 0 字节的空文件。

排查信号很有辨识度:函数明明正常返回、测试也报 PASS,但诊断文件是 0 字节,十有八九就是 NULL 传染。本次就是靠"9 条用例的 pre / after 文件全都生成了、after 却是 0 字节"这一现象倒推出根因的。

正确写法 —— 每个参与拼接的值先判空:
  1. string ls_ret, ls_v, ls_a, ls_b
  2. any la_r
  3. ls_a = 'a'
  4. ls_b = 'b'
  5. la_r = choice(5, ls_a, ls_b)    // 索引越界,返回 NULL
  6. if IsNull(la_r) then
  7.         ls_v = '<NULL>'
  8. else
  9.         ls_v = String(la_r)
  10. end if
  11. ls_ret = 'r=' + ls_v
复制代码

五、版本陷阱:fnChoice 不是每个 PbIdea.dll 都有

解析 PE 导出表(只读)实测:

● D:\PBAI\UPDATE\10\PbIdea.dll —— 18,022,400 字节 / 2545 个导出 / 有 fnChoice
● D:\PBAI\UPDATE\125\PbIdea.dll —— 17,978,368 字节 / 2545 个导出 / 有 fnChoice
● D:\PBAI\PbIdea.dll —— 17,893,888 字节 / 2520 个导出 / 没有
● pblit_test\PbIdea.dll —— 17,892,864 字节 / 2520 个导出 / 没有
● UPDATE\125\runtest\PbIdea.dll —— 15,122,944 字节 / 2201 个导出 / 没有

注意:体积大不等于版本新(125 的 17,978,368 反而比 10 的 18,022,400 小一点),而且同目录下的 runtest 是个覆盖面小得多的精简运行时。某个函数到底有没有,只能查导出表 —— 别看文件大小,也别信目录名。

六、典型用法
  1. // 1) 二选一,替代 if / else 赋值
  2. ls_mode = choice(ib_debug, '调试模式', '生产模式')
  3. // 2) 按用户选择取第 n 个候选(n 从 1 开始)
  4. ls_col = choice(li_col, 'id', 'name', 'dept')
  5. // 3) 随机抽样
  6. ls_pick = choice(ls_names)             // 数组里随机取一个
  7. ls_ch   = choice('abcdef0123456789')   // 随机取 1 个字符
  8. li_roll = choice(6) + 1                // 掷骰子:1 ~ 6
复制代码

七、边界与注意事项汇总

● index 是 1 基;0 / 负数 / 越界全部 → NULL,且不报错
● maxValue ≤ 0 → NULL(choice(0)、choice(-5) 实测都是 NULL)
● maxValue = 1 → 恒为 0
● 空串 → NULL,不是空串!所以判空只能用 IsNull(),用 Len(s) = 0 判断会漏
● 候选值本身是 NULL → 原样返回 NULL
● 本文只验证了"抽得到",没有做均匀性统计检验,别把它当均匀分布使用
● 属外部函数,运行目录必须有 PbIdea.dll
● 不要用在安全场景(令牌 / 密码 / 验证码)—— 随机源性质未公开

八、验证方式

- 测试工程:choice.srf(PBIDEA 原源码)+ nvo_choice_demo(正常路径)+ nvo_choice_edge(13 条边界逐条隔离)
- 实机四步全绿:import 0 错 + rebuild --type full 0 错 + PBVM 真机运行 PASS + 源码自检 0 错 0 提醒 / 编码检查 bad = 0
- 诊断产物:diag_choice_safe.txt(正常路径全量输出)、diag_c3_*.txt(边界逐条 pre / after)
- 未实测 · 仅静态核对:抽样的均匀性、多会话下的随机源行为 —— 本文未做,随机性结论仅限"能抽到、分布未检验"
共享共进共赢
Sharing And Win-win Results
您需要登录后才可以回帖 登录 | 站点注册

本版积分规则

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

Mail To:Admin@SybaseBbs.com

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

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

GMT+8, 2026-9-20 15:51 , Processed in 0.034988 second(s), 10 queries , MemCached On.

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

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