仓颉编程语言青少年基础教程:异常处理

异常(Exceptions)是一类特殊的可以被程序员捕获并处理的错误,是程序执行时出现的一系列不正常行为的统称。例如,要读取使用的文件不存在、非法输入、除零错误、数组越界等。如果不处理这些异常,程序可能会崩溃,终止运行。仓颉语言通过 try、catch、throw 和 finally 等关键字,帮助我们优雅地处理这些问题,用来保证系统的正确性和健壮性。

先看一个简单示例:

import std.fs.*

main(): Int64 {
    let p = Path("./no_such.txt")  //
    try {
        let f = File(p, Read)   // 若文件不存在会抛 FSException
        println("文件可以打开")
        f.close()
    } catch (e: FSException) {
        println("捕获到异常:${e.message}")
    }
    return 0
}

在仓颉语言中,异常类包括 Error 和 Exception:

  • Error 类描述仓颉语言运行时,系统内部错误和资源耗尽错误。应用程序不应该抛出这种类型错误,如果出现内部错误,只能通知给用户,尽量安全终止程序。——在编译时发现错误,属于静态防御(Static Defense)。

  • Exception 类描述的是程序运行时(Runtime)的逻辑错误或者 IO 错误导致的异常,例如数组越界或者试图打开一个不存在的文件等,这类异常需要在程序中捕获处理。——在运行时发现异常,属于动态防御(Dynamic Defense)。

开发者不可以通过继承仓颉语言内置的 Error 或其子类来自定义异常,但是可以继承内置的 Exception 或其子类来自定义异常。

:此处官方文档所说的数组越界——当数组的索引是变量且编译期无法确定其值是否越界的情况。

在仓颉数组的下标访问中,对数组的下标越界访问也有安全检查。当上下文足以静态分析的时候,下标访问可提前在编译期检测出来,编译器会直接给出报错;当上下文不足以静态分析的时候,下标访问会在运行时做一个检查,如果溢出会抛出运行时异常。

——见https://cangjie-lang.cn/docs?url=%2F0.53.18%2Fwhite_paper%2Fsource_zh_cn%2Fcj-wp-dynamiccheck.html 相关部分。

现象描述如下:

main(): Int64 {
    let arrayTest: Array<Int64> = [0, 1, 2]
    
    try {
         println("arrayTest[2] = ${arrayTest[2]}")
         println("arrayTest[3] = ${arrayTest[3]}")  //?! 越界错误捕获不到
    } catch (_) {
        println("catch an exception!")
    }

    return 0
}

这段代码?! 处的错误为何捕获不到,而报错,为什么?

解释:对于编译期可直接判定的数组越界(比如你代码中arrayTest[3],数组长度为 3,索引 3 明显超出0~2范围),仓颉编译器会在编译阶段直接报错,终止编译流程 —— 此时程序还未开始运行,try-catch代码块自然无法发挥作用。

这是一种 “提前防御”或称为静态防御(Static Defense)的设计:对于一眼就能看出的逻辑错误(如硬编码的越界索引),编译器直接拦截,避免程序带着明显错误运行。

代码修改如下:

import std.convert.*

main(): Int64 {
    let arr: Array<Int64> = [0, 1, 2]
    
    print("请输入要访问的索引(0-2):")
    let input = readln()
    
    try {
        let index = Int64.parse(input)
        
        // 手动检查索引范围,主动抛出包含详细信息的异常
        if (index < 0 || index >= arr.size) {
            throw Exception("数组越界:索引 ${index} 超出范围(数组长度为 ${arr.size},合法索引 0~${arr.size-1})")
        }
        
        println("arr[${index}] = ${arr[index]}")
    } catch (e: Exception) {
        println("捕获到异常:${e.message}")
    }
    
    return 0
}

就样就能捕获并展示数组越界异常。这段代码体验了仓颉语言 “防御式编程”。

添加的 if 检查和 throw 语句,正是 “动态防御” 的关键 —— 主动预判可能的错误(用户输入越界),并将其转化为可被 try-catch 处理的异常,这正是 “防御式编程” 的核心思想:“不要假设一切都会按预期运行,而是主动预判风险并处理”。对于在编译期无法确定、依赖于运行时数据(用户输入、网络返回、文件内容等)的潜在错误,通过抛出异常的方式提供给开发者一个捕获和处理的机会,保证程序不会因此崩溃。

