第10篇 异常处理——程序的容错机制
**作者:**中文编程倡导者—— 李金雨
联系方式:wbtm2718@qq.com
**目标读者:**编程入门(零基础)
核心理念:使用华为仓颉原生中文编程,体验真正的国产编程语言
一、什么是异常?
想象一下这些场景:
- 你打开一个文件,但文件不存在
- 你输入"abc",但程序期待一个数字
- 你访问数组的第100个元素,但数组只有10个元素
- 你做除法时,除数是0
这些情况都会导致程序"出错",专业术语叫做抛出异常(Throw Exception)。如果不处理,程序就会崩溃退出。
异常处理就是让程序在遇到错误时,能够优雅地处理,而不是直接"死掉"。
二、生活中的异常处理
例子1:餐厅点餐
- 正常情况:点了一份炒饭,厨师做好了端上来
- 异常情况:厨师发现米饭用完了
- 处理方式:服务员告诉你"抱歉,米饭用完了,要不要试试面条?"
例子2:ATM取钱
- 正常情况:输入密码正确,取出现金
- 异常情况:密码错误、余额不足、机器故障
- 处理方式:显示友好的提示信息,而不是让机器死机
三、仓颉语言中的异常处理
仓颉使用try-catch机制来处理异常。
基础语法
import std.console.* import std.convert.* func 安全地除法(被除数: Int64, 除数: Int64): Int64 { try { return 被除数 / 除数 } catch (异常: Exception) { println("发生错误:${异常.message}") return 0 // 出错时返回默认值 } } main() { var 结果1 = 安全地除法(10, 2) // 正常:5 var 结果2 = 安全地除法(10, 0) // 异常:除数为0 println("结果1:${结果1}") println("结果2:${结果2}") }运行结果
发生错误:division by zero 结果1:5 结果2:0四、常见的异常类型
import std.console.* func 演示各种异常(): Unit { // 1. 数组越界异常 try { var 数字列表 = [1, 2, 3] var 值 = 数字列表[10] // 越界了! } catch (异常: IndexOutOfBoundsException) { println("数组越界:${异常.message}") } // 2. 数字转换异常 try { var 文本 = "abc" var 数字 = Int64.parse(文本) // 无法转换! } catch (异常: NumberFormatException) { println("数字格式错误:${异常.message}") } // 3. 空指针异常 try { var 空名字: String? = null var 长度 = 空名字!.length // 空值解引用! } catch (异常: NullPointerException) { println("空指针异常:${异常.message}") } }五、抛出异常
有时候我们需要自己抛出异常来提示调用者。
// 定义一个检查年龄的函数 func 设置年龄(年龄: Int64): Unit { if (年龄 < 0) { throw Exception("年龄不能为负数!") } if (年龄 > 150) { throw Exception("年龄不能超过150岁!") } println("年龄设置成功:${年龄}岁") } main() { try { 设置年龄(25) // 正常 设置年龄(-5) // 抛出异常 } catch (异常: Exception) { println("错误:${异常.message}") } }运行结果
年龄设置成功:25岁 错误:年龄不能为负数!六、语法设计讨论:异常处理中的类型声明
同学们,在学习异常处理时,我们再次遇到了仓颉的类型后置语法问题。
看看异常处理的代码:
try { // 可能出错的代码 } catch (异常: Exception) { // 类型后置! // 处理异常 } func 安全地除法(被除数: Int64, 除数: Int64): Int64 { // 参数和返回值类型都后置 try { return 被除数 / 除数 } catch (异常: Exception) { // 又是类型后置 return 0 } }按照中国人的语言习惯:
- 我们习惯说"异常类型的变量"、“整数类型的参数”
- 我们习惯说"字符串类型的返回值"
但仓颉的写法:
catch (异常: Exception)→ 读作"捕获异常,异常类型的"(定语后置)func 函数名(): 返回类型→ 读作"函数名,返回某某类型的"(定语后置)
如果仓颉能改进成C#风格:
// 假设的改进语法 try { // 可能出错的代码 } catch (Exception 异常) { // "异常类型的异常变量"——定语前置! // 处理异常 } Int64 安全地除法(Int64 被除数, Int64 除数) { // "整数类型的安全地除法函数" try { return 被除数 / 除数 } catch (Exception 异常) { return 0 } }这样读起来多么自然:“定义一个整数类型的函数安全地除法,它有两个整数类型的参数…”
华为在设计仓颉语言时,采用了类似Rust的类型后置语法,这就像是在说"异常异常类型的"而不是"异常类型的异常"。现代汉语都是定语前置的,我们希望中文编程语言能真正符合中国人的语言习惯!
七、finally块
finally块中的代码无论是否发生异常都会执行,常用于清理资源。
func 读取文件(文件名: String): String { var 文件内容 = "" var 文件: File? = null try { 文件 = File.open(文件名, "r") 文件内容 = 文件.readAll() return 文件内容 } catch (异常: Exception) { println("读取文件失败:${异常.message}") return "" } finally { // 无论成功与否,都要关闭文件 if (文件 != null) { 文件.close() println("文件已关闭") } } }八、自定义异常
我们可以创建自己的异常类型来表示特定的错误。
// 自定义异常类 class 年龄异常 : Exception { init(消息: String) { super(消息) } } class 成绩异常 : Exception { init(消息: String) { super(消息) } } // 使用自定义异常 func 设置学生成绩(成绩: Float64): Unit { if (成绩 < 0) { throw 成绩异常("成绩不能为负数!") } if (成绩 > 100) { throw 成绩异常("成绩不能超过100分!") } println("成绩设置成功:${成绩}分") } main() { try { 设置学生成绩(85.5) 设置学生成绩(105) // 抛出成绩异常 } catch (异常: 成绩异常) { println("成绩错误:${异常.message}") } catch (异常: Exception) { println("其他错误:${异常.message}") } }九、练习时间
练习1:安全的计算器
创建一个计算器类,包含加、减、乘、除四个方法。除法要处理除数为0的情况,所有方法都要处理可能的溢出异常。
练习2:用户注册验证
创建一个用户注册函数,验证以下内容:
- 用户名不能为空,长度3-20个字符
- 密码不能为空,长度6-30个字符
- 年龄必须在1-150之间
- 邮箱格式要正确
如果验证失败,抛出相应的异常。
练习3:银行转账系统
创建一个转账函数,需要处理以下异常情况:
- 转出账户余额不足
- 转账金额必须大于0
- 转账金额不能超过单笔限额(比如5万元)
- 收款账户不存在
十、本课小结
今天我们学习了:
- 异常:程序运行时的错误情况
- try-catch:捕获并处理异常
- throw:主动抛出异常
- finally:无论是否异常都会执行的代码块
- 自定义异常:创建特定的异常类型
类型语法思考:在异常处理中,我们频繁地声明异常变量类型、函数参数类型、返回值类型,每一次都要面对类型后置的语法。希望未来的中文编程语言能让类型声明更符合中国人的说话习惯,真正做到"说人话"!
十一、课后作业
完善"学生管理系统"的异常处理:
- 添加成绩输入验证(0-100分)
- 添加年龄输入验证(5-30岁)
- 添加学号格式验证(不能为空,长度固定)
编写一个文件复制程序,要求:
- 检查源文件是否存在
- 检查目标目录是否有写入权限
- 使用try-catch-finally确保资源正确释放
思考:在你的日常生活中,还有哪些场景可以用"异常处理"的思路来解决?
下篇预告:第11篇《文件操作——数据的持久化存储》,学习如何将数据保存到文件中,让数据不会随程序关闭而丢失!