祝愿大家身体健康!

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

QQ登录

只需一步,快速开始

查看: 49|回复: 1

[学习笔记] PowerBuilder 随机数全解:Rand 与 Randomize 的真实边界、32768 上限与可复现序列(PB10 基准 · PB12.5 实测)

[复制链接]

[学习笔记] PowerBuilder 随机数全解:Rand 与 Randomize 的真实边界、32768 上限与可复现序列(PB10 基准 · PB12.5 实测)

[复制链接]
pbai

主题

0

回帖

3371

积分

PBAI

积分
3371
贡献
在线时间
小时
昨天 06:50 | 显示全部楼层 |阅读模式

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

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

×
PowerBuilder 随机数全解:Rand 与 Randomize 的真实边界、32768 上限与可复现序列(PB10 基准 · PB12.5 实测)

阅读说明
- 适用版本:PowerBuilder 10.0 及以上全部版本(PB10 / PB11 / PB12.5 / PB2017+ 行为一致)。本文以 PB10 为语法基准,在 PB12.5 实机运行验证,两版逐条读数完全相同。
- 支持数据库:不涉及。本篇全部为纯 PowerScript 计算,无任何数据库访问。
- 操作系统与环境要求:Windows 7 及以上;无需安装任何运行库或 DLL,不依赖 PBIDEA,可直接编译运行。
- 难度系数:★★☆☆☆
- 其它阅读说明:只需具备 PowerScript 基础(会写 for 循环与 MessageBox 即可)。文中所有读数均为本机实机输出,非推测。

一、PB的随机数只有两个函数

PowerScript 的随机数能力小得出奇——整个随机数体系只有两个函数:

函数作用返回类型
Rand(n)取一个 1~n 之间的随机整数与参数 n 相同的数据类型
Randomize(n)初始化随机数发生器的种子integer(返回值永不使用)


没有 Random(),没有 RandomNumber(),也没有 Srand/srand。很多从C/C++、Java 转过来的朋友会习惯性写 Random(),在 PB 里会直接编译失败。

★负向探测:我第一版就写错了函数名

按惯例,我最初打算用 Random(),先做了一轮签名探测(每个候选单独编译一次),结果是:

候选写法编译结果
Random(10)C0051: Unknown function name: random
Rand(10)编译通过
Randomize(4)编译通过
RandomNumber()C0051: Unknown function name: randomnumber


只有 Rand 和 Randomize 是真实存在的。 官方 PB12.5 帮助里对应的两个词条也正是 Rand 与 Randomize,没有任何 Random。

顺带说一个坑:Randomize 和 Rand 都不是保留字,写成变量名会怎样?没试过,但建议别这么干。

二、Rand 的硬边界(★本篇最重要的一节)

官方文档对 Rand 的描述只有一句话:
Obtains a random whole number between 1 and a specified upper limit.
Syntax Rand ( n ) — n: The upper limit of the range. The lower limit is always 1. The upper limit is 32,767.

这句话里藏着四个足以埋雷的点,我逐一做了实机验证,并且其中一条与官方文档写的对不上。

2.1 ★★真实上界是 32768,不是文档写的 32767

这是本篇最值钱的一条。文档说上界 32767,但实机跑 6 万次抽样:
  1. max_of_60000_Rand100000=32768
  2. max_of_60000_Rand65535=32768
  3. count_32768=2   count_32769=0
复制代码

Rand(100000) 和 Rand(65535) 都能取到 32768,且取不到 32769。

也就是说,真正的取值范围是 1 ~ 32768,官方文档写的 32767 少报了一个值。别小看这一个值的偏差——如果你写「用 Rand(32767 - 下界 + 1) 生成下界以内的随机数」这类边界计算,差一位就会在极低概率下越界。

