仓颉编程语言白皮书

随着万物互联以及智能时代的到来,软件的形态将发生巨大的变化。一方面,移动应用和移动互联网领域仍然强力驱动人机交互、设备协同、智能化、安全性等方向的创新,另一方面人工智能也同样驱动软件朝智能化、端边云协同等方向演进。新技术、新场景下应用软件的开发对编程语言提出了新的诉求和挑战。

仓颉编程语言作为一款面向全场景应用开发的现代编程语言,通过现代语言特性的集成、全方位的编译优化和运行时实现、以及开箱即用的 IDE 工具链支持,为开发者打造友好开发体验和卓越程序性能。其具体特性表现为:

  • 高效编程:面向应用开发,我们希望语言能够易学易用,降低开发者入门门槛和开发过程中的心智负担,支持各种常见的开发范式和编程模式,让开发者简洁高效地表达各种业务逻辑。仓颉是一门多范式编程语言,支持函数式、命令式和面向对象等多种范式,包括值类型、类和接口、泛型、代数数据类型、模式匹配、以及高阶函数等特性。此外,仓颉还支持类型推断,能够减轻开发者类型标注的负担;通过一系列简明高效的语法,能够减少冗余书写、提升开发效率;语言内置的各种语法糖和宏(macro)的能力,支持开发者基于仓颉快速开发领域专用语言(DSL),构建领域抽象。

  • 安全可靠:作为现代编程语言,仓颉追求编码即安全,通过静态类型系统和自动内存管理,确保程序的类型安全和 null safety 等内存安全;同时,仓颉还提供各种运行时检查,包括数组下标越界检查、类型转换检查、数值计算溢出检查、以及字符串编码合法性检查等,能够及时发现程序运行中的错误;此外,还通过代码扫描工具、混淆工具以及消毒器,进一步提供跨语言互操作安全和代码资产保护等支持。

  • 轻松并发:并发和异步编程能够有效提高处理器利用率,并在交互式应用中确保程序的响应速度,是应用开发中必不可少的能力。仓颉语言实现了轻量化用户态线程和并发对象库,让高效并发变得轻松。仓颉语言采用用户态线程模型,每个仓颉线程都是极其轻量级的执行实体,拥有独立的执行上下文但共享内存。对开发者来说,用户态线程的使用和传统的系统线程的使用方式保持一致,没有带来额外负担;而从运行态视角看,线程的管理由运行时完成,不依赖操作系统的线程管理,因此线程的创建、调度和销毁等操作更加高效,且资源占用比系统线程更少。为了避免数据竞争,仓颉语言提供了并发对象库,并发对象的方法是线程安全的,因此在多线程中调用这些方法和串行编程没有区别,应用逻辑的开发者无需额外关心并发管理。对于一些核心库,仓颉还提供了无锁或者细粒度锁的算法实现,能够进一步减少线程的阻塞,提升并发度。

  • 卓越性能:仓颉编译器及运行时从全栈对编译进行优化,包括编译器前端基于 CHIR(Cangjie HighLevel IR)高层编译优化(比如语义感知的循环优化、语义感知的后端协同优化等),基于后端的编译优化(比如:SLP 向量化、Intrinsic 优化、InlineCache、过程间指针优化、Barrier 优化等),基于运行时的优化(比如轻量锁、分布式标记、并发 Tracing 优化等),一系列的优化让仓颉充分发挥处理器能力,为应用提供卓越的性能支持。另外仓颉语言对运行时进行原生的轻量化设计,通过对运行时模块化分层设计,定义仓颉公共对象模型和运行时公共基础组件,基于公共对象模型,实现运行时的内存管理、回栈、异常处理、跨语言调用等基础能力,大幅减少多个能力间的冗余对象设计,精简运行时体积。同时通过包的按需加载技术,减少仓颉应用启动的冗余包内存开销,因此对于资源敏感设备,占用资源更少,支持更友好。

