仓颉白皮书——安全可靠的不可变优先与默认封闭设计
不可变优先:从变量、类型到函数参数,默认禁止修改,减少副作用和线程安全问题。默认封闭:类和方法默认不可继承 / 覆盖,强制开发者谨慎设计继承关系。
·
目录
一、不可变优先:让程序更安全可靠
在编程中,不可变(Immutable) 就像 “一次性印章”—— 一旦创建就无法修改。仓颉通过 “不可变优先” 设计,从根源上减少错误,让代码更安全、更易理解。
1. 不可变变量:用let锁定值
- 语法:用
let声明的变量是不可变变量,初始化后不能再修改。let age = 18 // 正确:age 是不可变变量,值为18 age = 20 // ❌ 错误:不可变变量不能重新赋值- 类比:像考试卷上的答案,写完就不能改了。
2. 不可变类型:对象内部状态锁定
- 什么是不可变类型?
对象创建后,内部字段不能被修改。仓颉的String、enum、struct默认都是不可变的。struct Point { let x: Int // ❌ 用let声明字段,不可变 let y: Int } let p = Point(x: 3, y: 5) p.x = 10 // ❌ 错误:不可变类型的字段不能修改- 为什么安全?
不可变对象在多线程中不会被意外修改,避免 “数据打架”(线程安全)。
- 为什么安全?
3. 函数参数:拒绝修改的 “只读门票”
- 在仓颉中,所有函数参数都是不可变的,不能修改也不能重新赋值。
struct MutablePoint { var x: Int // 字段可变(用var) var y: Int } func changePoint(point: MutablePoint) { point.x = 99 // ❌ 错误:参数不可变,不能修改字段 point = MutablePoint(x: 0, y: 0) // ❌ 错误:不能重新赋值 }- 好处:防止函数内部误改外部数据,保持函数 “纯净”(无副作用)。
4. 模式匹配:解包后仍是只读
- 模式匹配(如
match语句)绑定的变量默认不可变。enum Option<T> { Some(T) None } func processOption(opt: Option<Int>) { match opt { case Some(num) => { // num 是不可变变量 num = 0 // ❌ 错误:模式匹配绑定的变量不可变 } case None => () } }- 例外:极少场景需要可变,用
var显式声明(不推荐)。
- 例外:极少场景需要可变,用
5. 闭包陷阱:可变变量禁止 “逃逸”
- 闭包(Lambda)可以捕获外部变量,但如果捕获了可变变量(var),闭包不能被返回(逃逸)。
func makeClosure() { var count = 0 // 可变变量 // 闭包捕获了可变变量 count let closure = { count += 1 // 允许在闭包内修改 } return closure // ❌ 错误:闭包捕获可变变量,禁止逃逸 }- 为什么?
若闭包被返回,可能在外部作用域销毁后继续修改变量,导致内存错误。
- 为什么?
二、默认封闭:拒绝滥用继承的 “防护墙”
在面向对象编程中,继承是把双刃剑。仓颉通过 “默认封闭” 设计,强制开发者谨慎使用继承,避免代码复杂度失控。
1. 类默认不可继承:禁止 “子类化”
- 用
class定义的类默认不能被继承,必须用open/abstract/sealed显式允许。class Animal { func speak() { println("Animal sound") } } class Dog : Animal {} // ❌ 错误:Animal 默认不可继承 // 允许继承:用open修饰 open class Vehicle { func start() { println("Vehicle started") } } class Car : Vehicle {} // ✅ 正确:Vehicle 是open类- 修饰符区别:
open:允许继承,子类可覆盖方法。abstract:抽象类,必须被继承,不能实例化。sealed:允许继承,但仅限同一模块内(限制继承范围)。
- 修饰符区别:
2. 方法默认不可覆盖:禁止 “重写偷袭”
- 类的方法默认不能被子类修改(覆盖),必须用
open显式允许。open class Shape { func area() -> Double { return 0.0 } // 默认不可覆盖 open func draw() { println("Drawing shape") } // 允许覆盖 } class Circle : Shape { override func area() -> Double { // ❌ 错误:area() 不可覆盖 return 3.14 * radius * radius } override func draw() { // ✅ 正确:draw() 是open方法 println("Drawing circle") } }- 好处:防止子类意外破坏父类核心逻辑,如 “鸟类” 被继承后重写 “飞行” 方法为 “不会飞”,导致逻辑混乱。
3. 抽象类:强制子类 “填空”
- 抽象类是 “不完整的类”,必须被子类继承并实现抽象方法。
abstract class Animal { abstract func speak() -> String // 抽象方法,无实现 func eat() { println("Eating...") } // 普通方法,有实现 } class Dog : Animal { override func speak() -> String { // 必须实现抽象方法 return "Woof!" } } let dog = Dog() // 正确:子类实现了抽象方法 let animal: Animal = Dog() // 多态:父类引用指向子类对象- 注意:抽象类不能直接创建对象(
let animal = Animal()❌ 错误)。
- 注意:抽象类不能直接创建对象(
三、为什么这样设计?—— 安全与工程的平衡
| 特性 | 传统语言(如 Java) | 仓颉 | 优势 |
|---|---|---|---|
| 变量可变性 | final 关键字强制不可变 |
默认let不可变 |
减少 90% 可变变量误用风险 |
| 类继承 | 默认可继承 | 默认不可继承 | 防止 “类爆炸”,代码层次更清晰 |
| 方法覆盖 | 默认可覆盖 | 默认不可覆盖 | 防止子类 “偷改” 父类核心逻辑 |
| 闭包捕获 | 可变变量可逃逸 | 禁止可变变量逃逸 | 避免内存泄漏和竞态条件 |
四、初学者实践建议
-
优先用
let和struct:- 除非必须修改,否则不用
var和class。
// 推荐:不可变结构体 struct User { let id: Int let name: String } - 除非必须修改,否则不用
-
继承只用在明确需要多态时:
- 例如:图形类(
Shape)有Circle/Rectangle子类,需统一计算面积。
- 例如:图形类(
-
闭包中避免捕获可变变量:
- 用不可变变量(
let)替代,或限制闭包作用域。
- 用不可变变量(
-
组合优于继承:
- 用 “拼装” 组件代替继承,如
UserService组合Logger而非继承。
- 用 “拼装” 组件代替继承,如
五、常见错误与解决
| 错误信息 | 原因 | 解决方法 |
|---|---|---|
Cannot assign to let variable |
尝试修改let变量 |
用var声明变量 |
Class is not open for inheritance |
继承了默认不可继承的类 | 给父类添加open修饰符 |
Overriding non-open function |
重写了不可覆盖的方法 | 给父类方法添加open修饰符 |
Escaping closure captures mutable variable |
闭包捕获可变变量并逃逸 | 用let声明不可变变量,或不返回闭包 |
总结
- 不可变优先:从变量、类型到函数参数,默认禁止修改,减少副作用和线程安全问题。
- 默认封闭:类和方法默认不可继承 / 覆盖,强制开发者谨慎设计继承关系。
更多推荐



所有评论(0)