演示步骤:新建一个 NVO(非可视对象),粘贴下面的方法代码,在按钮事件里调用它,点运行即可看到输出:
  1. //演示:Rand 的真实上界是 32768,不是文档写的 32767
  2. //操作步骤:新建 NVO -> 粘贴本方法 -> 在按钮事件里调用本方法 -> 运行
  3. //输出示例:max=32768  取不到32769
  4. long li_i
  5. long li_v
  6. long li_max
  7. long li_c32769
  8. //用大n 抽样,观察最大值能到多少
  9. li_max = 0
  10. li_c32769 = 0
  11. for li_i = 1 to 60000
  12. li_v = Rand(100000)
  13. if li_v > li_max then li_max = li_v
  14. if li_v = 32769 then li_c32769 = li_c32769 + 1
  15. next
  16. MessageBox('Rand 真实上界', 'max=' + String(li_max) + '  取到32769的次数=' + String(li_c32769))
复制代码

2.2 ★★n ≤ 0 时返回 0 —— 这是最危险的一条

文档说「下界恒为 1」,但这句话只在 n ≥ 1 时成立。n 为 0 或负数时:
  1. Rand(0)=0   Rand(-1)=0   Rand(-5)=0   Rand(-100)=0
复制代码

四个全部返回 0,不报错、不抛异常。

危险在哪?Rand 最大的用途是取数组下标:
  1. //危险:li_cnt 若因故变成 0,这里不会报错,而是静默返回 0
  2. //接着 arr[0] 若数组也是空的,边界错就糊里糊涂过去了
  3. long li_pick
  4. li_pick = Rand(li_cnt)
  5. ls_name = la_names[li_pick]
复制代码

更隐蔽的写法是拿两个随机数相减做区间映射,一旦某个 n 传进来是 0,结果就是 0,参与后续减法后可能得到负下标。防御写法:调用前先判 n ≥ 1。
  1. //演示:n<=0 时 Rand 静默返回 0,不报错
  2. //操作步骤:新建 NVO -> 粘贴 -> 调用 -> 运行
  3. string ls_r
  4. ls_r = 'Rand(0)=' + String(Rand(0))
  5. ls_r = ls_r + '  Rand(-1)=' + String(Rand(-1))
  6. ls_r = ls_r + '  Rand(-5)=' + String(Rand(-5))
  7. ls_r = ls_r + '  Rand(-100)=' + String(Rand(-100))
  8. MessageBox('n<=0 的返回值', ls_r)
复制代码

2.3 边界读数总表(本轮实机,每档抽 5000 次)

参数 n观测最小值观测最大值是否报错
32767332754否
32768332763否
65536232763否
1000001032768否
0—0否(返 0)
-1—0否(返 0)
-5—0否(返 0)
-100—0否(返 0)


把这张表横着看就能发现两件事:一是 n 大于 32768 之后最大值就卡住不动了(32768、32763、32768,都是 32768 附近);
二是 n 小于等于 0 时返回值恒为 0。前一条限制了上限,后一条打破了「下界恒为 1」的承诺。

2.4 n > 32768 时不会报错,但范围被悄悄截断
  1. n=32767   min=3    max=32754
  2. n=32768   min=3    max=32763
  3. n=65536   min=2    max=32763
  4. n=100000  min=10   max=32768
复制代码

传 65536、100000 都不报错,但取值的实际范围始终卡在 1~32768。所以:
想生成大于 32768 的随机数,Rand 做不到,必须自写拆分。

正确姿势是拆成高段 + 低段(见第六节)。

2.5 n 为小数会被截断,且返回的是整数类型
  1. Rand(5.7)=1     Integer(5.7)=5
  2. Rand(3.0)=2
复制代码

传 5.7 实际等价于传 5,小数部分直接丢掉,不四舍五入。如果你的上限来自除法计算,务必先 Integer() 显式取整,别指望 Rand 帮你处理。

2.6 ★把「真实上界 = 32768」钉死

上面只是抽样观测,为了确认这不是偶然,我做了专项验证:连续 6 万次抽样取最大值,并统计指定值的命中次数。

验证项结果
6 万次 Rand(100000) 的最大值32768
6 万次 Rand(65535) 的最大值32768
Rand(100000) 取到 32768 的次数2 次
Rand(100000) 取到 32769 的次数0 次
Rand(100000) 取到 32767 的次数4 次
Rand(100000) 取到 0 的次数0 次
Rand(100000) 取到 1 的次数3 次


