扩展类

什么是“扩展类”?

“扩展类”的意思是:扩展已有的类型的成员。

比如 string 是标准库内置的类,提供了诸多工具函数,但如果有个功能 string 没有提供,开发者就只能自己实现一个工具函数。

但问题是自己实现的工具函数,只能通过 foo(s) 调用。
而开发者可能认为这个工具函数非常通用,应该和 string 类其他内置方法一样,通过 s.foo() 调用。

这样的好处是:

  1. 用法统一,减少记忆负担。
  2. 多次调用写起来更顺:s.foo().foo().foo()foo(foo(foo(s))) 更优雅。

设计难点

  1. 有没有必要支持扩展类?如果支持扩展类,有什么缺点?
  2. 只支持扩展方法还是同时支持扩展属性?
  3. 如果扩展和类成员同名,或者有多个同名的扩展,怎么处理?
  4. 怎么支持扩展泛型?怎么支持扩展特定类型参数的泛型?
  5. 怎么实现两个依赖的包存在同名扩展,还能让它们并存?

设计思路

先不思考有没有必要支持扩展类,先假设我们需要支持,那语法应该是怎样的?

参考其他语言,有三个方向:

1. extension 关键字

  1. extension String {
  2. foo() {
  3. return length
  4. }
  5. }

或者

  1. extension MyExt for String {
  2. foo() {
  3. return length
  4. }
  5. }

2. this 参数

  1. function foo(this: string) {
  2. return this.length
  3. }

或者

  1. function foo(this p: string) {
  2. return p.length
  3. }

3. 类型函数名

  1. function string.foo() {
  2. return this.length
  3. }

或者

  1. function <string> foo() {
  2. return this.length
  3. }

其中,方案一能支持扩展字段,其他方案则比较麻烦。

那就得思考是否需要支持扩展字段?

从原理上,扩展方法和扩展字段是有本质区别的。

因为扩展字段,需要在创建类实例时,就知道存在这个字段,以提前预留内存空间。

假设包 a 定义了类 C,然后包 b 引用了类 C,并扩展了一个字段 x。
那么所有 C 的实例必须都包含 x 字段,否则包 a 创建的 C 实例就因为缺少 x 字段而无法传递给包 b 的函数。

这会导致一个现象,假如用户扩展了 string 类的字段,那所有 string 的实例都会增加内存占用,即使大量 string 实例用不到它。
更严重的由于内存结构变化,可能会导致很多指针操作出错。

几行代码就能破坏已经测试通过的代码,这在工程上是不允许的。

从这个角度看,允许扩展字段是弊大于利的。

如果不支持扩展字段,那就无法支持通过扩展方法让类符合某个接口定义,也无法支持“override”扩展函数。

因此,当决策是“不支持扩展字段”时,方案二和方案三比方案一好,因为方案一容易让开发者误以为它支持扩展字段。

方案二和方案三对比,我认为方案二更优,因为:

  1. 相比方案三,它对语法的修改更少,更容易让开发者看懂其含义。
  2. this 的含义更清晰,不存在隐式 this

方案二中,用参数名 this 还是参数修饰符 this,我倾向于前者,因为:

  • 便于类型自身函数和扩展函数之间复制代码。

当一个扩展函数比较好用时,用户很可能会将其提升为类型内置方法,这时就存在复制代码的需求。

综上,应采用方案二的语法,且扩展只能作为语法糖,无法实际改变类本身,这意味着:

  1. 一个类本身不符合某个接口,即使通过扩展补充了方法,也依旧不符合。
  2. 通过 any 或父类型,不能调用扩展方法,因为扩展方法实际并不存在于类上。
  3. a 实际是 null 时,a.foo() 也能调用成功。

第二个问题:怎么处理同名扩展?

编译器无法猜测哪个扩展才是正确的。
那只能报错让用户选择。

扩展可以设计成:必须是当前范围存在 foo 引用时,才能使用 foo 扩展,这样允许依赖有同名扩展,但一个作用域只能使用其中一个。

有了扩展语法的初步设计,最后思考,是否需要将它加入语言?

如果支持了扩展类:

  • 从作者角度,只需要敲 a.,所有可用 API 都能列出来,不管这个 API 是谁定义的。
  • 从读者角度,a.foo() 有两者可能:a 本身定义;某个 function foo(this: A) 函数。

因此,这个设计是利好作者而不利于读者。

如果一个包引用了依赖包 A1.0 里的类 C,并扩展了方法 foo,那么这个包里 c.foo() 都指向扩展。

后来,A1.0 升级了、并内置提供了方法 foo,而当前包的源码没有修改。
那编译器有两个选择:

  1. 采用内置方法优先的原则:自动使用新增内置方法 foo,这很容易导致代码出现 bug。
  2. 当扩展方法和内置方法同名时,报错。那会出现一个现象:

升级某个依赖,可能会导致项目编译报错。

因此,支持扩展类,从工程上是一个隐患。

最终决策

暂不支持扩展类。

后续等待用户反馈再作决策。