除此之外,仓颉还支持面向应用开发的一系列工具链,包括语言服务(高亮、联想)、调试(跨语言调试、线程级可视化调试)、静态检查、性能分析、包管理、文档生成、Mock 工具、测试框架、覆盖率工具、Fuzz 工具以及智能辅助编程工具,进一步提升软件开发体验以及效率。以下我们将围绕上述几个方面介绍仓颉语言的主要特性,让读者能够快速了解仓颉语言的定位和主要技术特色。

高效编程

仓颉支持面向对象、函数式、命令式等多种编程范式的融合,既支持面向对象编程范式的模块化和灵活性,又支持函数式编程范式的简洁性和高抽象级表达,使得开发者能够根据业务需求,选择最合适的表达方式,简洁高效地开发业务代码。

除此以外,仓颉还借鉴了现代语言中的各种优秀语言特性,包括各种声明式语法和语法糖,除了能让通用场景的编程更加简洁,还可以针对特定场景快速设计领域特定语言(DSL),以提升领域易用性。

仓颉是一个典型的多范式编程语言,对过程式编程、面向对象编程和函数式编程都提供了良好的支持,包括值类型、类和接口、泛型、代数数据类型和模式匹配,以及函数作为一等公民等特性支持。

多范式

仓颉是一个典型的多范式编程语言,对过程式编程、面向对象编程和函数式编程都提供了良好的支持,包括值类型、类和接口、泛型、代数数据类型和模式匹配,以及函数作为一等公民等特性支持。

类和接口

仓颉支持使用传统的类(class)和接口(interface)来实现面向对象范式编程。仓颉语言只允许单继承,每个类只能有一个父类,但可以实现多个接口。每个类都是 Object 的子类(直接子类或者间接子类)。此外,所有的仓颉类型(包括 Object)都隐式地实现 Any 接口。

仓颉提供 open 修饰符,来控制一个类能不能被继承,或者一个对象成员函数能不能被子类重写(override)。

在下面的例子中,类 B 继承了类 A,且同时实现了接口 I1 和 I2。为了让 A 能够被继承,它的声明需要被 open 修饰。类 A 中的函数 f 也被 open 修饰,因此可以在 B 中被重写。对函数 f 的调用会根据对象具体的类型来决定执行哪个版本,即动态派遣

public open class A {
    let x: Int = 1
    var y: Int = 2

    public open func f(): Unit {
        println("function f in A")
    }

    public func g(): Unit {
        println("function g in A")
    }
}

public interface I1 {
    func h1(): Unit
}

public interface I2 {
    func h2(): Unit
}

public class B <: A & I1 & I2 {
    public override func f(): Unit {
        println("function f in B")
    }

    public func h1(): Unit {
        println("function h1 in B")
    }

    public func h2(): Unit {
        println("function h2 in B")
    }
}

main() {
    let o1: I1 = B()
    let o2: A = A()
    let o3: A = B()


    o1.h1() // "function h1 in B"
    o2.f()  // "function f in A"
    o3.f()  // 动态派遣,"function f in B"
    o3.g()  // "function g in A"  
}
interface I1 {
    func f(x: Int): Unit
}


interface I2 {
    func g(x: Int): Int
}


interface I3 <: I1 & I2 {
    func h(): Unit
}

函数作为一等公民

仓颉中函数可以作为普通表达式使用,可以作为参数传递,作为函数返回值,被保存在其他数据结构中,或者赋值给一个变量使用。

func f(x: Int) {
    return x
}

let a = f
let square = {x: Int => x * x} // lambda 表达式
// 函数嵌套定义,以及函数作为返回值
func g(x: Int) {
    func h(){
        return f(square(x))
    }
    return h
}

func h(f: ()->Int) {
    f()
}

let b = h(g(100))
class C{
    var x = 100
    func resetX(n: Int){
        x = n
        return x
    }
}

