仓颉白皮书——高效编程的其他现代特性及语法糖
文章探讨了编程语言中的语法糖及其应用,重点介绍了函数重载、命名函数、参数默认值、建造者模式、尾随lambda、管道操作符、操作符重载和属性等常见语法糖。语法糖通过简化代码写法,提升开发效率,而不改变语言功能。例如,函数默认值可以减少建造者模式的需求,尾随lambda和管道操作符使代码更直观,操作符重载为自定义类型提供简洁的语法表达,属性则封装了数据的访问和修改逻辑。这些语法糖在仓颉语言中得到了广泛
目录
回到原问题:为什么说 “函数默认值可以减少引入建造者模式的需求”?
什么是语法糖?
语法糖(Syntactic Sugar) 是编程语言中一种 “便捷写法”,本质上不会改变语言的功能或逻辑,但能让代码更简洁、易读,减少开发者的编码工作量。
就像给复杂的语法裹上一层 “糖衣”,让开发者用更简单的方式表达相同的意思,同时编译器或解释器会把这些 “糖衣” 还原成正式的语法结构执行。核心特点
不改变功能
语法糖的底层实现和传统写法完全等价,只是写法更简化。
例如:
- 数学中用
x²表示x × x,²就是语法糖,本质还是乘法运算。- 编程中用
a += 1代替a = a + 1,两者执行逻辑完全相同。提升开发效率
减少重复代码,让开发者更聚焦业务逻辑。
例如:
- 循环遍历数组时,用
for (item in array)代替传统的for (int i=0; i<array.length; i++)。- 仓颉中可能用
let list = [1, 2, 3]快速创建数组,而无需调用构造函数。常见语法糖举例
场景 语法糖写法 等价传统写法 变量赋值 a += 1a = a + 1空值判断 val = obj?.propif (obj != null) val = obj.prop; else null循环遍历 for (item in list)for (int i=0; i<list.size; i++) { item = list[i]; }对象字面量 let obj = {name: "Tom"}let obj = new Object(); obj.name = "Tom";三元表达式 result = condition ? a : bif (condition) result = a; else result = b;
1、函数重载
仓颉允许在同一个作用域下定义多个同名函数。编译器会根据参数的类型和个数,来决定最终执行的是哪一个函数。例如,下面的绝对值函数,为每种数值类型的函数为每种数据类型都提供了对应的实现,但是这些实现都命名为abs,从而让函数变得简单。
func abs(x: Int64):Int64 {...}
func abs(x: Int32): Int32 {...}
func abs(x: Int16):Int16 {...}
2、命名函数
命名函数是指在调用函数时,提供实参表达式的同时,还需要提供对应形参的名字。使用命名函数可以提高程序的可读性,减少参数的顺序依赖性,让程序更加便于维护和扩展。
在仓颉中,命名函数通过在形参后面加!来定义命名函数。当形参被定义为命名参数后,调用这个函数就必须在实参值前面指定参数名,如下面例子:
func dateof(year!: Int, month!: Int, dayOfMonth!: Int) {...}
dateof(year: 2025, month: 5, dayOfMonth: 20)
3、参数默认值:
仓颉的函数定义中,可以为特定形参指定默认值。当函数被调用时,如果说选择使用函数的默认值,则可以省略该参数。这个特性可以减少很多函数的重载或者引入建造者模式的需求,降低代码复杂度。如下:
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
建造者模式:
在编程中,建造者模式(Builder Pattern)是一种设计模式,属于 “创建型模式” 的一种。它的核心作用是将一个复杂对象的构建过程和表示(结构)分离,使得同样的构建过程可以创建不同的表示。这样做的目的是让构建复杂对象的过程更灵活、更易于维护,尤其是当对象的构造参数较多、组合方式复杂时。
为什么会需要建造者模式?
假设你要创建一个复杂对象(比如一个用户类
User),这个对象有很多可选参数(如姓名、年龄、地址、邮箱、权限等级等)。如果直接通过构造函数来创建对象,可能会面临以下问题:
- 参数数量多且容易混淆:构造函数可能需要十几个参数,调用时很难记住每个参数的顺序和含义。
- 可选参数处理麻烦:如果某些参数是可选的,可能需要写很多重载的构造函数(不同参数组合的版本),导致代码冗余。
例如,传统方式可能需要这样的代码:
// 多个重载的构造函数(伪代码示例) User(String name); // 仅姓名 User(String name, int age); // 姓名+年龄 User(String name, int age, String address); // 姓名+年龄+地址 ... // 更多参数组合的版本这种方式会让代码变得臃肿,维护困难。
建造者模式如何解决问题?
建造者模式通过一个 “建造者” 类来逐步设置对象的参数,最后一次性创建对象。它的核心思路是:
- 将对象的构建过程封装到建造者类中,通过链式调用设置参数。
- 分离构建过程和对象表示,同一个建造者可以灵活构造不同配置的对象。
用建造者模式改写后的代码示例(伪代码):
User user = new UserBuilder() .name("Alice") // 设置姓名 .age(25) // 设置年龄(可选) .address("北京") // 设置地址(可选) .build(); // 最终创建对象
- 链式调用:每个设置参数的方法(如
name()、age())返回建造者自身,方便连续调用。- 延迟构建:直到调用
build()方法时,才真正创建对象,确保所有必要参数已设置(可选校验)。回到原问题:为什么说 “函数默认值可以减少引入建造者模式的需求”?
在仓颉中,如果函数的参数有默认值,调用时可以省略该参数,这相当于在函数层面直接支持了可选参数。例如:
# 假设仓颉的函数定义(伪代码示例) func createUser( name: String, age: Int = 18, // age 有默认值 18 address: String = "未知" // address 有默认值 "未知" ) { // 创建用户逻辑 } // 调用时可以省略默认参数 createUser("Alice") // 等价于 createUser("Alice", 18, "未知") createUser("Bob", 25) // 省略 address,使用默认值此时,函数本身通过默认值已经处理了可选参数的问题,不需要为不同参数组合写多个重载函数,也不需要额外创建一个建造者类来管理参数。这就减少了使用建造者模式的必要性,因为默认值让函数调用更简洁,避免了复杂对象构建时的参数管理麻烦。
总结:建造者模式 vs. 函数默认值
- 建造者模式:适用于对象构造复杂、参数多且组合灵活的场景,通过独立的建造者类解耦构建过程。
- 函数默认值:适用于函数参数可选性较强的场景,通过简单的默认值设置即可实现灵活调用,代码更简洁。
4、尾随lambda(trailing lambda)
仓颉支持尾随 lambda 语法糖,从而更易于 DSL 中实现特定语法。具体来说,很多语言中都内置提供了如下经典的条件判断或者循环代码块:
if (x > 0) {
x = -x
}
while (x > 0) {
x--
}
这里对 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")
}
5、管道(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 省略很多
6、操作符重载
仓颉中定义了一系列使用特殊符号表示的操作符,其中大多数操作符都允许被重载,从而可以作用在开发者自己定义的类型上,为自定义类型的操作提供更加简洁直观的语法表达。
一、什么是操作符重载?
操作符重载 允许开发者为自定义类型(如结构体、类)重新定义已有操作符(如 + - * /)的行为。
本质是通过编写 操作符重载函数,让自定义类型使用操作符时具备特定逻辑,使代码更简洁直观。
类比现实:就像 “+” 号在数学中既可以表示数字相加,也能表示字符串拼接(如 "a"+"b"="ab"),这就是操作符在不同类型上的 “重载”。
二、仓颉中的操作符重载规则
- 通过
operator关键字定义重载函数
格式:operator func 操作符(参数) -> 返回类型 { ... } - 参数规则
- 一元操作符(如
!):1 个参数(rhs或lhs,取决于操作符位置)。 - 二元操作符(如
+):2 个参数,第一个参数用this表示当前实例(左操作数),第二个参数用rhs表示右操作数。
- 一元操作符(如
- 作用于自定义类型
只能为 结构体(struct) 或 类(class) 重载操作符,不能为基础类型(如Int、String)重载。
三、代码示例解析:为 Point 类型重载 + 操作符
struct Point {
let x: Int
let y: Int
init(x: Int, y: Int) {
self.x = x
self.y = y
}
// 重载二元操作符 `+`
operator func +(rhs: Point): Point {
// `this` 表示左操作数(当前 Point 实例)
// `rhs` 表示右操作数(传入的另一个 Point 实例)
return Point(
x: this.x + rhs.x, // 横坐标相加
y: this.y + rhs.y // 纵坐标相加
)
}
}
// 使用示例
let a = Point(x: 1, y: 2)
let b = Point(x: 3, y: 4)
let c = a + b // 等价于 a.+(b),调用重载的 `+` 操作符
print(c.x, c.y) // 输出:4 6
代码逐行解析
定义
Point结构体
- 包含
x和y两个整数属性,表示点的坐标。init初始化方法用于创建实例。重载
+操作符
operator func +(rhs: Point): Point:
operator关键字声明这是一个操作符重载函数。+是要重载的操作符(二元操作符)。rhs: Point表示右操作数的类型为Point。- 返回值类型为
Point,即两个点相加后的新点。函数体逻辑
this代表左操作数(如a + b中的a),是当前结构体的实例。- 通过
this.x + rhs.x和this.y + rhs.y计算新点的坐标,返回一个新的Point实例。调用方式
a + b会被编译器自动解析为a.+(b),即调用Point中重载的+操作符函数。四、常见可重载的操作符
在仓颉语言中,大部分操作符都可以被重载,但需遵循特定的语法规则。以下是常见可重载操作符的分类及示例:
1. 算术操作符:用于数学运算,可重载为自定义类型的组合或变换逻辑。
| 操作符 | 描述 | 重载函数名 | 参数与返回值 | 示例场景 |
|---|---|---|---|---|
+ |
加法 | operator +(rhs) |
this + rhs → 返回新实例 |
向量相加、坐标点合并 |
- |
减法 | operator -(rhs) |
this - rhs → 返回新实例 |
计算两点之间的差值 |
* |
乘法 | operator *(rhs) |
this * rhs → 返回新实例 |
向量缩放、矩阵乘法 |
/ |
除法 | operator /(rhs) |
this / rhs → 返回新实例 |
数值类型的比例计算 |
% |
取模 | operator %(rhs) |
this % rhs → 返回新实例 |
周期性数据处理 |
示例:为 Vector 类型重载 *(缩放)
struct Vector {
let x: Double
let y: Double
operator func *(scalar: Double): Vector {
return Vector(x: this.x * scalar, y: this.y * scalar)
}
}
let v = Vector(x: 1.0, y: 2.0)
let scaled = v * 2.5 // 向量缩放 2.5 倍
2. 比较操作符:用于对象间的比较,通常返回布尔值。
| 操作符 | 描述 | 重载函数名 | 参数与返回值 | 示例场景 |
|---|---|---|---|---|
== |
相等 | operator ==(rhs) |
this == rhs → Bool |
判断两个对象内容是否相同 |
!= |
不相等 | operator !=(rhs) |
this != rhs → Bool |
与 == 相反逻辑 |
> |
大于 | operator >(rhs) |
this > rhs → Bool |
自定义排序规则 |
< |
小于 | operator <(rhs) |
this < rhs → Bool |
自定义排序规则 |
>= |
大于等于 | operator >=(rhs) |
this >= rhs → Bool |
范围检查 |
<= |
小于等于 | operator <=(rhs) |
this <= rhs → Bool |
范围检查 |
示例:为 Person 类型重载 ==(根据 ID 判断相等)
struct Person {
let id: String
let name: String
operator func ==(rhs: Person): Bool {
return this.id == rhs.id // 仅比较 ID
}
}
let p1 = Person(id: "123", name: "Alice")
let p2 = Person(id: "123", name: "Bob")
print(p1 == p2) // 输出 true(ID 相同)
3. 一元操作符:只操作一个对象,常用于取反、自增等操作。
| 操作符 | 描述 | 重载函数名 | 参数与返回值 | 示例场景 |
|---|---|---|---|---|
- |
负号 | operator -() |
-this → 返回新实例 |
向量方向取反 |
+ |
正号 | operator +() |
+this → 返回新实例 |
保持原值(通常用于数值类型) |
! |
逻辑非 | operator !() |
!this → Bool |
自定义逻辑状态取反 |
++ |
前置自增 | operator ++() |
修改自身并返回新实例 | 计数器自增 |
-- |
前置自减 | operator --() |
修改自身并返回新实例 | 计数器自减 |
示例:为 Money 类型重载 -(负值)
struct Money {
let amount: Double
let currency: String
operator func -(): Money {
return Money(amount: -this.amount, currency: this.currency)
}
}
let debt = Money(amount: 100.0, currency: "CNY")
let credit = -debt // 负债务即信用
4. 复合赋值操作符:结合赋值与其他运算,需修改自身状态(通常用于可变类型)。
| 操作符 | 描述 | 重载函数名 | 参数与返回值 | 示例场景 |
|---|---|---|---|---|
+= |
加赋值 | operator +=(rhs) |
修改 this 并返回自身引用 |
累加器、数据聚合 |
-= |
减赋值 | operator -=(rhs) |
修改 this 并返回自身引用 |
资源消耗 |
*= |
乘赋值 | operator *=(rhs) |
修改 this 并返回自身引用 |
缩放比例更新 |
/= |
除赋值 | operator /=(rhs) |
修改 this 并返回自身引用 |
数值平均计算 |
示例:为 Counter 类型重载 +=
struct Counter {
var value: Int
operator func +=(rhs: Int) {
this.value += rhs // 修改自身状态
}
}
var counter = Counter(value: 10)
counter += 5 // 等价于 counter.value += 5
5. 其他可重载操作符
| 操作符 | 描述 | 重载函数名 | 参数与返回值 | 示例场景 |
|---|---|---|---|---|
[] |
下标访问 | operator [index] |
this[index] → 返回元素 |
自定义集合类型 |
() |
函数调用 | operator ()(参数) |
类似函数调用逻辑 | 可调用对象(如闭包) |
..< |
半开区间 | operator ..<(end) |
返回区间对象 | 自定义范围生成 |
&& |
逻辑与 | operator &&(rhs) |
需结合闭包实现惰性求值 | 自定义逻辑判断 |
四、重载操作符的限制
-
不能创建新操作符
只能重载语言预定义的操作符(如+、==),无法发明新符号(如@、$)。 -
优先级和结合性固定
重载后的操作符优先级与原生操作符一致(如*始终优先于+)。 -
至少有一个自定义类型操作数
不能为原生类型(如Int、String)重载操作符,必须作用于自定义的struct或class。 -
部分操作符不可重载
例如:?:(三元条件)、.(成员访问)、is(类型检查)等。
7、属性
一、什么是属性(Property)?
属性是仓颉语言中一种特殊的语法糖,它对外表现为普通的成员变量,但内部实现了 getter/setter 方法,用于控制数据的访问和修改。
核心作用:
- 隐藏数据存储的细节(如使用私有变量存储值)。
- 在数据访问 / 修改时执行额外逻辑(如日志记录、权限验证、数据转换)。
- 提供统一的访问接口,简化代码使用(无需显式调用
getX()/setX())。
二、属性的分类与语法
仓颉中属性分为两种类型:
-
只读属性(
prop)- 仅可读取,不可修改(类似
final变量)。 - 必须提供
get()方法,用于返回属性值。
prop x: Int { get() { return _x // 返回私有变量的值 } } - 仅可读取,不可修改(类似
-
可变属性(
mut prop)- 可读取和修改。
- 必须同时提供
get()和set()方法。
mut prop color: String { get() { return _color // 返回私有变量的值 } set(newValue) { // newValue 是默认参数名,也可自定义(如示例中的 `c`) _color = newValue // 更新私有变量 } }
三、代码示例解析
如下示例所示,开发者希望对 Point 类型的各数据成员的访问进行记录,则可以在内部声明 private 修饰的成员变量,通过声明对应的属性来对外暴露访问能力,并在访问的时候使用日志系统 Logger 记录它们的访问信息。对使用者来说,使用对象 p 的属性与访问它的成员变量一样,但内部却实现了记录的功能。 注意这里 x 和 y 是只读的,只有 get 实现,而 color 则是可变的,用 mut prop 修饰,同时具有get 和 set 实现。
class Point {
// 1. 私有成员变量:存储实际数据
private let _x: Int
private let _y: Int
private var _color: String
init(x: Int, y: Int, color: String) {
self._x = x
self._y = y
self._color = color
}
// 2. 只读属性 x:访问时记录日志
prop x: Int {
get() {
Logger.log(level: Debug, "access x")
return _x
}
}
// 3. 只读属性 y:访问时记录日志
prop y: Int {
get() {
Logger.log(level: Debug, "access y")
return _y
}
}
// 4. 可变属性 color:读写时均记录日志
mut prop color: String {
get() {
Logger.log(level: Debug, "access color")
return _color
}
set(c) { // 自定义参数名 `c`(默认是 newValue)
Logger.log(level: Debug, "reset color to ${c}")
_color = c
}
}
}
// 使用示例
main() {
let p = Point(0, 0, "red")
let x = p.x // 触发 x 的 get(),输出 "access x"
let y = p.y // 触发 y 的 get(),输出 "access y"
p.color = "green" // 触发 color 的 set(),输出 "reset color to green"
}
四、属性机制的关键特性
-
数据封装
- 私有变量(如
_x、_y、_color)存储实际数据,外部无法直接访问。 - 属性(如
x、y、color)作为公共接口,隐藏实现细节。
- 私有变量(如
-
访问控制
- 通过 getter/setter 可实现复杂的访问逻辑,例如:
mut prop age: Int { get() { return _age } set(newValue) { if (newValue < 0) { // 数据验证 throw Error("年龄不能为负数") } _age = newValue } }
- 通过 getter/setter 可实现复杂的访问逻辑,例如:
-
日志与调试
- 在 getter/setter 中添加日志,监控数据的访问和修改,例如:
get() { Logger.trace("读取属性: \(#function)") // #function 是当前函数名 return _value }
- 在 getter/setter 中添加日志,监控数据的访问和修改,例如:
-
计算属性
- 属性的值可通过计算得到,无需存储私有变量,例如:
prop area: Double { get() { return 3.14 * radius * radius // 基于其他属性计算 } }
- 属性的值可通过计算得到,无需存储私有变量,例如:
五、属性 vs. 传统 getter/setter
| 场景 | 属性语法 | 传统 getter/setter |
|---|---|---|
| 访问属性 | let x = obj.x |
let x = obj.getX() |
| 修改属性 | obj.x = 10 |
obj.setX(10) |
| 语法复杂度 | 简洁,类似变量 | 冗长,需显式调用方法 |
| 代码可读性 | 直观,符合自然语言习惯 | 繁琐,方法调用增加认知负担 |
六、注意事项
-
私有变量命名约定
- 通常使用下划线前缀(如
_x)区分属性名和私有变量名,避免混淆。
- 通常使用下划线前缀(如
-
避免无限递归
- 在 getter/setter 中必须访问私有变量,而非属性本身,否则会导致无限递归。
// 错误示例:get() 中访问属性本身,导致无限递归 prop x: Int { get() { return x // 错误!应返回 _x } }
更多推荐



所有评论(0)