【仓颉语言入门 · 第二篇 第 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 服务端、多线程日志分析器、简易爬虫
  1. 正则表达式(std.regex)
  2. 时间日期处理(std.time)
  3. 命令行参数解析(std.argopt)
  4. 随机数与概率(本文)
  5. 加密与哈希(std.crypto)
  6. 二进制数据处理(std.binary)
  7. 数据库入门:std.database.sql 与 CRUD
  8. 预编译、事务与防注入
  9. 手写 DAO 数据访问层
  10. 数据库综合小实战
  11. 反射:在运行时打量类型
  12. 宏与 deriving:让编译器帮你写代码
  13. 与 C 互操作:FFI
  14. 并发容器与对象池
  15. 实战三:HTTP 服务端
  16. 实战四:多线程日志分析器
  17. 实战五:简易爬虫

一、为什么需要随机数

先数数我们身边的"随机需求":

  • 抽奖:从 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
posted @ 2026-10-13 00:37  upala  阅读(16)  评论(0)    收藏  举报