main(){
    let o = C()
    let f = o.resetX  // 成员函数作为一等公民
    f(200)
    print(o.x) // 200
}

代数数据类型和模式匹配

代数数据类型是一种复合类型,指由其它数据类型组合而成的类型。两类常见的代数类型是积类型(如 structtuple 等)与和类型(如 tagged union)。

在此我们着重介绍仓颉的和类型 enum,以及对应的模式匹配能力。

在下面的例子中,enum 类型 BinaryTree 具有两个构造器,Node 和 Empty。其中 Empty 不带参数,对应于只有一个空节点的二叉树,而 Node 需要三个参数来构造出一个具有一个值和左右子树的二叉树。

enum BinaryTree {
    | Node(value: Int, left: BinaryTree, right: BinaryTree)
    | Empty
}
func sumBinaryTree(bt: BinaryTree) {
    match (bt) {
        case Node(v, l, r) =>
            v + sumBinaryTree(l) + sumBinaryTree(r)
        case Empty => 0
    }
}
  • 常量模式:可以使用多种字面量值进行判等比较,如整数、字符串等。
  • 绑定模式:可以将指定位置的成员绑定到新的变量,多用于解构 enum 或 tuple。上面的 sumBinaryTree 例子中就用到了绑定模式,将 Node 节点中实际的参数与三个新声明的变量 v、l 和 r 分别绑定。
  • 类型模式:可以用于匹配是否目标类型,多用于向下转型。
  • tuple 模式:用于比较或者解构 tuple。
  • 通配符模式:用于匹配任何值。

未来仓颉还计划引入更加丰富的模式,如序列(sequence)模式、record 模式等。

// 常量模式-字符串字面量
func f1(x: String) {
    match (x) {
        case "abc" => ()
        case "def" => ()
        case _ => () // 通配符模式
    }
}

// tuple 模式
func f2(x: (Int, Int)) {
    match (x) {
        case (_, 0) => 0  // 通配符模式和常量模式
        case (i, j) => i / j // 绑定模式,将 x 的元素绑定到 i 和 j 两个变量
    }
}

// 类型模式
func f3(x: ParentClass) {
    match (x) {
        case y: ChildClass1 => ...
        case y: ChildClass2 => ...
        case _ => ...
    }
}

泛型

在现代软件开发中,泛型编程已成为提高代码质量、复用性和灵活性的关键技术。泛型作为一种参数化多态技术,允许开发者在定义类型或函数时使用类型作为参数,从而创建可适用于多种数据类型的通用代码结构。泛型带来的好处包括:

  • 代码复用:能够定义可操作多种类型的通用算法和数据结构,减少代码冗余。
  • 类型安全:支持更多的编译时的类型检查,避免了运行时类型错误,增强了程序的稳定性。
  • 性能提升:由于避免了不必要的类型转换,泛型还可以提高程序执行效率。

仓颉支持泛型编程,诸如函数、struct、class、interface、extend 都可以引入泛型变元以实现功能的泛型化。数组类型在仓颉中就是典型的泛型类型应用,其语法表示为 Array<T>,其中 T 表示了元素的类型,可以被实例化为任何一个具体的类型,例如 Array<Int>Array<String>,甚至可以是嵌套数组 Array<Array<Int>>,从而可以轻易地构造各种不同元素类型的数组。

除了类型外,我们还可以定义泛型函数。例如我们可以为使用泛型函数来实现任意两个同类型数组的 concat 操作。如下代码所示,我们定义了一个泛型函数 concat,并且它支持任意两个 Array<T> 类型的数组参数,经过处理后返回了一个拼接后的新数组。这样定义的 concat 函数可以应用在 Array<Int>Array<String>Array<Array<Int>> 以及其它任意类型的数组上,实现了功能的通用化。