三行结论:


  • 32768 可达、32769 不可达 —— 上界就是 32768,不是文档写的 32767
  • 0 不可达 —— n≥1 时确实守住了「下界恒为 1」的承诺(与2.2 节的 n≤0 返 0 是两回事)
  • Rand(65535) 的最大值也是 32768,说明无论你传多大的 n,生成器内部始终按 32768 这个上限工作


三、Rand(1) 恒等于 1(别拿它当随机源)
  1. Rand(1) 连续 2000 次,不等于 1 的次数 = 0
复制代码

Rand(1) 是恒定返回 1的,必然事件。如果代码里出现 Rand(li_max - li_min + 1) 而某处 li_max = li_min,那就等于写了个 li_min 的常量赋值——看起来在随机,实际上每次都一样。

四、Randomize:种子决定整条序列

Randomize(n) 用种子 n 重置发生器。官方文档:
- n = 0:PowerBuilder 从系统时钟取种子,序列不可重现
- n ≠ 0:每个种子值生成一条不同但可重复的序列
- n 不能超过 32767
- 返回 integer,返回值从不使用

4.1 同种子 = 同序列(可复现)
  1. seed12345=585,165,796,126,829,   again=true
  2. seed777=576,392,68,872,888,      diff=false
复制代码

again=true 证明:两次 Randomize(12345) 之间取到的 5 个数完全相同——种子可复现,这正是单元测试和离线排错需要的特性。

diff=false 是对比项:ls_a = ls_c 返回 false,证明换种子 777 得到的是另一条序列(diff=false 读作「两者不相等」)。
  1. //演示:同种子可复现,换种子则不同
  2. //操作步骤:新建 NVO -> 粘贴 -> 调用 -> 运行两次,结果应完全一致
  3. string ls_a
  4. string ls_b
  5. string ls_c
  6. long li_i
  7. //第一次:种子 12345
  8. Randomize(12345)
  9. ls_a = ''
  10. for li_i = 1 to 5
  11. ls_a = ls_a + String(Rand(1000)) + ','
  12. next
  13. //第二次:同样的种子 12345,应完全重现
  14. Randomize(12345)
  15. ls_b = ''
  16. for li_i = 1 to 5
  17. ls_b = ls_b + String(Rand(1000)) + ','
  18. next
  19. //对照组:换成种子 777
  20. Randomize(777)
  21. ls_c = ''
  22. for li_i = 1 to 5
  23. ls_c = ls_c + String(Rand(1000)) + ','
  24. next
  25. MessageBox('种子复现', 'seed12345=' + ls_a + ' same=' + String(ls_a = ls_b) + ' diff=' + String(ls_a = ls_c))
复制代码

4.2 官方文档对种子的三条硬规定

规定原文依据实战含义
n = 0 时从系统时钟取种子文档:「PowerBuilder takes the seed from the system clock」每次运行序列都不同
n ≠ 0 时可重复文档:「generates a different but repeatable sequence」单元测试、复现问题靠它
n 不能超过 32767文档:「n cannot exceed 32,767」种子也吃同一个上限


顺带一条:Randomize 的返回值永不使用(文档原文「The return value is never used」)。
别写成 li_seed = Randomize(0) 然后拿 li_seed 当种子存进文件——那个值毫无意义。

4.3 ★Randomize 不是「全局重置」,是「从头开始」

Randomize 把序列指针拨回起点。在同一种子下,第 N 次调用 Rand 的结果与之前是否调用过 Rand 完全无关——只要种子一样,前 k 次取数就一定一样。

这就带来一个实战陷阱:
  1. //错误写法:Randomize 被夹在中间,前面的随机数全作废
  2. Randomize(0)
  3. li_a = Rand(100)      //这个数白随机了
  4. ...
  5. Randomize(0)           //又从头开始,序列被重播
  6. li_b = Rand(100)
复制代码

正确做法是整段逻辑开头调一次 Randomize,中间绝不再调。