仓颉的这种处理方式,本质是 “静态防御”+“动态防御” 的双重保险。

throw 和try 表达式

异常处理主要涉及:

  •  根据是否涉及资源的自动管理,将 try 表达式分为两类:不涉及资源自动管理的普通 try 表达式,以及会进行资源自动管理的 try-with-resources 表达式。

  •  throw 表达式(throw expression)由关键字 throw 以及尾随的表达式组成。尾随表达式的类型必须继承于 Exception 或 Error 类。

throw 负责“抛出”问题,try-catch 负责“接收并处理”问题。

throw的语法和用法

基本语法

throw 异常对象

说明,throw 用于 主动抛出异常对象,语法极其简单,但有两点必须牢记:

(1).只能抛 Exception 及其子类(Error 类不允许手动 throw)。

(2).抛出的是 对象,不是类名,也不是字符串字面量。

常见错误写法:

throw Exception              // ❌ 不是对象

throw "something wrong"      // ❌ 字符串不行

throw IO.Error               // ❌ Error 子类不允许手动抛

示例:整数四则运算计算器

import std.convert.*

/* ===================== 自定义异常 ===================== */
class InvalidInputException <: Exception {
    public init(msg: String) {
        super(msg)
    }
}

class DivideByZeroException <: Exception {
    public init() {
        super("除数不能为零")
    }
}

class UnsupportedOperatorException <: Exception {
    public init(op: String) {
        super("不支持的运算符:${op}")
    }
}

/* ===================== 核心业务 ===================== */
// 把字符串变成 Int64,失败就抛
func parseIntStrict(s: String): Int64 {
    try {
        return Int64.parse(s)
    } catch (e: Exception) {
        throw InvalidInputException("整数格式错误:${s}")
    }
}

// 计算核心:一旦非法立即 throw
func calculate(a: Int64, op: String, b: Int64): Int64 {
    match (op) {
        case "+" => a + b
        case "-" => a - b
        case "*" => a * b
        case "/" =>
            if (b == 0) {throw DivideByZeroException()}
            a / b
        case _   => throw UnsupportedOperatorException(op)
    }
}

/* ===================== 命令行交互 ===================== */
main() {
    println("=== 命令行整数计算器( 运算符op 支持 +  -  *  / ;exit 退出)===")
    println("请输入表达式,格式:a op b (例:123 + 456)")

    while (true) {
        print(">>> ")
        let line = readln()
        if (line.isEmpty()) {
            continue
        }
        if (line == "exit") {
            println("再见!")
            break
        }

        /* --------- 解析 --------- */
        let parts = line.split(" ")
        if (parts.size != 3) {
            println("❌ 格式错误!请用空格分隔:a op b")
            continue
        }

        /* --------- 业务 + 异常捕获 --------- */
        try {
            let a  = parseIntStrict(parts[0])
            let op = parts[1]
            let b  = parseIntStrict(parts[2])

            let result = calculate(a, op, b)
            println("结果:${result}")
        } catch (e: InvalidInputException) {
            println("❌ 输入非法:${e.message}")
        } catch (e: DivideByZeroException) {
            println("❌ 除零错误:${e.message}")
        } catch (e: UnsupportedOperatorException) {
            println("❌ 运算符错误:${e.message}")
        } catch (e: Exception) {
            println("❌ 未知错误:${e.message}")
        }
    }
}

编译运行截图:

try 表达式

仓颉通过try表达式处理异常,分为普通 try(无资源自动管理)和try-with-resources(自动释放资源)两种。

1.普通 try 表达式

可以捕获并处理异常,防止程序崩溃。包括三个部分:try 块,catch 块和 finally 块。基本语法:

try {
    // 可能出错的代码
} catch (e: 异常类型) {
    // 处理异常
} finally {
    // 无论是否出错都会执行(可选)
}

说明:

  • try 块:存放可能抛出异常的代码;

  • catch 块:捕获并处理异常(可多个,按 “子类在前、父类在后” 顺序,否则报 “不可达” 警告);

  • finally 块:无论是否抛出异常,必执行(用于释放资源、清理操作)。

注意:

  • 无 catch 块时,必须有 finally 块;

  • 有 catch 块时,finally 块可选;

  • try、catch 块的作用域相互独立,变量不共享。

示例