func concat<T>(lhs: Array<T>, rhs: Array<T>): Array<T> {
    let defaultValue = if (lhs.size > 0) {
        lhs[0]
    } else if (rhs.size > 0) {
        rhs[0]
    } else {
        return []
    }
    let newArr = Array<T>(lhs.size + rhs.size, item: defaultValue)
    // 使用数组切片进行整段拷贝
    newArr[0..lhs.size] = lhs
    newArr[lhs.size..lhs.size+rhs.size] = rhs
    return newArr
}
func lookup<T>(element: T, arr: Array<T>): Bool where T <: Equatable<T> {
    for (e in arr){
        if (element == e){
            return true
        }
    }
    return false
}

如下示例所示,Apple 是 Fruit 的子类,但是变量 a 和变量 b 之间是不能互相赋值的,Array<Fruit> 和 Array<Apple> 之间没有子类型关系。

open class Fruit {}
class Apple <: Fruit {}

main() {
    var a: Array<Fruit> = []
    var b: Array<Apple> = []
    a = b // 编译报错
    b = a // 编译报错
}

类型扩展

仓颉支持类型扩展特性,允许我们在不改变原有类型定义代码的情况下,为类型增加成员函数等功能。具体来说,

仓颉的类型扩展可以对已有的类型做如下几类扩展:

  • 添加函数
  • 添加属性
  • 添加操作符重载
  • 实现接口

下面的例子中,我们为 String 类型增加了 printSize 成员函数,因此在下面的代码中就可以像调用其他预定义的成员函数一样来调用 printSize。

extend String {
    func printSize() {
        print(this.size)
    }
}

main() {
    "123".printSize()
}

在下面的例子中,我们可以定义一个新接口 Integer,然后用 extend 给已有的整数类型实现 Integer 接口,这样已有的整数类型就自然成为了 Integer 的子类型。其中 sealed 修饰符表示该接口只能在当前包中被实现(或扩展)。

sealed interface Integer {}

extend Int8 <: Integer {}
extend Int16 <: Integer {}
extend Int32 <: Integer {}
extend Int64 <: Integer {}

let a: Integer = 123 // ok

类型推断

类型推断是指由编译器根据程序上下文自动推断变量或表达式的类型,而无需开发者显式写出。

仓颉作为现代编程语言,对类型推断也提供了良好的支持。

在仓颉中变量的定义可以根据初始化表达式的类型来推断其类型。除了变量以外,仓颉还额外支持了函数定义返回值类型的推断。在仓颉中,函数体的最后一个表达式会被视为这个函数的返回值。像变量一样,当函数定义省略了返回类型,函数就会通过返回值来推断返回类型。

var foo = 123 // foo 是 'Int64'
var bar = 'hello' // bar 是 'String'

func add(a: Int, b: Int) { // add 返回 Int
  a + b
}
func map<T, R>(f: (T)->R): (Array<T>)->Array<R> {
    ...
}

map({ i => i.toString() })([1, 2, 3]) // 支持推断泛型柯里化函数
// 推断结果为map<Int, String>({ i => i.toString() })([1, 2, 3])

其他现代特性及语法糖

函数重载

仓颉允许在同一作用域内定义多个同名函数。编译器根据参数的个数和类型,来决定函数调用实际执行的是哪个函数。例如,下面的绝对值函数,为每种数值类型都提供了对应的实现,但这些实现都具有相同的函数名 abs,从而让函数调用更加简单。

func abs(x: Int64): Int64 { ... }
func abs(x: Int32): Int32 { ... }
func abs(x: Int16): Int16 { ... }
...

命名参数

命名参数是指在调用函数时,提供实参表达式的同时,还需要同时提供对应形参的名字。使用命名参数可以提升程序的可读性,减少参数的顺序依赖性,让程序更加易于扩展和维护。

在仓颉中,函数定义时通过在形参名后添加 ! 来定义命名参数。当形参被定义为命名参数后,调用这个函数时就必须在实参值前指定参数名,如下面的例子所示:

func dateOf(year!: Int, month!: Int, dayOfMonth!: Int) {...}
dateOf(year: 2024, month: 6, dayOfMonth: 21)

参数默认值

仓颉的函数定义中,可以为特定形参提供默认值。函数调用时,如果选择使用该默认值做实参,则可以省略该参数。

这个特性可以减少很多函数重载或者引入建造者模式的需求,降低代码复杂度。

func dateOf(
    year!: Int64, 
    month!: Int64, 
    dayOfMonth!: Int64, 
    timeZone!: TimeZone = TimeZone.Local
) {...}

dateOf(year: 2024, month: 6, dayOfMonth: 21) 
dateOf(year: 2024, month: 6, dayOfMonth: 21, timeZone: TimeZone.UTC) 

尾随 lambda(trailing lambda)

仓颉支持尾随 lambda 语法糖,从而更易于 DSL 中实现特定语法。具体来说,很多语言中都内置提供了如下经典的条件判断或者循环代码块:

if (x > 0) {
    x = -x
}

while (x > 0) {
    x--
}

尾随 lambda 则能够让 DSL 开发者定制出类似的代码块语法,而无需在宿主语言中内置。例如,在仓颉中,我们支持下面这种方式的函数调用:

func unless(condition: Bool, f: ()->Unit) {
    if(!condition) {
        f()
    }
}

let a = f(...)
unless(a > 0) {
    print("no greater than 0")
}

这里对 unless 函数的调用看上去像是一种特殊的 if 表达式,这种语法效果是通过尾随 lambda 语法实现 —— 如果函数的最后一个形参是函数类型,那么实际调用这个函数时,我们可以提供一个 lambda 表达式作为实参,并且把它写在函数调用括号的外面。尤其当这个 lambda 表达式为无参函数时,我们允许省略 lambda 表达式中的双箭头 =>,将其表示为代码块的形式,从而进一步减少对应 DSL 中的语法噪音。因此,在上面的例子中,unless 调用的第二个实参就变成了这样的 lambda 表达式:

{ print("no greater than 0") }

如果函数定义只有一个参数,并且该参数是函数类型,我们使用尾随 lambda 调用该函数时还可以进一步省略函数调用的括号,从而让代码看上去更简洁自然。

func runLater(fn:()->Unit) {
    sleep(5 * Duration.Second)
    fn()
}

runLater() { // ok
    println("I am later")
}

runLater { // 可以进一步省略括号
    println("I am later")
}

管道(Pipeline)操作符

仓颉中引入管道(Pipeline)操作符,来简化嵌套函数调用的语法,更直观的表达数据流向。下面的例子中,给出了嵌套函数调用和与之等效的基于管道操作符 |> 的表达式。后者更加直观的反映了数据的流向:|> 左侧的表达式的值被作为参数传递给右侧的函数。

func double(a: Int) {
    a * 2
}

func increment(a: Int) {
    a + 1
}

main() {
println(double(increment(double(double(5))))) // 42

5 |> double |> double |> increment |> double |> println // 42
}

操作符重载

仓颉中定义了一系列使用特殊符号表示的操作符,其中大多数操作符都允许被重载,从而可以作用在开发者自己定义的类型上,为自定义类型的操作提供更加简洁直观的语法表达。

在仓颉中只需要定义操作符重载函数就能实现操作符重载。在下面的例子中,我们首先定义一个类型 Point 表示二维平面中的点,然后我们通过重载+操作符,来定义两个点上的加法操作。

struct Point {
    let x: Int
    let y: Int

    init(x: Int, y: Int) {...}

    operator func +(rhs: Point): Point {
        return Point(
            this.x + rhs.x,
            this.y + rhs.y
        )
    }
}

let a: Point = ...
let b: Point = ...
let c = a + b

属性(property)

在面向对象范式中,我们常常会将成员变量设计为 private 的,而将成员变量的访问封装成 getter 和 setter 两种 public 方法。