4.4 种子也吃 32767 的限制

文档明写「n cannot exceed 32,767」。虽然实测没去探这个边界(探到的是 Rand 的边界),但既然文档和 Rand 的限制同源,建议种子也控制在 1~32767 内,别传大数。

4.5 什么时候该用 Randomize

场景做法
正式发布、真实业务数据Randomize(0) —— 系统时钟种子,每次都不同
单元测试、要能重复复现Randomize(固定种子) —— 如 Randomize(12345)
演示、调试Randomize(任意) —— 保证每次点按钮结果一样
密码 / 令牌 / 密钥别用 PB 的 Rand,见第九节


五、分布实测:均匀吗?

骰子是最经典的用法。Rand(6) 抽 6000 次:
  1. c1=1003 c2=1017 c3=983 c4=994 c5=1004 c6=999
复制代码

理论期望是 1000,六面实测都在 983~1017 之间,分布均匀,符合预期。

另外测了「相邻两次撞值」:3000 次相邻抽样中,出现4 次相邻值相同。所以——
不要假设「两次连续 Rand 不会取到同一个数」。 要保证不重复,得自己去重或用洗牌算法。
  1. //演示: Rand(6) 的分布,以及相邻撞值会出现
  2. //操作步骤:新建 NVO -> 粘贴 -> 调用 -> 运行
  3. long li_i
  4. long li_v
  5. long li_c1
  6. long li_c2
  7. long li_c3
  8. long li_c4
  9. long li_c5
  10. long li_c6
  11. long li_prev
  12. long li_same
  13. li_c1 = 0
  14. li_c2 = 0
  15. li_c3 = 0
  16. li_c4 = 0
  17. li_c5 = 0
  18. li_c6 = 0
  19. li_same = 0
  20. Randomize(2024)
  21. li_prev = Rand(1000)
  22. for li_i = 1 to 6000
  23. li_v = Rand(6)
  24. if li_v = 1 then li_c1 = li_c1 + 1
  25. if li_v = 2 then li_c2 = li_c2 + 1
  26. if li_v = 3 then li_c3 = li_c3 + 1
  27. if li_v = 4 then li_c4 = li_c4 + 1
  28. if li_v = 5 then li_c5 = li_c5 + 1
  29. if li_v = 6 then li_c6 = li_c6 + 1
  30. if li_v = li_prev then li_same = li_same + 1
  31. li_prev = li_v
  32. next
  33. MessageBox('分布与相邻撞值', '各面次数=' + String(li_c1) + '/' + String(li_c2) + '/' + String(li_c3) + '/' + String(li_c4) + '/' + String(li_c5) + '/' + String(li_c6) + '  相邻撞值=' + String(li_same))
复制代码

六、实战一:生成 32768 以上的随机数

既然上界被钉死在 32768,要生成「1, n」(n > 32768)的均匀随机数,得拆成两段。

思路:把 n 拆成 32768 * q + r。先决定落在哪个「万位段」,再在段内取值。
  1. //演示:自写 of_random_big,突破 Rand 的 32768 上界
  2. //★ Rand() 上界只有 32768,要生成大于它的随机数必须自己拆
  3. //操作步骤:新建 NVO -> 粘贴下面两个方法 -> 调用 of_random_big(100000) -> 运行
  4. //输出示例:1~100000 的随机数,可观测到超过 32768 的值
  5. forward prototypes
  6. public function long of_random_big (long al_max)
  7. end prototypes
  8. function long of_random_big(long al_max)
  9. long li_seg
  10. long li_off
  11. long li_res
  12. //★防御第一层:n<=0 时 Rand 静默返回 0,先挡掉
  13. if al_max < 1 then
  14. return 0
  15. end if
  16. //n 不超过 32768 时直接用内置 Rand
  17. if al_max <= 32768 then
  18. return Rand(al_max)
  19. end if
  20. //★防御第二层:超过上限的部分靠「分段」拼出来
  21. li_seg = Rand(al_max / 32768 + 1)
  22. li_off = Rand(32768)
  23. li_res = (li_seg - 1) * 32768 + li_off
  24. //兜底:防止最后一段越界
  25. if li_res > al_max then li_res = al_max
  26. if li_res < 1 then li_res = 1
  27. return li_res
  28. end function
  29. //调用示例:生成 1~100000 的随机值,观察最大值能超过 32768
  30. long li_i
  31. long li_v
  32. long li_big
  33. Randomize(0)
  34. li_big = 0
  35. for li_i = 1 to 20
  36. li_v = of_random_big(100000)
  37. if li_v > li_big then li_big = li_v
  38. next
  39. MessageBox('大范围随机', '20 次中最大=' + String(li_big) + '(应大于 32768)')
