目录

常见语法糖举例

1、函数重载

2、命名函数

3、参数默认值:

建造者模式:

为什么会需要建造者模式?

建造者模式如何解决问题?

回到原问题:为什么说 “函数默认值可以减少引入建造者模式的需求”?

总结:建造者模式 vs. 函数默认值

 4、尾随lambda(trailing lambda)

5、管道(Pipeline)操作符

6、操作符重载

一、什么是操作符重载?

二、仓颉中的操作符重载规则

三、代码示例解析:为 Point 类型重载 + 操作符

四、重载操作符的限制

7、属性

一、什么是属性(Property)?

二、属性的分类与语法

三、代码示例解析

四、属性机制的关键特性

五、属性 vs. 传统 getter/setter

六、注意事项


什么是语法糖?

语法糖(Syntactic Sugar) 是编程语言中一种 “便捷写法”,本质上不会改变语言的功能或逻辑,但能让代码更简洁、易读,减少开发者的编码工作量。
就像给复杂的语法裹上一层 “糖衣”,让开发者用更简单的方式表达相同的意思,同时编译器或解释器会把这些 “糖衣” 还原成正式的语法结构执行。

核心特点

  1. 不改变功能
    语法糖的底层实现和传统写法完全等价,只是写法更简化。
    例如:

    • 数学中用  表示 x × x² 就是语法糖,本质还是乘法运算。
    • 编程中用 a += 1 代替 a = a + 1,两者执行逻辑完全相同。
  2. 提升开发效率
    减少重复代码,让开发者更聚焦业务逻辑。
    例如:

    • 循环遍历数组时,用 for (item in array) 代替传统的 for (int i=0; i<array.length; i++)
    • 仓颉中可能用 let list = [1, 2, 3] 快速创建数组,而无需调用构造函数。

常见语法糖举例

场景 语法糖写法 等价传统写法
变量赋值 a += 1 a = a + 1
空值判断 val = obj?.prop if (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 : b if (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),这个对象有很多可选参数(如姓名、年龄、地址、邮箱、权限等级等)。如果直接通过构造函数来创建对象,可能会面临以下问题:

  1. 参数数量多且容易混淆:构造函数可能需要十几个参数,调用时很难记住每个参数的顺序和含义。
  2. 可选参数处理麻烦:如果某些参数是可选的,可能需要写很多重载的构造函数(不同参数组合的版本),导致代码冗余。

例如,传统方式可能需要这样的代码:

// 多个重载的构造函数(伪代码示例)
User(String name); // 仅姓名
User(String name, int age); // 姓名+年龄
User(String name, int age, String address); // 姓名+年龄+地址
... // 更多参数组合的版本

这种方式会让代码变得臃肿,维护困难。

建造者模式如何解决问题?

建造者模式通过一个 “建造者” 类来逐步设置对象的参数,最后一次性创建对象。它的核心思路是:

 
  1. 将对象的构建过程封装到建造者类中,通过链式调用设置参数。
  2. 分离构建过程和对象表示,同一个建造者可以灵活构造不同配置的对象。

用建造者模式改写后的代码示例(伪代码):

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"),这就是操作符在不同类型上的 “重载”。

二、仓颉中的操作符重载规则
  1. 通过 operator 关键字定义重载函数
    格式:operator func 操作符(参数) -> 返回类型 { ... }
  2. 参数规则
    • 一元操作符(如 !):1 个参数(rhs 或 lhs,取决于操作符位置)。
    • 二元操作符(如 +):2 个参数,第一个参数用 this 表示当前实例(左操作数),第二个参数用 rhs 表示右操作数。
  3. 作用于自定义类型
    只能为 结构体(struct) 或 类(class) 重载操作符,不能为基础类型(如 IntString)重载。
三、代码示例解析:为 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

代码逐行解析

  1. 定义 Point 结构体

    • 包含 x 和 y 两个整数属性,表示点的坐标。
    • init 初始化方法用于创建实例。
  2. 重载 + 操作符

    • operator func +(rhs: Point): Point
      • operator 关键字声明这是一个操作符重载函数。
      • + 是要重载的操作符(二元操作符)。
      • rhs: Point 表示右操作数的类型为 Point
      • 返回值类型为 Point,即两个点相加后的新点。
  3. 函数体逻辑

    • this 代表左操作数(如 a + b 中的 a),是当前结构体的实例。
    • 通过 this.x + rhs.x 和 this.y + rhs.y 计算新点的坐标,返回一个新的 Point 实例。
  4. 调用方式

    • 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) 需结合闭包实现惰性求值 自定义逻辑判断
四、重载操作符的限制
  1. 不能创建新操作符
    只能重载语言预定义的操作符(如 +==),无法发明新符号(如 @$)。

  2. 优先级和结合性固定
    重载后的操作符优先级与原生操作符一致(如 * 始终优先于 +)。

  3. 至少有一个自定义类型操作数
    不能为原生类型(如 IntString)重载操作符,必须作用于自定义的 struct 或 class

  4. 部分操作符不可重载
    例如:?:(三元条件)、.(成员访问)、is(类型检查)等。

7、属性

一、什么是属性(Property)?

属性是仓颉语言中一种特殊的语法糖,它对外表现为普通的成员变量,但内部实现了 getter/setter 方法,用于控制数据的访问和修改。
核心作用

  • 隐藏数据存储的细节(如使用私有变量存储值)。
  • 在数据访问 / 修改时执行额外逻辑(如日志记录、权限验证、数据转换)。
  • 提供统一的访问接口,简化代码使用(无需显式调用 getX()/setX())。
二、属性的分类与语法

仓颉中属性分为两种类型:

  1. 只读属性(prop

    • 仅可读取,不可修改(类似 final 变量)。
    • 必须提供 get() 方法,用于返回属性值。
    prop x: Int {
        get() {
            return _x  // 返回私有变量的值
        }
    }
  2. 可变属性(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"
}
四、属性机制的关键特性
  1. 数据封装

    • 私有变量(如 _x_y_color)存储实际数据,外部无法直接访问。
    • 属性(如 xycolor)作为公共接口,隐藏实现细节。
  2. 访问控制

    • 通过 getter/setter 可实现复杂的访问逻辑,例如:
      mut prop age: Int {
          get() { return _age }
          set(newValue) {
              if (newValue < 0) {  // 数据验证
                  throw Error("年龄不能为负数")
              }
              _age = newValue
          }
      }
      
  3. 日志与调试

    • 在 getter/setter 中添加日志,监控数据的访问和修改,例如:
      get() {
          Logger.trace("读取属性: \(#function)")  // #function 是当前函数名
          return _value
      }
      
  4. 计算属性

    • 属性的值可通过计算得到,无需存储私有变量,例如:
      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)
语法复杂度 简洁,类似变量 冗长,需显式调用方法
代码可读性 直观,符合自然语言习惯 繁琐,方法调用增加认知负担
六、注意事项
  1. 私有变量命名约定

    • 通常使用下划线前缀(如 _x)区分属性名和私有变量名,避免混淆。
  2. 避免无限递归

    • 在 getter/setter 中必须访问私有变量,而非属性本身,否则会导致无限递归。
    // 错误示例:get() 中访问属性本身,导致无限递归
    prop x: Int {
        get() {
            return x  // 错误!应返回 _x
        }
    }
    
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