这样可以隐藏数据访问的细节,从而更容易实现访问控制、数据监控、跟踪调试、数据绑定等业务策略。

仓颉中直接提供了属性这一种特殊的语法,它使用起来就像成员变量一样可以访问和赋值,但内部提供了 getter 和 setter 来实现更丰富的数据操作。对成员变量的访问和赋值会被编译器翻译为对相应 getter 和 setter 成员函数的调用。

具体来说,prop 用于声明只读属性,只读属性只具有 getter 的能力,必须提供 get 实现;mut prop 用于声明可变属性。可变属性同时具备 getter 和 setter 的能力,必须提供 get 和 set 实现。

如下示例所示,开发者希望对 Point 类型的各数据成员的访问进行记录,则可以在内部声明 private 修饰的成员变量,通过声明对应的属性来对外暴露访问能力,并在访问的时候使用日志系统 Logger 记录它们的访问信息。对使用者来说,使用对象 p 的属性与访问它的成员变量一样,但内部却实现了记录的功能。

注意这里 x 和 y 是只读的,只有 get 实现,而 color 则是可变的,用 mut prop 修饰,同时具有get 和 set 实现。

class Point {
    private let _x: Int
    private let _y: Int
    private var _color: String

    init(x: Int, y: Int, color: String) {
        _x = x
        _y = y
        _color = color
    }

    prop x: Int {
        get() {
            println('level: Debug, "access x"')
            return _x
        }
    }

    prop y: Int {
        get() {
            println('level: Debug, "access y"')
            return _y
        }
    }

    mut prop color: String {
        get() {
            println('level: Debug, "access color"')
            return _color
        }

        set(c) {
            println('level: Debug, "reset color to ${c}"')
            _color = c
        }
    }
}

main() {
    let p = Point(0, 0, "red")
    let x = p.x // "access x"
    let y = p.y // "access y"
    p.color = "green" // "reset color to green"
}

安全可靠

编程语言的设计和实现,以及相应工具支持,对于程序质量和安全性有重要影响。

仓颉通过静态类型系统、动静态检查、自动内存管理、以及工具链来提升程序的安全性。

静态类型和垃圾收集

仓颉是静态类型语言,程序中所有变量和表达式的类型都是在编译期确定的,并且在程序运行过程中不会发生改变。相比动态类型系统,静态类型系统对开发者有更多的约束,但能够在编译期尽量早的发现程序中的错误,提高程序的安全性,同时也让程序的行为更加容易预测,为编译优化提供了更多信息,使能更多的编译优化,提升程序的性能。

垃圾收集(GC)是一种自动内存管理机制,它能够自动识别和回收不再需要使用的对象,将开发者从手工释放内存中解放出来,不仅可以提高开发效率,还能有效避免各种常见内存错误,提升程序的安全性。常用的垃圾收集技术包括 tracing 和引用计数(reference counting,即 RC)。仓颉采用 tracing GC 技术,通过在运行时跟踪对象之间的引用关系,来识别活动对象和垃圾对象。

空引用安全

空引用是指引用类型的值可以为 null。代码存在空引用会引发各种各样潜在的风险,空引用被图灵奖得主 Tony Hoare 称为“价值十亿美元的错误”。

在许多编程语言中,空引用都是最常见的陷阱之一,开发者很容易在未确保非空的情况下访问引用类型的成员,从而引发错误或异常。因为语言类型系统并未给非空引用类型提供任何保障。

空引用安全就是旨在消除代码空引用危险。

仓颉是实现了空引用安全的语言之一。在仓颉中,没有提供 null 值,换句话说,仓颉的引用类型永远是非空的。从而在类型上杜绝了空引用的发生。

值得注意的是,表示一个空值在语义中是十分有用的。在仓颉中,对于任意类型T,都可以有对应的可选类型 Option<T>。具有 Option<T>类型的变量要么对应一个实际的具有 T 类型的值 v,因此取值为 Some(v),要么具有空值,取值为 None。