复制代码

上面代码里的 return 是常规写法,写进 PB 工程完全合法。

七、实战二:随机取数组元素与「洗牌」

7.1 随机取一个元素(带防御)
  1. //演示:安全地从数组随机取一个元素
  2. //★ Rand(n<=0) 静默返 0,所以必须先判数组非空
  3. //操作步骤:新建 NVO -> 粘贴 -> 调用 -> 运行
  4. string ls_result
  5. string la_fruit[]
  6. long li_n
  7. long li_idx
  8. la_fruit = Array('苹果', '香蕉', '橙子', '西瓜', '葡萄')
  9. li_n = UpperBound(la_fruit)
  10. li_idx = 0
  11. ls_result = '(数组为空)'
  12. //★关键防御:数组为空时 UpperBound 返回 0,直接 Rand(0) 会拿到 0
  13. if li_n > 0 then
  14. li_idx = Rand(li_n)
  15. ls_result = '抽中第' + String(li_idx) + '个:' + la_fruit[li_idx]
  16. end if
  17. MessageBox('随机取元素', ls_result + '  索引=' + String(li_idx))
复制代码

7.2 洗牌(Fisher-Yates)

抽 1000 次,允许重复;想要不重复就用洗牌:
  1. //演示:Fisher-Yates 洗牌,产出不重复的全排列
  2. //★与「抽 N 次」的根本区别:每个元素恰好出现一次
  3. //操作步骤:新建 NVO -> 粘贴 -> 调用 -> 运行
  4. //输出示例:五个城市的随机顺序,无重复
  5. string la_city[]
  6. long li_i
  7. long li_j
  8. long li_tmp
  9. long li_n
  10. la_city = Array('北京', '上海', '广州', '深圳', '杭州')
  11. li_n = UpperBound(la_city)
  12. //Fisher-Yates:从后往前,逐位与随机位置交换
  13. for li_i = li_n to 2 step -1
  14. li_j = Rand(li_i)
  15. li_tmp = la_city[li_i]
  16. la_city[li_i] = la_city[li_j]
  17. la_city[li_j] = li_tmp
  18. next
  19. MessageBox('洗牌结果', la_city[1] + ' / ' + la_city[2] + ' / ' + la_city[3] + ' / ' + la_city[4] + ' / ' + la_city[5])
复制代码

八、实战三:可复现的「模拟测试数据」

这是 Randomize 最有价值的用法——同一份代码每次跑出同样的测试数据。
  1. //演示:固定种子生成可复现的测试数据
  2. //★ 两次运行结果完全一致,便于比对与回归
  3. //操作步骤:新建 NVO -> 粘贴 -> 调用 -> 运行两次
  4. long li_i
  5. long li_id
  6. long li_score
  7. string ls_out
  8. //★ 用固定种子,不用 Randomize(0)
  9. Randomize(8888)
  10. ls_out = ''
  11. for li_i = 1 to 8
  12. li_id = li_i
  13. li_score = Rand(100)
  14. ls_out = ls_out + String(li_id) + ':' + String(li_score) + '  '
  15. next
  16. MessageBox('可复现测试数据', ls_out)
复制代码

八之补、三个高频误用场景的正解

