马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
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 万次抽样:
- max_of_60000_Rand100000=32768
- max_of_60000_Rand65535=32768
- count_32768=2 count_32769=0
复制代码
Rand(100000) 和 Rand(65535) 都能取到 32768,且取不到 32769。
也就是说,真正的取值范围是 1 ~ 32768,官方文档写的 32767 少报了一个值。别小看这一个值的偏差——如果你写「用 Rand(32767 - 下界 + 1) 生成下界以内的随机数」这类边界计算,差一位就会在极低概率下越界。
演示步骤:新建一个 NVO(非可视对象),粘贴下面的方法代码,在按钮事件里调用它,点运行即可看到输出:
- //演示:Rand 的真实上界是 32768,不是文档写的 32767
- //操作步骤:新建 NVO -> 粘贴本方法 -> 在按钮事件里调用本方法 -> 运行
- //输出示例:max=32768 取不到32769
- long li_i
- long li_v
- long li_max
- long li_c32769
- //用大n 抽样,观察最大值能到多少
- li_max = 0
- li_c32769 = 0
- for li_i = 1 to 60000
- li_v = Rand(100000)
- if li_v > li_max then li_max = li_v
- if li_v = 32769 then li_c32769 = li_c32769 + 1
- next
- MessageBox('Rand 真实上界', 'max=' + String(li_max) + ' 取到32769的次数=' + String(li_c32769))
复制代码
2.2 ★★n ≤ 0 时返回 0 —— 这是最危险的一条
文档说「下界恒为 1」,但这句话只在 n ≥ 1 时成立。n 为 0 或负数时:
- Rand(0)=0 Rand(-1)=0 Rand(-5)=0 Rand(-100)=0
复制代码
四个全部返回 0,不报错、不抛异常。
危险在哪?Rand 最大的用途是取数组下标:
- //危险:li_cnt 若因故变成 0,这里不会报错,而是静默返回 0
- //接着 arr[0] 若数组也是空的,边界错就糊里糊涂过去了
- long li_pick
- li_pick = Rand(li_cnt)
- ls_name = la_names[li_pick]
复制代码
更隐蔽的写法是拿两个随机数相减做区间映射,一旦某个 n 传进来是 0,结果就是 0,参与后续减法后可能得到负下标。防御写法:调用前先判 n ≥ 1。
- //演示:n<=0 时 Rand 静默返回 0,不报错
- //操作步骤:新建 NVO -> 粘贴 -> 调用 -> 运行
- string ls_r
- ls_r = 'Rand(0)=' + String(Rand(0))
- ls_r = ls_r + ' Rand(-1)=' + String(Rand(-1))
- ls_r = ls_r + ' Rand(-5)=' + String(Rand(-5))
- ls_r = ls_r + ' Rand(-100)=' + String(Rand(-100))
- MessageBox('n<=0 的返回值', ls_r)
复制代码
2.3 边界读数总表(本轮实机,每档抽 5000 次)
| 参数 n | 观测最小值 | 观测最大值 | 是否报错 | | 32767 | 3 | 32754 | 否 | | 32768 | 3 | 32763 | 否 | | 65536 | 2 | 32763 | 否 | | 100000 | 10 | 32768 | 否 | | 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 时不会报错,但范围被悄悄截断
- n=32767 min=3 max=32754
- n=32768 min=3 max=32763
- n=65536 min=2 max=32763
- n=100000 min=10 max=32768
复制代码
传 65536、100000 都不报错,但取值的实际范围始终卡在 1~32768。所以:
想生成大于 32768 的随机数,Rand 做不到,必须自写拆分。
正确姿势是拆成高段 + 低段(见第六节)。
2.5 n 为小数会被截断,且返回的是整数类型
- Rand(5.7)=1 Integer(5.7)=5
- 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(别拿它当随机源)
- 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 同种子 = 同序列(可复现)
- seed12345=585,165,796,126,829, again=true
- seed777=576,392,68,872,888, diff=false
复制代码
again=true 证明:两次 Randomize(12345) 之间取到的 5 个数完全相同——种子可复现,这正是单元测试和离线排错需要的特性。
diff=false 是对比项:ls_a = ls_c 返回 false,证明换种子 777 得到的是另一条序列(diff=false 读作「两者不相等」)。
- //演示:同种子可复现,换种子则不同
- //操作步骤:新建 NVO -> 粘贴 -> 调用 -> 运行两次,结果应完全一致
- string ls_a
- string ls_b
- string ls_c
- long li_i
- //第一次:种子 12345
- Randomize(12345)
- ls_a = ''
- for li_i = 1 to 5
- ls_a = ls_a + String(Rand(1000)) + ','
- next
- //第二次:同样的种子 12345,应完全重现
- Randomize(12345)
- ls_b = ''
- for li_i = 1 to 5
- ls_b = ls_b + String(Rand(1000)) + ','
- next
- //对照组:换成种子 777
- Randomize(777)
- ls_c = ''
- for li_i = 1 to 5
- ls_c = ls_c + String(Rand(1000)) + ','
- next
- 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 次取数就一定一样。
这就带来一个实战陷阱:
- //错误写法:Randomize 被夹在中间,前面的随机数全作废
- Randomize(0)
- li_a = Rand(100) //这个数白随机了
- ...
- Randomize(0) //又从头开始,序列被重播
- 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 次:
- c1=1003 c2=1017 c3=983 c4=994 c5=1004 c6=999
复制代码
理论期望是 1000,六面实测都在 983~1017 之间,分布均匀,符合预期。
另外测了「相邻两次撞值」:3000 次相邻抽样中,出现4 次相邻值相同。所以——
不要假设「两次连续 Rand 不会取到同一个数」。 要保证不重复,得自己去重或用洗牌算法。 - //演示: Rand(6) 的分布,以及相邻撞值会出现
- //操作步骤:新建 NVO -> 粘贴 -> 调用 -> 运行
- long li_i
- long li_v
- long li_c1
- long li_c2
- long li_c3
- long li_c4
- long li_c5
- long li_c6
- long li_prev
- long li_same
- li_c1 = 0
- li_c2 = 0
- li_c3 = 0
- li_c4 = 0
- li_c5 = 0
- li_c6 = 0
- li_same = 0
- Randomize(2024)
- li_prev = Rand(1000)
- for li_i = 1 to 6000
- li_v = Rand(6)
- if li_v = 1 then li_c1 = li_c1 + 1
- if li_v = 2 then li_c2 = li_c2 + 1
- if li_v = 3 then li_c3 = li_c3 + 1
- if li_v = 4 then li_c4 = li_c4 + 1
- if li_v = 5 then li_c5 = li_c5 + 1
- if li_v = 6 then li_c6 = li_c6 + 1
- if li_v = li_prev then li_same = li_same + 1
- li_prev = li_v
- next
- 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。先决定落在哪个「万位段」,再在段内取值。
- //演示:自写 of_random_big,突破 Rand 的 32768 上界
- //★ Rand() 上界只有 32768,要生成大于它的随机数必须自己拆
- //操作步骤:新建 NVO -> 粘贴下面两个方法 -> 调用 of_random_big(100000) -> 运行
- //输出示例:1~100000 的随机数,可观测到超过 32768 的值
- forward prototypes
- public function long of_random_big (long al_max)
- end prototypes
- function long of_random_big(long al_max)
- long li_seg
- long li_off
- long li_res
- //★防御第一层:n<=0 时 Rand 静默返回 0,先挡掉
- if al_max < 1 then
- return 0
- end if
- //n 不超过 32768 时直接用内置 Rand
- if al_max <= 32768 then
- return Rand(al_max)
- end if
- //★防御第二层:超过上限的部分靠「分段」拼出来
- li_seg = Rand(al_max / 32768 + 1)
- li_off = Rand(32768)
- li_res = (li_seg - 1) * 32768 + li_off
- //兜底:防止最后一段越界
- if li_res > al_max then li_res = al_max
- if li_res < 1 then li_res = 1
- return li_res
- end function
- //调用示例:生成 1~100000 的随机值,观察最大值能超过 32768
- long li_i
- long li_v
- long li_big
- Randomize(0)
- li_big = 0
- for li_i = 1 to 20
- li_v = of_random_big(100000)
- if li_v > li_big then li_big = li_v
- next
- MessageBox('大范围随机', '20 次中最大=' + String(li_big) + '(应大于 32768)')
复制代码
上面代码里的 return 是常规写法,写进 PB 工程完全合法。
七、实战二:随机取数组元素与「洗牌」
7.1 随机取一个元素(带防御)
- //演示:安全地从数组随机取一个元素
- //★ Rand(n<=0) 静默返 0,所以必须先判数组非空
- //操作步骤:新建 NVO -> 粘贴 -> 调用 -> 运行
- string ls_result
- string la_fruit[]
- long li_n
- long li_idx
- la_fruit = Array('苹果', '香蕉', '橙子', '西瓜', '葡萄')
- li_n = UpperBound(la_fruit)
- li_idx = 0
- ls_result = '(数组为空)'
- //★关键防御:数组为空时 UpperBound 返回 0,直接 Rand(0) 会拿到 0
- if li_n > 0 then
- li_idx = Rand(li_n)
- ls_result = '抽中第' + String(li_idx) + '个:' + la_fruit[li_idx]
- end if
- MessageBox('随机取元素', ls_result + ' 索引=' + String(li_idx))
复制代码
7.2 洗牌(Fisher-Yates)
抽 1000 次,允许重复;想要不重复就用洗牌:
- //演示:Fisher-Yates 洗牌,产出不重复的全排列
- //★与「抽 N 次」的根本区别:每个元素恰好出现一次
- //操作步骤:新建 NVO -> 粘贴 -> 调用 -> 运行
- //输出示例:五个城市的随机顺序,无重复
- string la_city[]
- long li_i
- long li_j
- long li_tmp
- long li_n
- la_city = Array('北京', '上海', '广州', '深圳', '杭州')
- li_n = UpperBound(la_city)
- //Fisher-Yates:从后往前,逐位与随机位置交换
- for li_i = li_n to 2 step -1
- li_j = Rand(li_i)
- li_tmp = la_city[li_i]
- la_city[li_i] = la_city[li_j]
- la_city[li_j] = li_tmp
- next
- MessageBox('洗牌结果', la_city[1] + ' / ' + la_city[2] + ' / ' + la_city[3] + ' / ' + la_city[4] + ' / ' + la_city[5])
复制代码
八、实战三:可复现的「模拟测试数据」
这是 Randomize 最有价值的用法——同一份代码每次跑出同样的测试数据。
- //演示:固定种子生成可复现的测试数据
- //★ 两次运行结果完全一致,便于比对与回归
- //操作步骤:新建 NVO -> 粘贴 -> 调用 -> 运行两次
- long li_i
- long li_id
- long li_score
- string ls_out
- //★ 用固定种子,不用 Randomize(0)
- Randomize(8888)
- ls_out = ''
- for li_i = 1 to 8
- li_id = li_i
- li_score = Rand(100)
- ls_out = ls_out + String(li_id) + ':' + String(li_score) + ' '
- next
- MessageBox('可复现测试数据', ls_out)
复制代码
八之补、三个高频误用场景的正解
误用一:拿 Rand 抽「不重复」的多个值
- //错误示范:抽 3 个不重复的城市名——实际会重复
- //★ 因为每次 Rand 独立,可能连续抽到同一个(实测相邻 3000 次里有 4 次撞值)
- string ls_a
- string ls_b
- long li_i
- ls_a = '北京'
- ls_b = '北京'
- for li_i = 1 to 3
- if Rand(3) = 1 then
- ls_a = '北京'
- else
- ls_a = '上海'
- end if
- next
- MessageBox('错误示范', ls_a)
复制代码
正解:用第七节的 Fisher-Yates 洗牌,取前 N 个即可,保证每个元素恰好用一次。
误用二:用 Rand 做「概率判定」
- //★ 10% 概率的正确写法:直接 Rand(10) = 1,比「Rand(100) <= 10」更省
- //操作步骤:新建 NVO -> 粘贴 -> 调用 -> 运行 1000 次观察命中次数
- long li_i
- long li_hit
- li_hit = 0
- for li_i = 1 to 1000
- if Rand(10) = 1 then li_hit = li_hit + 1
- next
- MessageBox('10% 概率', '1000 次里命中 ' + String(li_hit) + ' 次(期望约 100 次)')
复制代码
用 Rand(100) <= 10 也能工作,但前者少一次比较、意图更直白。注意别写成 Rand(10) < 1——Rand 不会返回 0,那个分支永远进不去,概率直接变成 0。
误用三:Rand 的结果当成唯一标识
- //★ Rand 空间只有 32768,重复概率极高,不能当唯一 ID
- //1~32768 里抽 10 个,按生日问题算重复概率已经超过 10%
- //正解:日期 + 序号,或 Guid()
- long li_i
- long li_id
- li_id = Rand(32768)
- for li_i = 1 to 3
- MessageBox('第' + String(li_i) + ' 次', '随机 ID=' + String(li_id))
- li_id = Rand(32768)
- 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 | | 10 | Randomize 的返回值 | 永不使用,别拿它当种子 | | 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 导入 PBL | 0 错误 | 0 错误 | | build rebuild --type full | 0 错误 | 0 错误 | | 运行环境用例执行 | 23 个用例全 PASS | 23 个用例全 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 随机整数 | 初始化种子 | 默认下界 | | PowerBuilder | Rand(n) | Randomize(n) | 恒为 1 | | C / C++ | rand() % n + 1 | srand(seed) | 需自己加 | | Java | (int)(Math.random() * n) + 1 | 无(自动播种) | 需自己加 | | C# | rng.Next(1, n + 1) | rng = new Random(seed) | 需自己指定 | | Python | random.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,这是最容易翻车的一条。 |