// 模拟除法运算,处理除零异常(假设仓颉内置ArithmeticException)
func divide(a: Int64, b: Int64): Int64 {
    if (b == 0) {
        throw ArithmeticException("除零错误:除数不能为0") // 抛出算术异常
    }
    return a / b
}

main() {
    try {
        let result = divide(10, 0) // 触发除零异常
        println("计算结果:${result}")
    } catch (e: ArithmeticException) {
        println("捕获异常:${e.toString()}") // 调用toString()输出异常类型+信息
    }
    println("程序继续执行……")
}

编译运行截图:

示例2:从键盘读入数值,并保证用户输入1到l00之间的整数。

import std.convert.*  // 用于 Int64.parse() 函数

// 函数:从键盘读取1-100之间的整数(循环重试直到输入合法)
func readIntBetween1And100(): Int64 {
    var inputValid = false  // 标记输入是否合法
    var result: Int64 = 0   // 存储最终合法的输入值

    // 循环重试:直到输入合法才退出
    while (!inputValid) {
        // 1. 提示用户输入
        print("请输入1到100之间的整数:")
        // 读取键盘输入(readln() 直接返回 String)
        let inputStr = readln()

        // 2. 处理“无输入”情况(空串用 isEmpty() 判断)
        if (inputStr.isEmpty()) {
            println("错误:未输入任何内容,请重新输入!")
            continue  // 跳过后续逻辑,直接进入下一次循环
        }

        // 3. 尝试将输入字符串转换为整数(处理格式错误)
        try {
            // 将字符串转为Int64(若不是纯数字会抛出异常)
            result = Int64.parse(inputStr)
            
            // 4. 校验数值是否在1-100范围内
            if (result >= 1 && result <= 100) {
                inputValid = true  // 输入合法,标记为true以退出循环
                println("输入正确!您输入的数值是:${result}")
            } else {
                println("错误:数值超出范围!请输入1到100之间的整数,重新输入!")
            }
        } catch (e: Exception) {
            // 捕获“字符串无法转为整数”的异常(如输入字母、符号、小数等)
            println("错误:输入格式不合法!请仅输入纯数字,重新输入!")
        }
    }

    return result
}

// 主函数:调用输入函数并演示使用
main() {
    println("=== 整数输入校验演示 ===")
    let validNum = readIntBetween1And100()
    // 后续可使用合法输入值(如:输出输入值的2倍)
    println("您输入数值的2倍是:${validNum * 2}")
}

2.try-with-resources 表达式

仓颉提供了 try-with-resources 来自动管理资源。新手注意其中with 和 resources 都不是关键字,是一种描述性称呼。这是一种专门用于自动管理资源的异常处理机制,主要解决资源(如文件、网络连接、数据库连接等)的释放问题,确保资源在使用后能被正确关闭,无需手动在 finally 块中处理。

语法:

try (资源声明1, 资源声明2, ...) {
    // 使用资源的代码块
} [catch (异常模式) {
    // 异常处理(可选)
}] [finally {
    // 最终操作(可选)
}]

说明

(1)资源声明部分:

  • 在 try 关键字后的括号中声明资源,多个资源用逗号分隔

  • 资源类型必须实现 Resource 接口(包含 isClosed() 和 close() 方法)

  • 声明格式:变量名 = 资源实例(如 file = File("data.txt"))

(2)try 后的花括号内是使用资源的业务逻辑

(3)可选部分:

  • catch 块:捕获处理代码块或资源操作中可能抛出的异常

  • finally 块:无论是否发生异常都会执行的代码(通常无需使用,因资源已自动释放)

示例:

import std.fs.*

// 生成函数:用于数组初始化
func zero(_: Int64): UInt8 {
    UInt8(0)
}

// 1. 正确实现的文件包装类(实现Resource接口)
class AutoFile <: Resource {
    private let file: File
    private var closed: Bool = false  // 避免与isClosed()方法重名

    // 构造函数:使用正确的OpenMode枚举
    public init(path: String, mode: OpenMode) {
        file = File(path, mode)
    }

    // 写入字符串(兼容标准库的字符编码转换)
    public func write(content: String): Unit {
        if (closed) {
            throw Exception("文件已关闭,无法写入")
        }
        
        // 字符串转字节数组(手动实现)
        let n = content.size
        var bytes = Array<UInt8>(n, zero)
        for (i in 0..n) {
            bytes[i] = UInt8(Int64(content[i]))
        }
        file.write(bytes)
    }