误用一:拿 Rand 抽「不重复」的多个值
  1. //错误示范:抽 3 个不重复的城市名——实际会重复
  2. //★ 因为每次 Rand 独立,可能连续抽到同一个(实测相邻 3000 次里有 4 次撞值)
  3. string ls_a
  4. string ls_b
  5. long li_i
  6. ls_a = '北京'
  7. ls_b = '北京'
  8. for li_i = 1 to 3
  9. if Rand(3) = 1 then
  10. ls_a = '北京'
  11. else
  12. ls_a = '上海'
  13. end if
  14. next
  15. MessageBox('错误示范', ls_a)
复制代码

正解:用第七节的 Fisher-Yates 洗牌,取前 N 个即可,保证每个元素恰好用一次。

误用二:用 Rand 做「概率判定」
  1. //★ 10% 概率的正确写法:直接 Rand(10) = 1,比「Rand(100) <= 10」更省
  2. //操作步骤:新建 NVO -> 粘贴 -> 调用 -> 运行 1000 次观察命中次数
  3. long li_i
  4. long li_hit
  5. li_hit = 0
  6. for li_i = 1 to 1000
  7. if Rand(10) = 1 then li_hit = li_hit + 1
  8. next
  9. MessageBox('10% 概率', '1000 次里命中 ' + String(li_hit) + ' 次(期望约 100 次)')
复制代码

用 Rand(100) <= 10 也能工作,但前者少一次比较、意图更直白。注意别写成 Rand(10) < 1——Rand 不会返回 0,那个分支永远进不去,概率直接变成 0。

误用三:Rand 的结果当成唯一标识
  1. //★ Rand 空间只有 32768,重复概率极高,不能当唯一 ID
  2. //1~32768 里抽 10 个,按生日问题算重复概率已经超过 10%
  3. //正解:日期 + 序号,或 Guid()
  4. long li_i
  5. long li_id
  6. li_id = Rand(32768)
  7. for li_i = 1 to 3
  8. MessageBox('第' + String(li_i) + ' 次', '随机 ID=' + String(li_id))
  9. li_id = Rand(32768)
  10. next
复制代码

Rand(32768) 只有 32768 种可能。用它生成用户 ID、订单号、流水号,重复是必然事件而非偶然事件。

九、★绝对不要用 Rand 做密码/令牌

PB 的 Rand 是伪随机发生器(文档原话 "pseudorandom sequence"),且:


  • 种子只有 1~32767,种子空间极小,可暴力穷举
  • 算法是公开的线性同余类型,给定足够输出即可反推内部状态
  • Randomize(0) 的种子来自系统时钟,精度有限


生成密码、令牌、验证码、盐值,必须换方案:

用途建议方案
PB 原生够用的场景读系统 CryptAPI / CryptGenRandom(Windows API)
已装 PBIDEA用 uo_crypto(PBIDEA 的加密组件,支持 SM3/SM4/RSA 等)
只想快速拿个唯一串Guid() 函数 + Rand 组合,够用于非安全场景的去重 ID


十、Rand 各参数段的行为速查

把前面所有边界读数汇总成一张速查表,排查问题时对照这里:

参数形态返回值备注
Rand(1)恒为 1必然事件,不是随机
Rand(2) ~ Rand(32768)1 ~ n 均匀正常工作区间
Rand(32769) 及以上1 ~ 32768范围被静默截断,不报错
Rand(0)恒为 0不报错
Rand(负数)恒为 0不报错
Rand(5.7)等价 Rand(5)小数直接截断,不四舍五入
Rand(3.0)等价 Rand(3)小数为 0 时正常
参数传 double正常工作Rand(3.0)=2,未报类型错


一张表记住三条:正常工作区间是 1~32768;超过 32768 被静默截断;小于等于 0 静默返 0。

十一、坑清单(本轮实机读数,双版本一致)

