其他现代特性及语法糖

函数重载

仓颉允许在同一作用域内定义多个同名函数。编译器根据参数的个数和类型,来决定函数调用实际执行的是哪个函数。例如,下面的绝对值函数,为每种数值类型都提供了对应的实现,但这些实现都具有相同的函数名 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) // ok
dateOf(year: 2024, month: 6, dayOfMonth: 21, timeZone: TimeZone.UTC) // ok

尾随 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
}

double(increment(double(double(5)))) // 42

5 |> double |> double |> increment |> double // 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 的属性与访问它的成员变量一样,但内部却实现了记录的功能。 注意这里 xy 是只读的,只有 get 实现,而 color 则是可变的,用 mut prop 修饰,同时具有getset 实现。

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

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

    prop x: Int {
        get() {
            Logger.log(level: Debug, "access x")
            return _x
        }
    }

    prop y: Int {
        get() {
            Logger.log(level: Debug, "access y")
            return _y
        }
    }

    mut prop color: String {
        get() {
            Logger.log(level: Debug, "access color")
            return _color
        }

        set(c) {
            Logger.log(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"
}