扩展类
什么是“扩展类”?
“扩展类”的意思是:扩展已有的类型的成员。
比如 string 是标准库内置的类,提供了诸多工具函数,但如果有个功能 string 没有提供,开发者就只能自己实现一个工具函数。
但问题是自己实现的工具函数,只能通过 foo(s) 调用。
而开发者可能认为这个工具函数非常通用,应该和 string 类其他内置方法一样,通过 s.foo() 调用。
这样的好处是:
- 用法统一,减少记忆负担。
- 多次调用写起来更顺:
s.foo().foo().foo()比foo(foo(foo(s)))更优雅。
设计难点
- 有没有必要支持扩展类?如果支持扩展类,有什么缺点?
- 只支持扩展方法还是同时支持扩展属性?
- 如果扩展和类成员同名,或者有多个同名的扩展,怎么处理?
- 怎么支持扩展泛型?怎么支持扩展特定类型参数的泛型?
- 怎么实现两个依赖的包存在同名扩展,还能让它们并存?
设计思路
先不思考有没有必要支持扩展类,先假设我们需要支持,那语法应该是怎样的?
参考其他语言,有三个方向:
1. extension 关键字
extension String {foo() {return length}}
或者
extension MyExt for String {foo() {return length}}
2. this 参数
function foo(this: string) {return this.length}
或者
function foo(this p: string) {return p.length}
3. 类型函数名
function string.foo() {return this.length}
或者
function <string> foo() {return this.length}
其中,方案一能支持扩展字段,其他方案则比较麻烦。
那就得思考是否需要支持扩展字段?
从原理上,扩展方法和扩展字段是有本质区别的。
因为扩展字段,需要在创建类实例时,就知道存在这个字段,以提前预留内存空间。
假设包 a 定义了类 C,然后包 b 引用了类 C,并扩展了一个字段 x。
那么所有 C 的实例必须都包含 x 字段,否则包 a 创建的 C 实例就因为缺少 x 字段而无法传递给包 b 的函数。
这会导致一个现象,假如用户扩展了 string 类的字段,那所有 string 的实例都会增加内存占用,即使大量 string 实例用不到它。
更严重的由于内存结构变化,可能会导致很多指针操作出错。
几行代码就能破坏已经测试通过的代码,这在工程上是不允许的。
从这个角度看,允许扩展字段是弊大于利的。
如果不支持扩展字段,那就无法支持通过扩展方法让类符合某个接口定义,也无法支持“override”扩展函数。
因此,当决策是“不支持扩展字段”时,方案二和方案三比方案一好,因为方案一容易让开发者误以为它支持扩展字段。
方案二和方案三对比,我认为方案二更优,因为:
- 相比方案三,它对语法的修改更少,更容易让开发者看懂其含义。
this的含义更清晰,不存在隐式this。
方案二中,用参数名 this 还是参数修饰符 this,我倾向于前者,因为:
- 便于类型自身函数和扩展函数之间复制代码。
当一个扩展函数比较好用时,用户很可能会将其提升为类型内置方法,这时就存在复制代码的需求。
综上,应采用方案二的语法,且扩展只能作为语法糖,无法实际改变类本身,这意味着:
- 一个类本身不符合某个接口,即使通过扩展补充了方法,也依旧不符合。
- 通过
any或父类型,不能调用扩展方法,因为扩展方法实际并不存在于类上。 - 当
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,而当前包的源码没有修改。
那编译器有两个选择:
- 采用内置方法优先的原则:自动使用新增内置方法 foo,这很容易导致代码出现 bug。
- 当扩展方法和内置方法同名时,报错。那会出现一个现象:
升级某个依赖,可能会导致项目编译报错。
因此,支持扩展类,从工程上是一个隐患。
最终决策
暂不支持扩展类。
后续等待用户反馈再作决策。