乐于分享
好东西不私藏

华为仓颉编程语言:不改源码,给 String 偷偷加成员函数

华为仓颉编程语言:不改源码,给 String 偷偷加成员函数

不改源码,给 String 偷偷加成员函数

华为仓颉编程语言里的扩展,有一种很让人眼前一亮的能力:不去改 `String` 的源码,也能让字符串实例像是多了一个成员函数。

这不是修改标准库,也不是运行期把成员函数塞进去,而是在当前包的编译上下文里,用 `extend` 为一个可见类型补充成员。写完以后,调用体验就像这个成员函数原本属于 `String` 一样。

extend String {          public func printSize() {          println("the size is ${this.size}")          }          }          main() {          let text = "123"          text.printSize()          }

这段代码的重点是 `extend String`。它告诉编译器:在当前可见范围内,我要给 `String` 增加一组额外能力。于是 `text.printSize()` 可以像普通成员函数一样调用。

扩展不是改类,而是补能力

理解扩展时,最容易误会的一点是:它不是把原类型定义改掉。`String` 的源码没有变化,标准库也没有被重新编译。扩展只是让当前包里的类型检查和成员查找,知道这个类型还可以使用一批新成员。

这就像给一本书夹了一张便签。书本身没改,但你在阅读这本书时,可以顺手看到额外说明。扩展的价值就在这里:它不破坏原类型封装,却能让代码更贴近业务表达。

例如我们可以为字符串补一个“带边框打印”的函数:

extend String {          public func printBox() {          let border = "=========="          println(border)          println(this)          println(border)          }          }          main() {          "hello".printBox()          }

这样的函数如果写成普通工具函数,调用形式可能是 `printBox(text)`;写成扩展后,调用形式变成 `text.printBox()`。读起来更像“这个字符串自己执行了一个动作”。

String 可以加函数,也可以加计算属性

扩展不仅能加成员函数,也能加成员属性。不过这里的属性必须有实现,本质上是计算属性,不能新增存储字段。

比如我们可以给 `String` 补一个 `isLong` 属性:

extend String {          public prop isLong: Bool {          get() {          this.size > 10          }          }          }          main() {          let title = "Cangjie extension"          println(title.isLong)          }

这里没有给 `String` 增加一个真实存储的变量。`isLong` 每次访问时都会根据 `this.size` 算出结果。

如果扩展允许随便加成员变量,原类型的内存布局和封装边界都会被打乱。仓颉明确禁止扩展增加成员变量,正是为了保持类型模型清晰稳定。

this 可以用,private 不能碰

在扩展的实例成员里,可以使用 `this` 访问当前对象,也可以省略 `this`。前面的 `this.size` 就是典型用法。

但是扩展不能访问被扩展类型里的 `private` 成员。原因很直接:扩展不是类型定义的一部分,它不能突破原类型作者设置的封装边界。

class UserName {          private let raw: String          public init(raw: String) {          this.raw = raw          }          public prop size: Int64 {          get() { raw.size }          }          }          extend UserName {          public func printSize() {          println(this.size)          // println(raw)          }          }

在这个例子里,扩展可以访问 `public prop size`,但不能直接访问 `private let raw`。这正是扩展和继承,扩展和源码修改之间的边界。

不能遮蔽已有成员

扩展还有一个重要规则:不能重新定义类型上已经有的成员,也不能和同一类型的其他扩展成员重名。

如果一个类型已经有 `f`,扩展里不能再写一个同签名的 `f` 来覆盖它。

class A {          public func f() {}          }          extend A {          // public func f() {}          }

同样,两个扩展之间也不能互相遮蔽:

class B {}          extend B {          public func g() {}          }          extend B {          // public func g() {}          }

这个规则让成员查找更确定。看到 `x.g()` 时,编译器和读代码的人都不需要猜:到底是哪一个扩展抢到了这个名字。

直接扩展和接口扩展,思路不一样

给 `String` 加 `printSize` 属于直接扩展。它只是补一个成员函数,没有改变类型和接口之间的抽象关系。

接口扩展则更进一步:它能让一个已有类型实现某个接口。比如我们定义一个 `PrintableSize`,然后让 `String` 通过扩展实现它。

interface PrintableSize {          func printSize(): Unit          }          extend String <: PrintableSize {          public func printSize(): Unit {          println("the size is ${this.size}")          }          }          func show(item: PrintableSize) {          item.printSize()          }          main() {          show("123")          }

这时 `String` 不只是“多了一个成员函数”,还可以被当作 `PrintableSize` 使用。接口扩展适合把已有类型纳入新的抽象体系,让业务代码获得更灵活的多态表达。

导入导出规则决定它能走多远

扩展的导入导出规则很值得注意,尤其是扩展标准库类型时。

在当前包里直接扩展 `String`,当前包可以直接使用新成员。但直接扩展只要和被扩展类型不在同一个包,就不会作为公共能力导出。也就是说,你在自己的包里给 `String` 写了一个直接扩展,别的包导入你的包以后,并不能像本地成员一样使用它。

这条规则能避免一种混乱:一个包导入之后,全世界的 `String` 都突然多出各种成员函数。仓颉让扩展能力遵守包边界,代码关系会更可控。

如果你是在做应用内部的语法糖,直接扩展很顺手。如果你是在做公共库,就要认真设计 API:是提供普通函数,还是定义接口并用接口扩展,或者干脆封装一个新的业务类型。

什么时候适合给 String 加扩展

适合扩展的场景,通常有三个共同点。

第一,这个能力确实像目标类型自己的行为。比如 `text.printSize()`,`text.printBox()`,读起来都自然。

第二,它不需要额外存储状态。因为扩展不能加成员变量,所以它更适合纯计算,格式化,转换,校验这类能力。

第三,它应该服务于局部清晰,而不是制造全局惊喜。扩展让代码更短,但也要让团队成员容易找到它在哪里定义,为什么存在。

一个稳妥的命名方式,是让扩展函数表达出明确动作:

extend String {          public func printDebugLabel(label: String) {          println("${label}: ${this}")          }          }          main() {          "ready".printDebugLabel("status")          }

扩展的美感在于,它让已有类型长出更贴近业务的表达,却仍然尊重源码,封装和包边界。学会它以后,你会发现很多工具函数都可以变成更顺手的成员调用。

学习仓颉编程,就找九丘教育!