可选类型(Option<T>)是一种 enum 类型,是一个经典的代数数据类型,表示有值或空值两种状态。

enum Option<T> {
    Some(T) | None
}

var a: Option<Int> = Some(123)
a = None

基于可选类型使用的广泛性,仓颉还为可选类型提供了丰富的语法糖支持。例如可以使用 ?T 来代替 Option<T>,也提供了可选链操作符(?.)来简化成员访问,以及空合并操作符(??)来合并有效值。

var a: ?Int = None
a?.toString() // None
a ?? 123 // 123
a = Some(321)
a?.toString() // Some("321")
a ?? 123 // 321

值类型

值类型是一种具有传递即复制的语义行为的类型,具有值类型的变量,其中保存的是数据自身,而不是指向数据的引用。由于值类型的这种特性,开发者选择性地使用值类型可以使得程序显著减少修改语义,从而让程序变得更可预测、更可靠。

例如最典型的并发安全问题就是在程序不同的线程中传递了同一个可变对象,此时访问这个对象的字段将会造成不可预测的 data race 问题。如果这个对象具备值语义,那么在传递的过程中我们就可以保证它经过了完整的复制,让每个线程对该值的访问都是彼此独立的,从而保证了并发安全。

仓颉原生支持了值类型,除了常见的 Int 类型以外,仓颉也可以使用 struct 来实现用户自定义的值类型。

如下面的例子,Point 正是一个值类型,因此在经过赋值后,a 和 b 已经是两个彼此独立的变量,对 a 的修改不会影响到 b。

struct Point {
    var x: Int
    var y: Int
    init(x: Int, y: Int) { ... }
    ...
}

var a = Point(0, 0)
var b = a
a.x = 1
print(b.x) // 0

不可变优先

不可变(Immutable)指的是在变量赋值或对象创建结束之后,使用者就不能再改变它的值或状态。不可变意味着只读不写,因此不可变对象天然地具备线程安全的特性,即如无其它特殊限制的话可以在任何线程上自由调用。此外,相较于可变对象,不可变对象的访问没有副作用,因此在一些场合下也会让程序更易于了解,而且提供较高的安全性。

不可变通常可以分为两种,一种是不可变变量,不可变变量是指经初始化后其值就不可被修改的变量;另一种是不可变类型,不可变类型是指在构造完成后实际数据对象的内容无法被改变。

在仓颉中,let 定义的变量是不可变变量,而像 String、enum 等类型是不可变类型,这些都是不可变思想在仓颉中的应用。更多地使用不可变特性可以让程序更安全,也更利于理解和维护。

函数参数不可变

在仓颉中,所有函数形参都是不可变的,这意味着我们无法对形参赋值,如果形参是值类型,也无法修改形参的成员。

struct Point {
    var x: Int
    var y: Int
    init(x: Int, y: Int) { ... }
    ...
}


func f(a: Point) {  
    a = Point(0, 0) 
    a.x = 2 
}

模式匹配引入的新变量不可变

在仓颉中,模式匹配支持变量绑定模式,我们可以将目标值解析到新绑定的变量中,但这个变量仍然是不可变的。这意味着我们无法对绑定的变量赋值,如果变量是值类型,也无法修改变量的成员。

func f(a: ?Point) {
    match (a) {
        case Some(b) => 
            b = Point(0, 0) 
            b.x = 2 
        case None => ()
    }
}

闭包捕获可变变量不允许逃逸

在仓颉中,闭包指的自包含的函数或 lambda,闭包可以从定义它的静态作用域中捕获变量,即使对闭包调用不在定义的作用域,仍可以访问其捕获的变量。

仓颉中允许闭包捕获可变变量,但不允许该闭包继续逃逸,这避免了对可变变量修改可能导致的意外行为。

func f() {
    let a = 1
    var b = 2
    func g() {
        print(a) 
        print(b) 
    }
    return g 
}