【仓颉语言入门 · 第10课】
【仓颉语言入门 · 第10课】错误处理:异常机制与 Result
先说一句"对不起":第 9 课的 Q8 和下节预告明明写着"第 10 课讲
Result<T, E>的Ok(值)/Err(原因)"。但笔者在仓颉 SDK 1.1.3 下逐个敲进代码做实测时,编译器直接甩回来三句undeclared type name 'Result'、undeclared identifier 'Ok'——这个版本的标准库里根本还没有内置 Result。官方 V5 文档里的 Result 是更新版本的特性。所以这节课我们做两件事:第一,扎扎实实地学会仓颉当下唯一成熟的错误处理机制——异常:
throw、try-catch-finally、多 catch、异常传播、自定义异常;第二,学会 1.1.3 时代表达"可恢复错误"的正确姿势——用 Option 和自己手写的结果枚举达到 Result 同款效果,等你将来升级到带 Result 的 SDK,迁移只需换个名字。本文是系列第 10 课,所有代码与报错文案均在仓颉 SDK 1.1.3 下实测编译运行。途中会顺手澄清四个网上二手教程的高频坑:
NumberFormatException并不存在、自定义异常打印出来前缀是Exception、仓颉没有throws声明、也没有defer关键字。
目录(系列导航)
整套路线共 7 个模块、30 课,按每周 2~3 课的节奏,大约 2~3 个月可以走完一遍:
| 模块 | 课次 | 内容 |
|---|---|---|
| 一、环境与入门 | 01~05 | 环境搭建与 Hello World、变量与基本类型、运算符与输入输出、分支、循环 |
| 二、常用类型与数据组织 | 06~10 | 字符串、数组与区间、ArrayList/HashMap/HashSet、可空类型、错误处理 |
| 三、函数与函数式 | 11~14 | 函数、Lambda 与高阶函数、闭包、迭代器与惰性序列 |
| 四、面向对象与类型系统 | 15~20 | struct/class、构造与属性、接口、枚举与 match 模式匹配、泛型、扩展 |
| 五、工程化与标准库 | 21~25 | cjpm 包管理与多文件、文件 IO、JSON 处理、网络编程、单元测试 |
| 六、并发编程 | 26~28 | 线程、Channel 通道与同步原语、并发实战 |
| 七、项目实战 | 29~30 | 命令行小工具、GeoJSON 数据处理实战 |
- 环境搭建与第一个仓颉程序
- 变量与常量:let / var 与基本数据类型
- 运算符与标准输入输出
- 分支结构:if 与 match 表达式
- 循环结构:while / for / Range
- 字符串详解与字符串插值
- 数组 Array 与区间 Range
- 集合框架:ArrayList、HashMap、HashSet
- 可空类型
?与 Option - 错误处理:异常机制与 Result(本文)
- 函数定义、参数与返回值
- Lambda 与高阶函数
- 闭包、作用域与函数类型
- 迭代器 Iterator 与 Sequence
- 结构体 struct 与类 class
- 构造函数、属性与方法
- 接口 interface 与实现
- 枚举 enum、代数数据类型与 match 模式匹配
- 泛型编程
- 扩展、类型别名与可见性控制
- cjpm 包管理与多文件项目组织
- 文件与目录 IO
- JSON 处理(结合 stdx 扩展库)
- 网络编程入门
- 单元测试
- 并发基础:线程的创建与等待
- Channel 通道与同步原语
- 并发实战:多线程任务处理
- 实战一:带文件持久化的命令行小工具
- 实战二:GeoJSON 数据处理程序
一、程序出错的两条路线
程序出错从来不稀奇:用户输入了 "abc" 当年龄、数组下标越界、HashMap 里没有这个键、整数除以了零。语言设计上处理这些"出错"有两条经典路线。
路线一:错误码(返回值)。 C 语言的老传统——函数返回一个整数,0 表示成功,非 0 表示错误码。问题在于:调用方完全可以无视返回值,于是错误被悄悄吞掉,程序带着错误状态继续跑,直到十万八千里外莫名其妙地崩溃。
路线二:异常(exception)。 出错时不是"返回",而是用 throw 抛出一个异常对象;当前函数立刻停下来,异常沿着调用链一路向上找接手的人;找到就进入 catch 块处理,找不到程序就终止并打印现场。错误再也无法被"不小心忽略"。
仓颉两条路线都提供,而且把路线一升级成了类型安全的版本:
- 异常:
throw/try-catch,适合表示"意外"——程序逻辑没料到、继续往下走已经不安全的情况; - Option 与结果枚举:第 9 课的 Option 回答"有没有值";本课第六节会手写一个结果枚举,回答"成功还是失败、为什么失败",让编译器像检查 Option 一样逼着调用方处理失败。
这节课前半段先把异常学透,后半节回到结果枚举。
二、抛出异常:throw 与异常家族
2.1 最常见的几个标准异常
仓颉的异常都继承自 Exception,异常对象里携带一条人能看懂的 message。下面四个是前 10 课里你已经或即将撞到的"老朋友",触发方式和消息文案全部实测:
| 异常类型 | 触发场景 | 实测 message |
|---|---|---|
IllegalArgumentException | Int64.parse("abc") 等转换失败 | The part of value convert failed. |
IndexOutOfBoundsException | 数组/列表下标越界 | The length of the array is 3, but the index is 9. |
NoneValueException | 对 None 调 getOrThrow()、用 [] 读不存在的 HashMap 键 | Value does not exist! |
ArithmeticException | 整数除以零 | Divided by zero! |
另外,文件系统相关的错误由 std.fs 包中的 FSException 体系表示,第 22 课讲文件 IO 时再细聊。
2.2 用 throw 主动抛出
throw 后面直接跟一个异常对象(仓颉没有 new 关键字):
import std.convert.*
func parseAge(text: String): Int64 {
let age = Int64.parse(text) // 非数字 -> 抛 IllegalArgumentException
if (age < 0 || age > 150) {
throw IllegalArgumentException("年龄 ${age} 不合常理")
}
return age
}
注意第二行的 Int64.parse("abc")——
🚫 辟谣:很多从 Java/Kotlin 抄来的二手文章会写
NumberFormatException。仓颉 1.1.3 没有这个类型,字符串转整数失败实际抛的是IllegalArgumentException,消息为The part of value convert failed.。catch 时写错异常类型,异常就会从你手边溜过去。
2.3 和 Java 的关键区别:没有 throws 声明
Java 要求函数在签名上声明"我可能抛这些受检异常"(void f() throws IOException),调用方不 catch 就编译不过。仓颉完全没有这套机制:
- 任何普通函数里都可以直接
throw,不需要任何声明; - 调用方不强制 catch——不接,异常就自动向上传播(第五节细讲)。
少写了样板代码,代价是你需要自觉地在"程序边界"处理异常,而不是假装它们不存在。
三、接住异常:try-catch-finally
3.1 基本写法
class AgeException <: Exception {
init(msg: String) {
super(msg)
}
}
main(): Int64 {
try {
throw AgeException("测试异常")
} catch (e: AgeException) {
println("捕获 AgeException:${e}")
println("message = ${e.message}")
} finally {
println("finally 总是执行")
}
return 0
}
运行结果(SDK 1.1.3 实测):
捕获 AgeException:Exception: 测试异常
message = 测试异常
finally 总是执行
三段职责分明:
try { ... }:把可能出错的代码放进去;catch (e: 异常类型) { ... }:接住类型匹配的异常,e就是异常对象,e.message取出消息字符串;finally { ... }:无论是否发生异常都会执行,常用于清理现场。
⚠️ 先记住这个"怪现象":上面输出第一行是
Exception: 测试异常,而不是AgeException: 测试异常。SDK 1.1.3 中自定义异常直接插值打印,前缀固定显示父类名Exception(内置异常则正常显示自己的名字,见后文)。想在日志里区分异常,可靠的做法是取e.message。第六节会专门讲这个坑。
3.2 catch (_):我只关心"出事了"
不关心异常内容时,catch 的变量名可以写成下划线 _,语义是"接住,但别给我绑定变量":
func atIndex(a: Array<Int64>, i: Int64): Int64 {
return a[i]
}
main(): Int64 {
try {
println("${atIndex([1, 2, 3], 9)}")
} catch (_) {
println("越界了,但我不关心细节")
}
return 0
}
越界了,但我不关心细节
3.3 finally:连 return 都拦得住
finally 最硬的一条保证:即使 try 或 catch 里写了 return,返回动作也会先让 finally 执行完。
func foo(): Int64 {
try {
return 1
} finally {
println("finally:即使 return 也会先执行我")
}
}
main(): Int64 {
println("foo() = ${foo()}")
return 0
}
finally:即使 return 也会先执行我
foo() = 1
正因为有这条保证,关闭文件、释放连接这类"必须善后"的代码才适合放进 finally。
📌 三条牢记:① try 块正常走完 → finally 执行;② try 里抛异常被 catch → finally 执行;③ try/catch 里
return→ 先执行 finally,再真正返回。唯一"拦不住"的是进程被杀掉这种极端情况。🚫 另外提前辟个谣:仓颉 1.1.3 没有
defer关键字(实测defer { ... }报undeclared identifier 'defer'),也没有 Java 的 try-with-resources 语法。新版本文档里出现的defer是后续版本特性,在 1.1.3 上做善后,老老实实用 finally。
四、try 是表达式;多 catch 要"先窄后宽"
4.1 try 像 if、match 一样有返回值
第 4、9 课学过 if 和 match 都是表达式,try 也是:try 分支和 catch 分支产生的值,就是整个 try 表达式的值。
main(): Int64 {
let r = try {
100 / 2
} catch (e: Exception) {
-1
}
println("r = ${r}")
return 0
}
r = 50
注意两个分支的值类型要一致(这里都是 Int64)。一旦除法抛异常,r 就会得到兜底值 -1。
4.2 多 catch:一场自上而下的"类型匹配"
一个 try 后面可以挂多个 catch,规则只有一条:从上到下,进入第一个类型匹配的块。因此正确顺序是先具体(窄)、后宽泛(父类):
func atIndex(a: Array<Int64>, i: Int64): Int64 {
return a[i]
}
main(): Int64 {
let data = [10, 20, 30]
try {
println("${atIndex(data, 9)}")
} catch (e: IndexOutOfBoundsException) {
println("窄:下标异常 -> ${e.message}")
} catch (e: Exception) {
println("宽:其他异常兜底 -> ${e.message}")
}
return 0
}
窄:下标异常 -> The length of the array is 3, but the index is 9.
4.3 顺序写反会怎样?
如果把 catch (e: Exception) 放到最前面,它会把所有异常一网打尽,后面的窄 catch 永远轮不到。编译器不会报 error,但会给一个 warning(实测文案):
warning: useless exception type
==> ...src/main.cj:13:17:
|
13 | } catch (e: IndexOutOfBoundsException) {
| ^
|
# note: this warning can be suppressed by setting the compiler option `-Woff unused`
useless exception type(无用的异常类型)——看到它就知道 catch 顺序写反了。这和第 4 课 match 的"分支要互斥、顺序要从细到粗"是同一种思维。
💡 为什么示例里下标访问要包一层
atIndex函数?因为仓颉会对编译期就能算出的常量下标做静态检查(第 9 课 6.1 节):直接写[1,2,3][9],编译器当场报array index 9 is past the end,根本走不到运行时 catch。把下标变成函数参数,检查就推迟到运行期,我们才能演示 catch。整数除零的演示同理(见 FAQ Q7)。
五、异常的传播:不接,就一路向上
5.1 调用链上的"击鼓传花"
函数 A 调 B、B 调 C,C 里抛出异常,而 B、A 都没 catch 时会怎样?异常会沿着 C → B → A → ... → main 的调用链一路传播,直到某一层接住:
import std.convert.*
func parseAge(text: String): Int64 {
let age = Int64.parse(text)
if (age < 0 || age > 150) {
throw IllegalArgumentException("年龄 ${age} 不合常理")
}
return age
}
func inputAge(text: String): Int64 {
println("inputAge:收到原始输入 ${text}")
return parseAge(text) // 不 catch,继续向上抛
}
main(): Int64 {
for (text in ["25", "abc", "999"]) {
try {
let age = inputAge(text)
println(" 录入成功:${age}")
} catch (e: Exception) {
println(" main 兜底:${e.message}")
}
}
return 0
}
inputAge:收到原始输入 25
录入成功:25
inputAge:收到原始输入 abc
main 兜底:The part of value convert failed.
inputAge:收到原始输入 999
main 兜底:年龄 999 不合常理
parseAge 抛出的异常穿过毫无防备的 inputAge,最后在 main 的边界被统一接住。这就是异常相比错误码的优势:中间层不用写任何样板代码,错误也不会丢。
5.2 谁都不接:默认处理与堆栈
如果一路传到 main 都没人 catch,仓颉运行时会执行默认处理:打印 An exception has occurred:、异常类型与消息,以及完整的调用堆栈,然后终止程序。上面的 "abc" 例子若去掉 try-catch,实测输出为:
An exception has occurred:
IllegalArgumentException: The part of value convert failed.
at std.convert::parseCompInt(std.core::Array<...>, UInt64, UInt64, UInt64, Bool)(std/convert\parsable.cj:0)
at std.convert::Int64::parse(std.core::String)(std/convert\parsable.cj:752)
at lesson10::main()(.../src/main.cj:6)
最后一帧是你的用户代码,文件路径随工程所在位置变化;前两帧来自标准库。堆栈从下往上读:main → Int64::parse → parseCompInt,事故现场一目了然。这也是程序崩溃时你第一个该看的东西。
5.3 catch 住又想继续抛:rethrow
有时中间层想"留个记号"(记日志、打点),但并不打算真正处理异常,可以在 catch 块里用 throw 异常对象 把它重新抛出去:
main(): Int64 {
try {
try {
throw IllegalArgumentException("原始问题")
} catch (e: IllegalArgumentException) {
println("内层先记下日志,再向外抛")
throw e // 重新抛出
}
} catch (e: Exception) {
println("外层兜底:${e.message}")
}
return 0
}
内层先记下日志,再向外抛
外层兜底:原始问题
📌 经验法则:只在你真的能处理(换默认值、重试、提示用户重输)时才 catch;处理不了就让它传播。层层 catch 又层层吞掉,是比不 catch 更糟糕的代码。
六、自定义异常与"可恢复错误"惯用法
6.1 自定义异常:继承 Exception 即可
标准异常表达不了业务语义时(比如"余额不足"“年龄越界”“库存为负”),就自己定义一个。写法和官方库 FSException 完全同款——定义一个 Exception 的子类,构造器把消息转给父类:
class AgeException <: Exception {
init(msg: String) {
super(msg)
}
}
需要的话也可以提供无参构造器(与官方库 FSException 的定义方式一致):
public class AgeException <: Exception {
public init() {
super()
}
public init(message: String) {
super(message)
}
}
两种构造方式分别抛出并捕获,实测打印:
try {
throw AgeException("年龄非法")
} catch (e: AgeException) {
println("${e}")
}
try {
throw AgeException()
} catch (e: AgeException) {
println("无参构造:[${e}],message 为空串? [${e.message}]")
}
Exception: 年龄非法
无参构造:[Exception],message 为空串? []
⚠️ 再强调一遍这个 1.1.3 实测怪癖:自定义异常对象用
${e}插值或打印,显示的前缀是父类名Exception,而不是你起的类名(内置异常如ArithmeticException则会正确显示自己的名字)。无参构造时 message 是空字符串。因此:
- 给用户看的信息,一律用
e.message,并在消息文本里写足上下文(“年龄 -3 超出 0~150”);- 想在日志中区分异常种类,靠 catch 的类型(多 catch 各进各的块),不要依赖打印前缀。
等后续版本修复这个表现后,本节描述可能不再适用——以你本地版本实测为准。
什么时候值得自定义异常?当调用方需要按类型区分对待时:catch (AgeException) 提示重新输入,catch (IllegalArgumentException) 提示格式错误。图省事全抛 IllegalArgumentException 也能跑,只是调用方无法精细分辨。
6.2 1.1.3 没有 Result?三分钟自己造一个
先看"案发现场"。如果你照着新版官方文档或某些教程写:
let r: Result<Int64, String> = Ok(42) // ❌ 1.1.3 编译不过
let e: Result<Int64, String> = Err("坏了")
编译器三连:
error: undeclared type name 'Result'
error: undeclared identifier 'Ok'
error: undeclared identifier 'Err'
别怀疑自己的环境——Result 在 1.1.3 标准库中确实尚不存在。但 Result 本质上一点也不神秘,它就是一个枚举。我们用第 4 课见过、第 18 课才会细讲的 enum,三行就能造出同款:
enum ParseResult {
Ok(Int64) | Err(String)
}
语法先照抄(enum 的系统讲解在第 18 课):花括号里列出这个类型所有的"构造器",用 | 分隔;括号里只写载荷的类型,不写名字。Ok(Int64) 表示成功并携带一个整数,Err(String) 表示失败并携带一条原因。
使用时配合异常→枚举的"边界转换"和 match:
import std.convert.*
enum ParseResult {
Ok(Int64) | Err(String)
}
func parseInt(s: String): ParseResult {
try {
return ParseResult.Ok(Int64.parse(s))
} catch (e: IllegalArgumentException) {
return ParseResult.Err("${s} 不是合法整数")
}
}
main(): Int64 {
for (s in ["42", "3.14"]) {
match (parseInt(s)) {
case ParseResult.Ok(v) => println("${s} -> Ok(${v})")
case ParseResult.Err(msg) => println("${s} -> Err(${msg})")
}
}
return 0
}
42 -> Ok(42)
3.14 -> Err(3.14 不是合法整数)
这就是 Result 的全部思想:把失败塞进返回值类型里,match 不写全 Err 分支就编译不过,想忘记处理失败都难——和第 9 课 Option 逼你处理 None 一模一样。将来升级到内置 Result 的 SDK,把自定义枚举换成标准 Result<T, E> 即可,调用处的 match 思路完全不变。
6.3 三种手段怎么选
| 手段 | 回答的问题 | 适用场景 |
|---|---|---|
Option / ?T | 有没有值? | 查字典、找元素、可空字段(第 9 课) |
| 手写结果枚举(未来的 Result) | 成功还是失败?为什么? | 输入校验、网络/解析等预期内的业务失败,调用方应当逐一处理 |
| 异常 | 出意外了,流程已不安全 | 数组越界、整数除零、不变量被破坏等异常情况;或在底层简化控制流,到边界再转换成枚举 |
一个务实的组合模式(下一节综合实战就是它):底层函数大胆 throw,省去层层判空;在靠近用户的边界函数统一 catch,转换成结果枚举;最外层用 match 平和地处理两种结局。
七、CIDE 实操:健壮的年龄录入程序
7.1 编写程序
新建工程(或单文件),输入下面的程序。它把本节课全部要素串了起来:自定义异常、parse 异常、多 catch(先窄后宽)、finally 善后、异常→手写结果枚举的边界转换、match 处理。
import std.convert.*
// ① 自定义异常:年龄超出业务范围
class AgeException <: Exception {
init(msg: String) {
super(msg)
}
}
// ② 手写"结果枚举":成功携带年龄,失败携带原因
enum AgeResult {
Ok(Int64) | Err(String)
}
// ③ 核心逻辑:可能抛两种异常
func checkAge(text: String): Int64 {
let age = Int64.parse(text) // 非数字 -> IllegalArgumentException
if (age < 0 || age > 150) {
throw AgeException("年龄 ${age} 超出 0~150") // 业务错误 -> AgeException
}
return age
}
// ④ 边界层:把异常"翻译"成 AgeResult,调用方就不用碰异常
func inputAge(text: String): AgeResult {
try {
let age = checkAge(text)
return AgeResult.Ok(age)
} catch (e: AgeException) {
return AgeResult.Err("业务校验:${e.message}")
} catch (e: IllegalArgumentException) {
return AgeResult.Err("输入格式:${text} 不是整数")
} finally {
println(" ([${text}] 处理完毕)")
}
}
main(): Int64 {
for (text in ["25", "abc", "-3", "200"]) {
match (inputAge(text)) {
case AgeResult.Ok(age) => println("录入成功:${age} 岁")
case AgeResult.Err(msg) => println("录入失败:${msg}")
}
}
return 0
}
7.2 运行与验证
在 CIDE 中点运行(或 cjpm run),应得到下面的输出(已逐字核对):
([25] 处理完毕)
录入成功:25 岁
([abc] 处理完毕)
录入失败:输入格式:abc 不是整数
([-3] 处理完毕)
录入失败:业务校验:年龄 -3 超出 0~150
([200] 处理完毕)
录入失败:业务校验:年龄 200 超出 0~150
注意每行的先后顺序:finally 在函数 return 之前执行,所以"处理完毕"总是先打出来,回到 main 后才打印"录入成功/失败"。
可以自己动手改:
- 把 ④ 里两个 catch 的顺序对调(
IllegalArgumentException在前),观察类型不同、互不为父子类时输出为何不变;再把其中一个换成父类Exception放到最前,重新编译,亲眼看到warning: useless exception type; - 删掉 ④ 里任意一个 catch 分支,分别用
"abc"和"-3"测试,观察异常如何"漏"到下一层(本例 main 没接,会直接触发默认处理并终止); - 删掉
match的case AgeResult.Err(...)分支,编译器立即报错——再体会一次"类型系统逼你处理失败"; - 把 finally 块整段删掉,对比输出顺序,理解 finally 的善后时机。
7.3 用调试器看清异常的传播
- 在 ③ 的
throw AgeException(...)一行打断点,Shift + F9 启动调试; - 循环到
"-3"时断住,Variables 面板能看到局部变量age = -3;按 F8 单步,异常抛出后当前函数立即结束,程序不会顺序执行下一行; - 观察左侧 Call Stack(调用堆栈) 面板:
checkAge→inputAge→main的帧依次排列——这就是异常"向上爬"的那条链;双击各帧可以回看每一层的变量值; - 在 ④ 的
catch (e: AgeException)内打断点,断住时在 Variables 中展开e,确认message的内容正是"年龄 -3 超出 0~150"; - 继续运行(F5)到
"abc",观察这次进入的是另一个 catch 分支。
调试结束点停止按钮。
八、常见问题 FAQ
Q1:为什么写 Result<Int64, String> / Ok(...) / Err(...) 报 undeclared?
因为 SDK 1.1.3 的标准库还没有内置 Result 类型(官方 V5 文档对应的是更新版本)。按 6.2 节用 enum XxxResult { Ok(T) | Err(E) } 手写,配合 match 使用,思想和写法与未来的 Result 完全一致。
Q2:Int64.parse("abc") 抛的是 NumberFormatException 吗?
不是。1.1.3 没有这个异常类型,实际抛出 IllegalArgumentException: The part of value convert failed.(堆栈位于 std/convert/parsable.cj)。catch 时请按真名书写。
Q3:函数需要像 Java 那样声明 throws 吗?调用方不 catch 会编译报错吗?
都不会。仓颉没有受检异常机制:函数内可以直接 throw,无需声明;调用方也不强制 catch。未被接住的异常会沿调用链自动传播,直到被接住或由运行时默认处理终止程序。
Q4:多 catch 时编译器警告 useless exception type 是什么意思?
说明你把父类异常(通常是 Exception)的 catch 写到了子类前面,后面的 catch 永远不会被执行。调整为先具体、后宽泛即可。
Q5:自定义异常打印出来为什么是 Exception: xxx 而不是我的异常类名?
这是 SDK 1.1.3 的实测表现:自定义异常的字符串形式前缀固定为 Exception,内置异常则正常显示自身类名。给用户看的信息请用 e.message,并在消息里写足上下文;区分异常种类靠 catch 的类型而非打印文本。
Q6:仓颉有 defer 或 try-with-resources 吗?资源清理写在哪?
1.1.3 都没有(defer 实测报 undeclared identifier 'defer')。善后清理统一写在 finally 块中,它在正常返回、异常抛出、提前 return 三种情况下都会执行。
Q7:整数除零和浮点除零行为一样吗?
不一样,差别很大:
- 整数除以零(下标/除数来自运行时变量、绕过编译期检查时)抛
ArithmeticException: Divided by zero!;若编译器能直接算出除数是常量 0,会在编译期就报error: divide by zero拒绝编译; - 浮点除以零不抛异常:
10.0 / 0.0得到inf,0.0 / 0.0得到nan(符合 IEEE 754 规范)。
Q8:到底该抛异常还是返回 Option / 结果枚举?
判断标准是"失败是否在预期之内"。用户输入非法、查不到数据、网络瞬时抖动这类调用方本就该处理的可恢复失败,用返回值(Option 或结果枚举),让编译器帮你盯着;数组越界、整数除零、对象处于不可能状态这类程序 bug 或不变量破坏,用异常快速暴露。两者可以配合:底层抛异常简化逻辑,边界层 catch 后转换成结果枚举交给上层。
九、课后练习
- 仿照 6.2 节定义
enum ParseResult { Ok(Int64) | Err(String) },实现func tryParse(s: String): ParseResult:内部用 try-catch 包住Int64.parse。用"123"、"3.14"、"abc"三组输入测试,match打印形如123 -> Ok(123)、abc -> Err(abc 不是合法整数)的结果。 - 自定义
InsufficientBalanceException(继承Exception,携带消息)。写一个Account类,持有余额字段,提供withdraw(amount: Int64)方法:余额不足时throw InsufficientBalanceException("余额只剩 X,却要取 Y");取款成功则扣减并返回新余额。在 main 里准备 100 元的账户,分别取 30 和 200,用 catch 接住异常并只打印e.message。 - 准备一个会产生两类异常的程序:用
atIndex函数制造IndexOutOfBoundsException,用Int64.parse制造IllegalArgumentException,各挂一个专门的 catch,最后再加catch (e: Exception)兜底。确认正常后,把兜底 catch 移到最前面重新编译,记录useless exception type警告,再改回正确顺序。 - 写
func process(ok: Bool): Int64:try 块中ok为true时返回1,为false时抛IllegalArgumentException("故意失败");finally 块打印"资源已释放"。分别调用process(true)和process(false)(后者外面套 try-catch),验证两条路径下"资源已释放"都会打印,且正常路径的返回值仍是1。 - 打开 7.1 的程序 Shift + F9 调试:在
throw AgeException(...)行断点,断住后查看 Call Stack 中checkAge → inputAge → main三层栈帧;再在两个 catch 分支都打断点,分别用"-3"和"abc"触发,截图或口述两次进入的分支与 Variables 中e.message的差异。
下节预告
这节课我们一直在"用"函数(parseAge、inputAge、main),但函数到底有多少讲究,一直没系统讲过。第 11 课学习函数定义、参数与返回值:参数默认值、命名参数、可变参数、多返回值、Unit 返回类型、函数重载,以及仓颉函数"一等公民"特性的第一印象。我们下节课见!
系列说明:本系列基于 Windows 平台 + CIDE + 仓颉 SDK(1.1.3)编写,所有代码均已实际编译运行通过。如遇 SDK 版本差异导致的细节出入(例如新版本已内置 Result、defer 或修复了自定义异常的打印前缀),以你本地版本为准,欢迎评论区交流。
💬 遇到问题?扫码联系作者
跟着课程练习时,如果在 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
更多推荐


所有评论(0)