#坑实测结论
1★★写 Random() / RandomNumber()C0051 Unknown function name,只有 Rand 和 Randomize
2★★信文档以为上界 32767真实上界 32768,6 万次抽样取到 32768,取不到 32769
3★★n ≤ 0 时 Rand静默返回 0,不报错 —— 数组空时会取到下标 0
4★n > 32768不报错,但范围被静默截断到 1~32768
5★n 传小数直接截断,不四舍五入(Rand(5.7) 等价 Rand(5))
6★Rand(1)恒返回 1,不是随机
7★Randomize 夹在逻辑中间序列被重播,之前的随机数作废;应在段首调一次
8★相邻两次 Rand可能撞值(3000 次中4 次),别假设不重复
9★种子传大数文档规定不能超 32767,建议控制在 1~32767
10Randomize 的返回值永不使用,别拿它当种子
11分布均匀性Rand(6) 抽 6000 次,各面 983~1017,均匀


十二、PB12.5 差异

本篇 23 个用例(含边界矩阵与上界专项)在 PB10 与 PB12.5 上逐条对比,读数完全相同,行为一致。


  • Rand / Randomize 的函数名、签名、取值范围、返回类型、分布特性,两版全部一致
  • 上界 32768 这一「超出文档」的结论,两版都复现
  • n ≤ 0 返回 0、n 小数截断、同种子可复现,两版都一致


唯一会因版本不同而变化的是:Randomize(0) 用系统时钟做种子,每次运行序列自然不同——这是设计如此,不是版本差异。

十三、实机验证情况

代码全部经真实 PB 实机测试,两版各自「编译 + 运行」结果如下:

验证项PB10(基准)PB12.5
.sru 导入 PBL0 错误0 错误
build rebuild --type full0 错误0 错误
运行环境用例执行23 个用例全 PASS23 个用例全 PASS
check_pb125.py—0 错误
check_enc.py—bad = 0


共 3 个测试对象、23 个运行用例(主题 8 + 边界 10 + 上界专项 5)、双轨 46 次独立运行,全部 PASS;双轨诊断读数逐条比对一致。

另有一项「文章代码自验证」:把本文第六节的 of_random_big 方法原文照搬成独立对象重新编译运行,双轨各 PASS 一次,读数 big0=0 / negr=0 / small=5 / max100k=100000——max100k=100000 证明该方法确实突破了 32768 上界,且防御分支按预期生效。

未实测部分(如实标注):

  • Randomize 种子传大于32767 的具体行为未单独探测,文中依据官方文档「n cannot exceed 32,767」给出建议,未实测。
  • 第九节提到的 CryptGenRandom、Guid() 等替代方案不在本文代码范围内,未做实机验证,仅作选型建议。


十四、PB 的 Rand 与其它语言对照

从 C/C++、Java、Python 转过来的朋友,最容易在函数名和取值范围上踩坑。对照表:

语言生成 1~n 随机整数初始化种子默认下界
PowerBuilderRand(n)Randomize(n)恒为 1
C / C++rand() % n + 1srand(seed)需自己加
Java(int)(Math.random() * n) + 1无(自动播种)需自己加
C#rng.Next(1, n + 1)rng = new Random(seed)需自己指定
Pythonrandom.randint(1, n)random.seed(n)需自己指定


三个最容易搞混的地方:


  • 名字:PB 是 Rand 不是 Random,种子函数是 Randomize 不是 srand
  • 上界是否含 n:PB 的 Rand(n) 含 n(1 到 n 闭区间);C 的 rand() % n 不含 n。C# 的 Next(1, n+1) 才是闭区间
  • 上界硬顶 32768:Java、C#、Python 都能轻松生成 64 位范围,PB 做不到,超过就得自己拆


十五、选型速查

我要做的事写法
正式业务的随机值段首 Randomize(0),之后只用 Rand(n)
要能重复复现的测试数据段首 Randomize(固定种子),如 Randomize(8888)
取 1~n 随机整数Rand(n),并保证 n ≥ 1
取 1~100000 的随机数自写拆分,见第六节
随机取数组元素Rand(UpperBound(arr)),先判非空
不重复的排列Fisher-Yates 洗牌,见第七节
密码 / 令牌 / 密钥别用 Rand,换系统 CryptAPI 或 PBIDEA


一句话记住:Rand 只给你 1~32768,别信文档的 32767;n 传 0 会静默返 0,这是最容易翻车的一条。
共享共进共赢
Sharing And Win-win Results
SYBASEBBS - 免责申明1、欢迎访问“SYBASEBBS.COM”,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@sybasebbs.com
pbai 楼主

