【仓颉语言入门 · 第二篇 第 4 课】
【仓颉语言入门 · 第二篇 第 4 课】随机数与概率:给程序加上不确定性(std.random)
上一课结尾留了个话头:程序"听话"之后,下一个需求是"随机"。抽奖程序不能每次都中一等奖,模拟测试不能每次生成同一批数据,洗牌算法要是能把牌序打乱——这些都要靠随机数。这一课的
std.random会讲清楚三件事:怎么取一个随机整数/浮点;怎么从"一半概率"到"按权重抽奖"控制概率分布;以及为什么默认的随机数每次运行都不一样(种子的秘密)。学完顺手给第 3 课的日志工具家族添个新成员 logpick:从日志文件里随机抽查 N 条。
本文所有代码、输出与报错文案均在仓颉 SDK 1.2.0 下逐行实测编译运行(凡受随机性影响无法逐字复现的输出,文中会明确标注"示例")。
目录(第二篇导航)
第一篇《仓颉语言入门》共 30 课已完结(环境搭建 → 语法 → 函数 → 类型系统 → 工程化 → 并发 → 两场实战)。第二篇在更高的起点上继续,共 17 课、4 个模块:
| 模块 | 课次 | 内容 |
|---|---|---|
| 一、补遗篇 | 01~06 | 正则、时间日期、命令行参数、随机数、加密哈希、二进制处理 |
| 二、数据库篇 | 07~10 | std.database.sql 入门与 CRUD、预编译与事务、手写 DAO、综合小实战 |
| 三、进阶硬核篇 | 11~14 | 反射、宏与 deriving、C 互操作 FFI、并发容器与对象池 |
| 四、实战项目篇 | 15~17 | HTTP 服务端、多线程日志分析器、简易爬虫 |
- 正则表达式(std.regex)
- 时间日期处理(std.time)
- 命令行参数解析(std.argopt)
- 随机数与概率(本文)
- 加密与哈希(std.crypto)
- 二进制数据处理(std.binary)
- 数据库入门:std.database.sql 与 CRUD
- 预编译、事务与防注入
- 手写 DAO 数据访问层
- 数据库综合小实战
- 反射:在运行时打量类型
- 宏与 deriving:让编译器帮你写代码
- 与 C 互操作:FFI
- 并发容器与对象池
- 实战三:HTTP 服务端
- 实战四:多线程日志分析器
- 实战五:简易爬虫
一、为什么需要随机数
先数数我们身边的"随机需求":
- 抽奖:从 1000 个用户里抽 3 个幸运儿;
- 模拟:测试一个排队系统,得模拟"顾客随机到达";
- 测试数据:注册 10000 个"用户",手机号、昵称都别重样;
- 游戏:掷骰子、发扑克、暴击判定;
- 抽查审计:从 6 条日志里随机抽查 3 条看看。
这些事有个共同点:结果在写代码时不确定,跑起来才知道。你可能会想,这还不简单——
let lucky = 42 // 写死的"随机数"一点都不随机
真正的随机数生成器要满足:每次取值都不一样、取值范围可控、分布符合预期。计算机本身是确定性的机器,所以业界用的是伪随机数生成器:内部维护一串"看起来毫无规律"的数字,按固定算法不断推进。既然是算法,就有一个起点——这个起点叫种子(seed)。同一个种子出发,往后走的每一步都一模一样。这件事初听是缺点,其实是这套机制最有趣的特性,第五节专门讲。
std.random 的主角只有一个类:
| 类型 | 角色 |
|---|---|
Random |
随机数生成器:先构造(可选种子),再反复"取" |
先在 cjpm 工程里引入:
import std.random.*
二、第一行随机代码:构造生成器,反复取值
2.1 最小可用例子
用 Random(42) 构造一个固定种子的生成器,连续取 5 个 [0, 1000) 里的随机整数:
import std.random.*
main(): Int64 {
let r = Random(42)
print("5 个随机数: ")
for (i in 0..5) {
print("${r.nextInt64(1000)} ")
}
println()
0
}
5 个随机数: 662 428 144 550 257
现在记住这 5 个数:662 428 144 550 257。你再跑一遍,输出还是这 5 个数;换台电脑跑,还是它们。这就是固定种子的魔力——第五节揭晓原理。如果只想要"每次都不一样",把构造器换成 Random() 即可。
2.2 两种构造器:要不要种子
Random 有两种构造方式,实测对比(在某次运行中):
import std.random.*
main(): Int64 {
let r1 = Random()
let r2 = Random()
print("r1 前5个: ")
for (i in 0..5) {
print("${r1.nextInt64(1000)} ")
}
println()
print("r2 前5个: ")
for (i in 0..5) {
print("${r2.nextInt64(1000)} ")
}
println()
0
}
r1 前5个: 371 636 51 137 331
r2 前5个: 670 705 227 880 969
(示例输出——Random() 用系统熵做种子,每次运行都不同,但同一次运行里两个实例也各走各的序列。)
两种构造器一句话总结:
| 写法 | 种子来源 | 结果 |
|---|---|---|
Random() |
系统熵(时间等) | 每次运行都不同,适合游戏、抽奖 |
Random(42) |
你给定的值 | 结果完全可复现,适合测试、调试 |
三、取值全家桶:整数、浮点、布尔、字节
3.1 区间语义:左闭右开 [0, bound)
带一个参数的取值方法,返回值落在 [0, bound)——含下限、不含上限。验证一下,取 1 万次 nextInt64(100) 看最小最大值:
import std.random.*
main(): Int64 {
let r = Random(7)
var mn = Int64.Max
var mx = Int64.Min
for (i in 0..10000) {
let v = r.nextInt64(100)
if (v < mn) {
mn = v
}
if (v > mx) {
mx = v
}
}
println("min=${mn} max=${mx}")
0
}
min=0 max=99
1 万次里最小 0、最大 99,100 从未出现——[0, 100) 实锤。想要 1~6 的骰子,就得 nextInt64(6) + 1,先取 [0,6) 再整体加 1。
bound 还有硬性要求:必须是正数。传 0 或负数直接抛异常(实测文案):
IllegalArgumentException: Upper must be positive, got: 0.
IllegalArgumentException: Upper must be positive, got: -5.
3.2 整数家族:八种整型一个套路
nextInt64(bound) 不是独苗,仓颉给八种整型都配了同款方法——无参取全范围、带 bound 取 [0, bound):
| 方法 | 无参版本 | 带 bound 版本 |
|---|---|---|
| 有符号 | nextInt8() nextInt16() nextInt32() nextInt64() |
nextInt8(b) nextInt16(b) nextInt32(b) nextInt64(b) |
| 无符号 | nextUInt8() nextUInt16() nextUInt32() nextUInt64() |
nextUInt8(b) nextUInt16(b) nextUInt32(b) nextUInt64(b) |
最常用的是 nextInt64。但掷骰子这种小范围场景,用 nextUInt8(6) 更贴切(注意参数类型是 UInt8,字面量 6 会自动适配):
import std.random.*
main(): Int64 {
let r = Random(3)
// 掷 6000 次骰子,统计每个点数出现的次数
let counts = Array<Int64>(6, repeat: 0)
for (i in 0..6000) {
let face = Int64(r.nextUInt8(6)) + 1
counts[face - 1] += 1
}
for (face in 1..=6) {
println("${face} 点:${counts[face - 1]} 次")
}
0
}
1 点:971 次
2 点:1003 次
3 点:1072 次
4 点:945 次
5 点:1005 次
6 点:1004 次
6000 次掷骰子,理想情况每面正好 1000 次——实测每面都在 1000 上下晃,这就是"均匀"的含义:单次不可预测,大量重复后接近均分。
还有一个按位取值的 nextBits(bound: UInt64):取 [0, bound) 间的随机数,但 bound 必须是 2 的幂时效率最高,日常用得少,知道有这号人物即可。
3.3 浮点与布尔:概率控制的原料
nextFloat64() 返回 [0.0, 1.0) 的小数,nextBool() 返回各占一半的真/假。实测:
import std.random.*
main(): Int64 {
let r = Random(7)
var mn = 1.0
var mx = 0.0
for (i in 0..10000) {
let v = r.nextFloat64()
if (v < mn) {
mn = v
}
if (v > mx) {
mx = v
}
}
println("nextFloat64: min=${mn} max=${mx}")
let r2 = Random(9)
var heads = 0
for (i in 0..10000) {
if (r2.nextBool()) {
heads += 1
}
}
println("nextBool 1万次正面: ${heads}")
0
}
nextFloat64: min=0.000260 max=0.999995
nextBool 1万次正面: 4982
1 万次浮点全部落在 [0.0, 1.0);1 万次抛硬币正面 4982 次——接近但不是精确的 5000,这就是真实的随机(要是每次都精确 5000,那才叫有问题)。浮点家族还有 nextFloat32() / nextFloat16(),用法同理。
3.4 随机字节
nextBytes 有两种用法:给个数,返回新数组;或者给个数组,原地填满:
import std.random.*
main(): Int64 {
let r = Random(7)
let b = r.nextBytes(8)
print("nextBytes(8): ")
for (x in b) {
print("${x} ")
}
println()
let buf = Array<Byte>(4, repeat: 0)
r.nextBytes(buf)
println("填充后 buf.size=${buf.size}")
0
}
nextBytes(8): 167 98 206 246 221 108 129 70
填充后 buf.size=4
随机字节是生成测试文件、模拟网络包的原料。第五节之后还会用到它的亲戚——加密哈希。
实测提示:API 里还有一个
nextUInt8s(注意多个 s),在 1.2.0 编译会报废弃警告warning: function 'nextUInt8s' is deprecated. Use member function 'public func nextBytes(arr: Array<Byte>): Unit' instead.——它是被nextBytes取代的老方法,别再用。
3.5 seed 是只读的
Random 对象身上可以随时查看当前种子:
let r = Random(42)
println(r.seed) // 42
但它是只读属性,想"半路换种子"编译器直接拦下(实测):
error: cannot assign to immutable value
note: property 'seed' is immutable
想换种子就新造一个实例:let r2 = Random(7)。
四、概率控制:从"一半"到"按权重"
4.1 一半:硬币
nextBool() 天生就是公平硬币(3.3 节实测 4982/10000)。所有"五五开"的判断都归它:暴击判定、正面朝上、是否走东边。
4.2 三成:浮点阈值法
想改成"三成概率"?用 nextFloat64() 和阈值比较:随机小数落在 [0.0, 0.3) 的概率恰好是 30%——因为小数在 [0,1) 上均匀铺开,切走前 30% 的长度,命中概率就是 30%:
import std.random.*
main(): Int64 {
let r = Random(9)
var hit = 0
for (i in 0..10000) {
if (r.nextFloat64() < 0.3) {
hit += 1
}
}
println("10000 次里命中 ${hit} 次")
0
}
10000 次里命中 3004 次
3004/10000 = 30.04%,和设计的 30% 紧紧贴合。这个"浮点阈值法"是概率控制万能公式:p 成概率就是 r.nextFloat64() < p。
4.3 按权重抽奖:轮盘算法
真实抽奖很少是均匀的:一等奖 1 份、二等奖 3 份、三等奖 6 份、谢谢参与 90 份。把权重想成一把长度 100 的尺子,每段奖品占一段;随机往尺子上扔飞镖,扔中哪段就是哪个奖品:
|一等奖|二等奖|三等奖|-------------- 谢谢参与 --------------|
0 1 4 10 100
代码三步:算总权重 → 扔飞镖(nextInt64(总权重))→ 从头累加找飞镖落在哪段:
import std.random.*
main(): Int64 {
let prizes: Array<(String, Int64)> = [
("一等奖", 1), ("二等奖", 3), ("三等奖", 6), ("谢谢参与", 90)
]
var total: Int64 = 0
for (p in prizes) {
total += p[1]
}
let r = Random(11)
// 模拟 10 万次抽奖,统计每档命中率
let count = Array<Int64>(prizes.size, repeat: 0)
for (i in 0..100000) {
let dart = r.nextInt64(total)
var acc: Int64 = 0
for (j in 0..prizes.size) {
acc += prizes[j][1]
if (dart < acc) {
count[j] += 1
break
}
}
}
for (j in 0..prizes.size) {
let pct = Float64(count[j]) * 100.0 / 100000.0
println("${prizes[j][0]}: ${count[j]} 次(${pct}%)")
}
0
}
一等奖: 1028 次(1.028000%)
二等奖: 3021 次(3.021000%)
三等奖: 5894 次(5.894000%)
谢谢参与: 90057 次(90.057000%)
设计权重 1:3:6:90,实测命中率 1.028% : 3.021% : 5.894% : 90.057%——轮盘算法成立。它比"给每档奖品写一行 if"优雅得多:改权重只改数字,逻辑零改动。
4.4 正态分布:nextGaussian
均匀分布解决"人人平等",但很多自然现象不是平等的:成年人的身高、测量误差、接口响应时间,都是中间高两边低的钟形曲线(正态分布)。nextGaussianFloat64 专管这个,两个可选参数用命名实参传入:mean 是中心(默认 0.0),sigma 是离散程度(默认 1.0):
import std.random.*
main(): Int64 {
let r = Random(1)
// 标准正态:中心 0,波动 1
var sum = 0.0
for (i in 0..10000) {
sum += r.nextGaussianFloat64()
}
println("标准正态 1万次均值: ${sum / 10000.0}")
// 身高模型:中心 170cm,波动 5cm
var sum2 = 0.0
for (i in 0..1000) {
sum2 += r.nextGaussianFloat64(mean: 170.0, sigma: 5.0)
}
println("身高模型 1千次均值: ${sum2 / 1000.0}")
0
}
标准正态 1万次均值: 0.011657
身高模型 1千次均值: 170.062281
均值分别贴近 0 和参数中心——参数生效。两个细节实测过:命名实参不能省(写 nextGaussianFloat64(170.0, 5.0) 报 error: missing argument prefix 'mean:' or 'sigma:' for named parameter);同款还有 nextGaussianFloat32 / nextGaussianFloat16。
五、种子的秘密:可复现性
5.1 为什么每次运行都不一样
Random() 的种子来自系统熵——当前时间、进程信息这些"每次都不同"的原料。种子不同,从起点出发的整条序列就完全不同,于是你每次运行都看到新结果。这就是"随机"的日常来源。
5.2 固定种子:时光机
种子决定了整条序列。Random(42) 无论跑多少遍、在哪台机器上跑,取出来的数都一模一样(2.1 节的 662 428 144 550 257)。同种子实例之间也彼此独立地"重放"同一条序列:
import std.random.*
main(): Int64 {
let r1 = Random(42)
let r2 = Random(42)
print("r1 前3个: ${r1.nextInt64(1000)} ${r1.nextInt64(1000)} ${r1.nextInt64(1000)}")
println()
print("r2 前3个: ${r2.nextInt64(1000)} ${r2.nextInt64(1000)} ${r2.nextInt64(1000)}")
println()
0
}
r1 前3个: 662 428 144
r2 前3个: 662 428 144
两个细节要看清:同一个实例连续取值是沿序列往前走(r1 三次取值拿到三个不同的数);新造一个同种子实例才是回到起点重放。
5.3 什么时候要固定种子
| 场景 | 用哪种 | 理由 |
|---|---|---|
| 游戏、抽奖、模拟 | Random() |
每次都要新鲜感 |
| 单元测试 | Random(42) |
断言需要确定的输入 |
| 调试复现 bug | Random(42) |
同一种子重现同一条路径 |
| 抽样审计(本章 logpick) | 可选 -s 参数 |
审计要能复查"当时抽了谁" |
一句话:给别人惊喜用系统熵,给自己留后路用固定种子。
5.4 一个实测提醒:全局 seed 函数暂不可用
翻标准库文档能看到 std.random 里有个全局函数 seed,但在 SDK 1.2.0 下它存在却不可访问:直接调用报 undeclared identifier 'seed',显式 import std.random.seed 报 'seed' is not accessible in package 'std.random'。本课所有"固定种子"需求一律用 Random(seed) 构造器实现,别在 seed() 上浪费时间。
六、随机打乱与抽样:Fisher-Yates
6.1 洗牌:边走边换
把一副牌打乱,最直观的思路是"每张牌随机找一个位置放"——容易有死角。经典算法 Fisher-Yates 反着来:走到第 k 张,从第 k 张往后(含自己)随机挑一张,与第 k 张交换。走完全程,每种排列概率严格相等:
import std.random.*
main(): Int64 {
let deck = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"]
let r = Random(99)
for (k in 0..deck.size - 1) {
let swap = k + r.nextInt64(deck.size - k)
let tmp = deck[k]
deck[k] = deck[swap]
deck[swap] = tmp
}
var line = ""
for (card in deck) {
line = line + card + " "
}
println(line)
0
}
9 Q 3 4 8 K 2 7 J 5 6 A 10
关键一行是 k + r.nextInt64(deck.size - k):deck.size - k 是"剩下的牌数",加上 k 后正好落在 [k, deck.size)——只跟没洗过的牌换,洗过的不回头。
6.2 不重复抽 N 条:只洗前 N 张
"从 100 个人里抽 3 个中奖者",用不着全洗——部分洗牌:只执行前 N 轮交换,取前 N 个位置。因为每个位置都被"从后面随机换上来"过一遍,前 N 个不重复且公平:
import std.random.*
main(): Int64 {
let tags = ["A", "B", "C", "D", "E", "F"]
let r = Random(2026)
let n = 3
for (k in 0..n) {
let swap = k + r.nextInt64(tags.size - k)
let tmp = tags[k]
tags[k] = tags[swap]
tags[swap] = tmp
}
print("抽中: ")
for (k in 0..n) {
print("${tags[k]} ")
}
println()
0
}
抽中: F C E
种子固定,这个"抽签结果"永远可复现——审计场景的刚需。下面把它用起来。
七、CIDE 实操:logpick 随机抽查日志
第 3 课的 logtool 解决"按级别过滤"。运维还有个场景:例行抽查——从日志里随机抽几条人工过目,每次抽的最好不一样,但抽查了哪些要能复查。现在写 logpick:-n 指定条数(必填),-i 指定日志路径(默认 data/app.log),-s 指定种子(选填,给了就 可复现)。
7.1 数据文件
沿用第 3 课的 data/app.log(放在工程根目录):
2026-10-09 08:30:15 [INFO] auth - 用户 user01 从 192.168.1.23 登录
2026-10-09 08:31:02 [ERROR] db - 连接超时: 192.168.1.10:5432
2026-10-09 08:31:45 [WARN] auth - 密码重试过多: user07
2026-10-09 08:32:20 [INFO] api - 请求 /api/orders 耗时 42ms
2026-10-09 08:33:05 [ERROR] api - 调用支付网关失败: 10.0.0.8
2026-10-09 08:34:10 [INFO] auth - 用户 13812345678 修改了手机号 13998887766
7.2 完整代码
src/main.cj 全文(参数解析是第 3 课的 argopt,文件读取是第 22 课的老三样,抽样是本章 6.2 的部分洗牌):
import std.random.*
import std.argopt.*
import std.fs.*
import std.unicode.*
import std.convert.*
import std.collection.*
// 随机抽查 N 条日志:-i 日志路径(默认 data/app.log)
// -n 抽查条数(必填),-s 随机种子(选填,给定时结果可复现)
main(args: Array<String>): Int64 {
let specs = [
ArgumentSpec.Full("input", r'i', ArgumentMode.RequiredValue),
ArgumentSpec.Full("count", r'n', ArgumentMode.RequiredValue),
ArgumentSpec.Full("seed", r's', ArgumentMode.RequiredValue)
]
try {
let pa = parseArguments(args, specs)
// 1. 抽查条数是必填项,还要能转成整数
var n: Int64 = 0
if (let Some(s) <- pa.options.get("count")) {
n = Int64.parse(s)
} else {
println("缺少抽查条数:-n <条数>")
return 1
}
if (n <= 0) {
println("抽查条数必须是正整数,收到:${n}")
return 1
}
// 2. 日志路径:给了 -i 就用,否则用默认
var path = "data/app.log"
if (let Some(p) <- pa.options.get("input")) {
path = p
}
// 3. 读日志,收进数组(跳过空行)
let raw = String.fromUtf8(File.readFrom(path))
let lines = ArrayList<String>()
for (line0 in raw.split("\n")) {
let line = line0.trim()
if (line != "") {
lines.add(line)
}
}
let total = lines.size
// 4. 要的比有的多:全部输出,只是打乱顺序
if (n >= total) {
println("日志共 ${total} 条,不足 ${n} 条,全部输出(已随机排序)")
n = total
}
// 5. 选随机源:给了 -s 用固定种子(可复现),否则用系统熵
var r: Random
if (let Some(s) <- pa.options.get("seed")) {
let sd = UInt64(Int64.parse(s))
r = Random(sd)
println("使用种子 ${sd}(同种子重跑结果相同)")
} else {
r = Random()
}
// 6. 部分洗牌(Fisher-Yates):把第 k 张与后面随机一张交换
for (k in 0..n) {
let swap = k + r.nextInt64(total - k)
let tmp = lines[k]
lines[k] = lines[swap]
lines[swap] = tmp
}
// 7. 打印抽中的前 n 条
println("---- 随机抽查 ${n} 条 ----")
for (k in 0..n) {
println(lines[k])
}
} catch (e: IllegalArgumentException) {
println("条数/种子必须是整数:${e.message}")
return 1
} catch (e: ArgumentParseException) {
println("参数错误:${e.message}")
println("用法:logpick -n <条数> [-i <日志路径>] [-s <种子>]")
return 1
} catch (e: Exception) {
println("运行出错:${e.message}")
return 1
}
0
}
7.3 六种运行场景
全部实测(cwd 为工程根目录)。场景一:固定种子,跑两遍结果分毫不差——这就是审计要的"可复查":
cjpm run -- -n 3 -s 7
使用种子 7(同种子重跑结果相同)
---- 随机抽查 3 条 ----
2026-10-09 08:30:15 [INFO] auth - 用户 user01 从 192.168.1.23 登录
2026-10-09 08:31:45 [WARN] auth - 密码重试过多: user07
2026-10-09 08:31:02 [ERROR] db - 连接超时: 192.168.1.10:5432
再跑一遍,输出与上面逐字一致(重放)。场景二:不带 -s,每次都是新结果——这是"例行抽查"要的新鲜感:
cjpm run -- -n 3
---- 随机抽查 3 条 ----
2026-10-09 08:32:20 [INFO] api - 请求 /api/orders 耗时 42ms
2026-10-09 08:30:15 [INFO] auth - 用户 user01 从 192.168.1.23 登录
2026-10-09 08:34:10 [INFO] auth - 用户 13812345678 修改了手机号 13998887766
(示例输出——不带种子每次运行都不同。)场景三:要得太多,全部输出并提示:
cjpm run -- -n 99
日志共 6 条,不足 99 条,全部输出(已随机排序)
---- 随机抽查 6 条 ----
2026-10-09 08:31:45 [WARN] auth - 密码重试过多: user07
2026-10-09 08:31:02 [ERROR] db - 连接超时: 192.168.1.10:5432
2026-10-09 08:34:10 [INFO] auth - 用户 13812345678 修改了手机号 13998887766
2026-10-09 08:32:20 [INFO] api - 请求 /api/orders 耗时 42ms
2026-10-09 08:33:05 [ERROR] api - 调用支付网关失败: 10.0.0.8
2026-10-09 08:30:15 [INFO] auth - 用户 user01 从 192.168.1.23 登录
场景四到六:三个入口校验,全部有友好提示:
cjpm run -- -n 0
抽查条数必须是正整数,收到:0
cjpm run -- -n abc
条数/种子必须是整数:The part of value convert failed.
cjpm run -- -i data/app.log
缺少抽查条数:-n <条数>
7.4 逐段看懂
- 步骤 1~2:第 3 课的标准动作——必填项检查、默认值兜底。注意
-n abc这种"选项格式对、值不是数"的情况argopt管不了(它只管配对,不管值的内容),所以解析后要自己Int64.parse,坏了落进IllegalArgumentException的 catch。 - 步骤 3:
ArrayList收集非空行,比先数一遍再开定长数组省事(ArrayList 的增删是第 14 课的老朋友)。 - 步骤 4:
n >= total时把 n 压到 total,后续洗牌逻辑不用写 if——先把非法态归约成合法态,主逻辑就只剩一条路。 - 步骤 5:
UInt64(Int64.parse(s))两跳转换,因为Random的种子参数是UInt64。 - 步骤 6:6.2 的部分洗牌原样落地,
total - k是"剩下的条数"。
八、常用 API 速查
| API | 说明 |
|---|---|
Random() |
系统熵种子,每次运行不同 |
Random(seed: UInt64) |
固定种子,序列完全可复现 |
r.seed |
查看种子(只读,不能赋值) |
nextInt64() / nextInt64(bound) |
全范围随机整数 / [0, bound)(bound 须为正,八种整型各有同款) |
nextBool() |
各半的真/假 |
nextFloat64() |
[0.0, 1.0) 小数(另有 Float32/Float16 版) |
nextGaussianFloat64(mean:, sigma:) |
正态分布,命名实参传参 |
nextBytes(count) / nextBytes(buffer) |
随机字节:新建数组 / 原地填充 |
nextBits(bound: UInt64) |
按位取随机数 |
概率三板斧:nextBool() 一半;nextFloat64() < p 任意概率 p;权重轮盘 dart < 累加权重 按份数分配。
九、常见问题 FAQ
Q1:文档里的全局 seed() 函数为什么调不了?
1.2.0 实测:直接调用报 undeclared identifier 'seed',显式导入报 'seed' is not accessible in package 'std.random'——它存在但没有公开。固定种子用 Random(seed) 构造器,效果一样。
Q2:Int64.parse("abc") 抛的是什么异常?为什么我 catch NumberFormatException 编译不过?
仓颉标准库没有 NumberFormatException 这个类型(实测 undeclared type name 'NumberFormatException')。解析失败抛的是 IllegalArgumentException,消息为 The part of value convert failed.,catch 它即可。
Q3:nextInt64(0) 会返回 0 吗?
不会,直接抛异常:IllegalArgumentException: Upper must be positive, got: 0.。bound 必须是正数,写循环抽样的前先确认上界合法。
Q4:为什么我两次运行 Random() 的结果不一样,同事电脑上跑也不一样?
Random() 用系统熵做种子,每次构造都是新起点。要复现就用 Random(42)——同种子同序列,跨机器也一致。
Q5:编译警告 function 'nextUInt8s' is deprecated 是怎么回事?
nextUInt8s 是被废弃的老接口,换 nextBytes(arr: Array<Byte>): Unit(原地填充)或 nextBytes(count)(新建数组)。
Q6:nextGaussianFloat64(170.0, 5.0) 为什么报错?
它的参数是命名参数,必须带前缀:nextGaussianFloat64(mean: 170.0, sigma: 5.0)。实测报错原文:error: missing argument prefix 'mean:' or 'sigma:' for named parameter。
十、课后练习
规矩不变:前 4 题必做,第 5 题选做。每题只考本课 1~2 个知识点,题面给全函数签名和 API 提示,先看懂题再动笔。
第 1 题(必做)掷骰子统计
模拟掷骰子 6000 次,统计每个点数出现的次数并打印。要求用 Random(3) 固定种子。
提示:nextUInt8(6) 返回 [0, 6) 的 UInt8,用 Int64(...) 转整再加 1 得到 1~6;用 Array<Int64>(6, repeat: 0) 计数。
import std.random.*
main(): Int64 {
let r = Random(3)
let counts = Array<Int64>(6, repeat: 0)
// 补全:循环 6000 次掷骰子并计数,然后打印每个点数的次数
0
}
期望输出:
1 点:971 次
2 点:1003 次
3 点:1072 次
4 点:945 次
5 点:1005 次
6 点:1004 次
第 2 题(必做)读程序写输出
不运行程序,写出输出结果;再运行验证。
import std.random.*
main(): Int64 {
let r = Random(42)
for (i in 0..3) {
println(r.nextInt64(1000))
}
0
}
期望输出:
662
428
144
要点:固定种子决定整条序列;r 是同一个实例,连续取值是沿序列推进,不是重复拿同一个数。
第 3 题(必做)三成概率走小路
迷宫岔路口:70% 走大道,30% 抄小道。用 Random(8) 模拟 1000 次选择,打印走小道的次数。
提示:r.nextFloat64() < 0.3 即"命中 30% 概率"。
import std.random.*
main(): Int64 {
let r = Random(8)
var hit = 0
// 补全:循环 1000 次,命中 0.3 概率就 hit += 1,最后打印
0
}
期望输出:
1000 次里命中 299 次
第 4 题(必做)年会展位抽奖
公司年会抽纪念品,奖池与权重:橡皮 10、笔记本 20、水杯 30、谢谢参与 40。用权重轮盘(4.3 节三步法)抽 1 次,打印抽中的奖品名。要求用 Random(5),输出唯一。
提示:Array<(String, Int64)> 存"奖品名 + 权重";先算总权重,再 nextInt64(总权重) 扔飞镖,从头累加权重判断飞镖落在哪段。
import std.random.*
main(): Int64 {
let prizes: Array<(String, Int64)> = [
("橡皮", 10), ("笔记本", 20), ("水杯", 30), ("谢谢参与", 40)
]
// 补全:总权重 → 扔飞镖 → 累加定位,打印"抽中:xxx"
0
}
期望输出:
抽中:笔记本
第 5 题(选做)整副洗牌
把 13 张牌 ["A", "2", "3", ..., "K"] 用 Fisher-Yates 完整洗乱后一行打印(空格分隔)。要求用 Random(99)。
提示:完整洗牌循环到 deck.size - 1(6.1 节原样代码);拼接输出用 var line = "" 累加。
import std.random.*
main(): Int64 {
let deck = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"]
let r = Random(99)
// 补全:完整 Fisher-Yates 洗牌 + 一行打印
0
}
期望输出:
9 Q 3 4 8 K 2 7 J 5 6 A 10
下节预告
随机数管"不可预测",下一课的 加密与哈希(std.crypto) 管恰恰相反的东西——不可逆:为什么密码不能明文存?文件下载完怎么校验"一个字节都没坏"?MD5、SHA-256 这些哈希算法做了什么、怎么用?下一课把"摘要"这件事讲透,顺手给 logpick 的日志文件做一个完整性校验。
系列说明:本系列基于 Windows 平台 + CIDE + 仓颉 SDK(1.2.0)编写,所有代码均已实际编译运行通过。如遇 SDK 版本差异导致的细节出入,以你本地版本为准,欢迎评论区交流。
💬 遇到问题?扫码联系作者
跟着课程练习时,如果在 SDK 安装、环境变量配置、编译报错或调试上卡住,欢迎扫码加作者企业微信直接咨询(请备注"仓颉课程"):

离线环境下图片可能加载不出来,也可以在 CIDE 菜单 Help ▸ 联系作者 / Contact 中查看同一张二维码(应用内置兜底图,无需联网)。
📥 工具下载
本系列全程使用的仓颉 IDE —— CIDE(免费开源、社区版):
- GitCode 仓库 / 安装包下载:https://gitcode.com/wp_upala/cide
- 打开页面后进入 发行版(Releases),两种包任选其一:
- 安装版:下载
CIDE-<版本>-x64-Setup.exe,双击安装,适合日常长期使用; - 免安装版(Portable):下载
CIDE-<版本>-x64-Portable.zip,解压到任意目录即用,不写注册表、不留安装痕迹,拷到 U 盘也能在别的电脑直接运行(包内附《使用说明.txt》)。适合先试用、或在受限电脑上学习本系列课程。
- 安装版:下载
- 仓颉 SDK 请前往仓颉编程语言官网下载:https://cangjie-lang.cn

浙公网安备 33010602011771号