    // 读取全部内容
    public func readAll(): String {
        if (closed) {
            throw Exception("文件已关闭,无法读取")
        }
        
        // 读取文件内容到缓冲区
        var buffer = Array<UInt8>(1024, zero)  // 正确初始化数组
        let bytesRead = file.read(buffer)
        return String.fromUtf8(buffer[0..bytesRead])
    }

    // Resource接口:检查是否已关闭
    public func isClosed(): Bool {
        closed
    }

    // Resource接口:关闭文件(自动调用)
    public func close(): Unit {
        if (!closed) {
            file.close()
            closed = true
            println("文件已自动关闭(资源释放)")
        }
    }
}

// 2. 主函数:使用try-with-resources
main() {
    let filePath = "test.txt"

    // 写入文件(自动管理资源)
    try (writer = AutoFile(filePath, OpenMode.Write)) {  // 使用OpenMode枚举
        writer.write("Hello, 仓颉语言!\n")
        writer.write("这是使用try-with-resources的示例")
        // 可以取消下面这行的注释来测试异常情况
        // throw Exception("测试异常")
    } catch (e: Exception) {
        println("写入失败:${e.message}")
    }

    // 读取文件(再次使用自动资源管理)
    try (reader = AutoFile(filePath, OpenMode.Read)) {  // 使用OpenMode枚举
        let content = reader.readAll()
        println("文件内容:\n${content}")
    } catch (e: Exception) {
        println("读取失败:${e.message}")
    }
}

编译运行输出:

文件已自动关闭(资源释放)
文件内容:
Hello, 仓颉语言!
这是使用try-with-resources的示例
文件已自动关闭(资源释放)

3.CatchPattern 的类型模式和通配符模式

CatchPattern 是仓颉语言中在 try-catch 异常处理结构中,用于精确捕获和匹配特定类型异常的一种模式语法。它决定了当 try 块中抛出异常时,哪个 catch 块应该被激活来处理该异常。

(1). 类型模式:匹配单个 / 多个异常

  • 单个异常:e: 异常类(单类,如e: OrderException);

  • 多个异常:e: 异常类1 | 异常类2(多类“或”关系,用|连接,匹配任意一个,变量类型为多个异常的 “最小公共父类”——捕获后,异常会被 转换 为这几个类的 最小公共父类,因此通过 e 只能访问父类的属性和方法,无法访问子类独有的内容)。

示例:批量捕获异常

main(): Int64 {
    try {
        throw IllegalArgumentException("这是一个异常!")
    } catch (e: OverflowException) {
        println(e.message)
        println("OverflowException 被捕获!")
    } catch (e: IllegalArgumentException | NegativeArraySizeException) {
        println(e.message)
        println("IllegalArgumentException 或 NegativeArraySizeException 被捕获!")
    } finally {
        println("终于执行了!")
    }
    return 0
}

编译运行输出:

这是一个异常!

IllegalArgumentException NegativeArraySizeException 被捕获!

终于执行了!

(2). 通配符模式:匹配所有异常

用_表示通配符,不绑定任何变量,捕获try块中抛出的任意Exception子类,适合 “统一处理所有未预料异常” 的场景。等价于 catch (e: Exception),但省掉了变量名。

示例:通配符捕获

main(): Int64 {
    try {
        throw OverflowException()
    } catch (_) {
        println("捕获一个异常!")
    }
    return 0
}

常见运行时异常

在仓颉语言中内置了最常见的异常类,开发人员可以直接使用。

异常

描述

ConcurrentModificationException

并发修改产生的异常

IllegalArgumentException

传递不合法或不正确参数时抛出的异常

NegativeArraySizeException

创建大小为负的数组时抛出的异常

NoneValueException

值不存在时产生的异常,如 Map 中不存在要查找的 key

OverflowException

算术运算溢出异常

示例:定义一个可能抛出异常的除法函数

// 定义一个可能抛出异常的除法函数
func divide(a: Int, b: Int): Int {
    if (b == 0) {
        // 当除数为0时,抛出异常(throw的类型是Nothing,后续代码不执行)
        throw NoneValueException("除数不能为0")  // 假设throw可以携带错误信息(字符串类型)
        // 以下代码永远不会执行(因为throw中断了流程)
       
    }
    a / b  // 正常情况返回除法结果
}