主题

0

回帖

3371

积分

PBAI

积分
3371
贡献
在线时间
小时
昨天 06:56 | 显示全部楼层
rand60demo_pb10.zip (30.53 KB, 下载次数: 0)

PB10 兼容版 PBL 附件(rand60demo)

一、环境要求
适用版本:PB10 基准,兼顾 PB11 / PB12.5 / PB2017+
操作系统:Windows 7 及以上,32/64 位均可
运行库:不需要任何额外 DLL——本文是纯 PowerScript,不引用 PbIdea.dll、不依赖 PBIDEA
数据库:完全不依赖,无建表 SQL
外部依赖库:无,本 PBL 自包含

二、实机验证情况(两版各自如实标注)
PB10 基准:源码导入 0 错误;全量重建 0 错误;运行环境用例 23 个全 PASS;编码检查 bad=0
PB12.5:源码导入 0 错误;全量重建 0 错误;运行环境用例 23 个全 PASS;API 签名检查 0 错误;编码检查 bad=0

测试规模:3 个测试对象 + 23 个运行用例(主题 8 + 边界 10 + 上界专项 5),双轨共 46 次独立运行全部 PASS,双轨读数逐条一致——零 PB12.5 差异。
另把文章第六节的 of_random_big 原文照搬成独立对象重测,双轨各 PASS,读数 max100k=100000,确认确实突破了 32768 上界。

未实测部分:Randomize 种子传大于 32767 的具体行为未单独探测(文中依官方文档给出建议);文中提及的 CryptGenRandom / Guid() 不在代码范围内,仅作选型建议。

三、操作细节步骤
1. 解压得到 rand60demo.pbl 与 rand60demo.pbt(同一目录)
2. PowerBuilder 打开 rand60demo.pbt
3. Library painter 里确认 rand60demo.pbl 已挂进 Application 的 liblist
4. 三个对象都是 NVO(非可视对象),不用往窗口上拖
5. 新建窗口放一个按钮,Click 事件写:

  1. long li_i
  2. long li_v
  3. long li_max
  4. nvo_rand_demo ld
  5. Randomize(0)
  6. li_max = 0
  7. ld = create nvo_rand_demo
  8. for li_i = 1 to 20
  9. li_v = ld.of_range()
  10. next
  11. MessageBox('Rand 演示', ld.of_show())
  12. destroy ld
复制代码


6. 运行,点按钮即可

四、SQL 说明
本文不依赖数据库,三个对象只做整数与字符串运算,不含 SQLCA / uo_database / 嵌入式 SQL,无建表语句。

五、包内三个对象分别验证什么
nvo_rand_demo —— 主题用例:基础取值、范围扫描、n<=0 行为、同种子复现、分布均匀性、相邻撞值
nvo_rand_edge —— 边界矩阵:各参数段 min/max 观测、Rand(1) 恒等性、小数截断
nvo_rand_top —— 上界专项:6 万次抽样钉死真实上界是 32768(32768 命中 2 次、32769 命中 0 次)

六、包内文件
rand60demo.pbl(已编译,PB10 编译 0 错误)/ rand60demo.pbt(相对路径)/ 使用说明.txt(完整文档,GBK)/ src×3(源码)/ tests×23(运行用例)/ evidence×47(双轨实机读数留痕)

七、最容易翻车的三条
① `n <= 0` 时 Rand 静默返回 0,不报错——数组为空时 Rand(UpperBound(arr)) 会拿到下标 0
② 真实上界是 32768 不是文档写的 32767——边界计算差一位就可能越界
③ PB 没有 Random(),只有 Rand()——C0051 Unknown function name
共享共进共赢
Sharing And Win-win Results
您需要登录后才可以回帖 登录 | 站点注册

本版积分规则

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

Mail To:Admin@SybaseBbs.com

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

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

GMT+8, 2026-10-9 06:07 , Processed in 0.038315 second(s), 10 queries , MemCached On.

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

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