main() {
    let num1 = 10;
    let num2 = 0;  // 除数为0,会触发异常
    
    // 使用try-catch捕获并处理异常
    try {
        let result = divide(num1, num2);
        println("${num1} / ${num2} = ${result}");  // 若正常执行,打印结果
    } catch (e:NoneValueException) {
        // 捕获到throw抛出的异常,处理错误
        println("发生错误:${e}");  // 输出错误信息
    }
    
    // 异常被处理后,程序继续执行后续代码
    println("程序继续运行...");
}

错误处理中使用Option 类型

Option类型是仓颉的 “无值 / 有值” 标记类型(本质是枚举),可用于替代简单异常场景(如 “值不存在” 无需抛异常,用None表示即可)。

Option有两种状态:

  • Some(v):表示 “有值”,v为具体值;

  • None:表示 “无值/空值”,对应 “无结果” 或 “轻微错误”。

常用解构使用方式

1. 模式匹配(最灵活)

通过match匹配Some/None,分别处理有值和无值场景。示例源码:

// 从Option中提取字符串(有值返回内容,无值返回默认值)
func getValue(opt: ?String): String {
    match (opt) {
        case Some(str) => "获取到值:${str}"
        case None => "无值(默认返回空字符串)"
    }
}

main() {
    let a = Some("Hello Cangjie")
    let b: ?String = None // 显式声明为None
    println(getValue(a))  // 输出:获取到值:Hello Cangjie
    println(getValue(b))  // 输出:无值(默认返回空字符串)
}

2. Coalescing 操作符(??):无值时返回默认值

语法:e1 ?? e2,若e1是Some(v)则返回v,否则返回e2(默认值)。示例源码:

main() {
    let score1: ?Int64 = Some(90) // 有值
    let score2: ?Int64 = None     // 无值
    // 无值时默认返回0
    let final1 = score1 ?? 0
    let final2 = score2 ?? 0
    println("分数1:${final1}")  // 分数1:90
    println("分数2:${final2}")  // 分数2:0
}

3. 问号操作符(?):链式访问无值安全

用于链式调用(如., (), []),若中间环节为None,则整个表达式返回None。

可以把 ?. 想成 “安全电梯”:只要前面是 None,电梯停运,整层表达式直接变 None,不抛异常。问号操作符(?)后面的 点(.) 就是 普通的成员访问符,功能跟正常对象用 .一样。示例源码:

// 定义层级:A → B → Option<C> → d
class A { public var b: B = B() }
class B {
    public var c: Option<C> = Some(C())
    public var c1: Option<C> = None
}
class C { public var d: Int64 = 100 }

main() {
    let a = Some(A())

    // 读取:层层安全拆包
    let v1 = a?.b.c?.d     // Some(100)
    let v2 = a?.b.c1?.d    // None

    // 写入:同样短路
    a?.b.c?.d = 200        // 成功,a.b.c.d 现在是 200
    a?.b.c1?.d = 300       // 短路,无效果

    println("v1 = ${v1}")   // Some(200)
    println("v2 = ${v2}")   // None
}

其中,class A { public var b: B = B() },表示定义一个类 A,它里面有一个公共的成员变量 b,类型是 B,并且初始化为 B() 的一个新实例。

路径图示:

a(Some)

├─b(B)───c(Some)───d(100) → Some(100)

└─b(B)───c1(None)         → 短路→ None

4. getOrThrow ():无值时抛出异常

当 “无值” 属于严重错误时,用getOrThrow()提取值,若为None则抛出NoneValueException。示例源码:

main() {
    let data: ?Int64 = None // 无值数据
    try {
        let value = data.getOrThrow() // 无值时抛出异常
        println("数据值:${value}")
    } catch (e: NoneValueException) {
        println("处理错误:${e.message}(数据不存在)")
    }
}

附录:相关官方文档

https://cangjie-lang.cn/docs?url=%2F1.0.0%2Fuser_manual%2Fsource_zh_cn%2Ferror_handle%2Fcommon_runtime_exceptions.html

https://cangjie-lang.cn/docs?url=%2F1.0.0%2Fuser_manual%2Fsource_zh_cn%2Ferror_handle%2Fexception_overview.html

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