携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第6天,点击查看活动详情。
委托模式(Delegation Pattern)作为软件设计模式的其中一种,可以满足继承的需求。而 Kotlin 是自带委托支持的,只需一个 by 就搞定了。
委托
基本作用
委托是由关键词 by 引出的,其基本作用就是通过一个接口的实现类对象,生成接口的实现 —— 即继承功能。我们直接来看看官方给出的例子说明:
interface Base {
fun print()
}
class BaseImpl(val x: Int) : Base {
override fun print() { print(x) }
}
class Derived(b: Base) : Base by b
fun main() {
val b = BaseImpl(10)
Derived(b).print()
}
Base 是功能接口,其实现为 BaseImpl,而 Derived 并没有直接实现 Base,却能调用方法 Base.print(),关键就在于 Base by b 的使用,它的意思就是:这个类使用对象 b 来实现接口 Base,其具体的实现,就依赖于对象 b 所对应的实现类了。上述例子的结果:
10
我们继续添加代码:
Derived(BaseImpl(5)).print()
结果:
105
10后输出了5,因为次的 Derived 用了不同的实现对象了。
委托的多态性
委托完成了继承的工作,那继承的多态特性还在吗?我们来改改前面的例子:
// ...
class Derived(b: Base) : Base by b {
override fun print() {
print("I am derived")
}
}
// ...
结果:
I am derived
嗯,没有打印 10,证明多态生效,调用了 Derived 类的 print()。
如果是普通域呢?来给 Base 增加一个 message 字段:
interface Base {
val message: String
fun print()
}
class BaseImpl(val x: Int, override val message: String = "base impl") : Base {
override fun print() { print(x) }
}
fun main() {
val b = BaseImpl(10)
Derived(b).print()
println()
println(Derived(b).message)
}
结果:
I am derived
base impl
在 Derived 中覆写之:
class Derived(b: Base) : Base by b {
override val message: String
get() = "derived message"
override fun print() {
print("I am derived")
}
}
其他不变,执行:
I am derived
derived message
一样的,多态成功。kotlin 的接口域本质上,就是一个 get 函数,自然不会丢失函数的多态性,来看到它的 java 码就明了了:
@Metadata(
mv = {1, 5, 1},
k = 1,
d1 = {"\u0000\u0018\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0010\u000e\n\u0002\b\u0003\n\u0002\u0010\u0002\n\u0000\bf\u0018\u00002\u00020\u0001J\b\u0010\u0006\u001a\u00020\u0007H&R\u0012\u0010\u0002\u001a\u00020\u0003X¦\u0004¢\u0006\u0006\u001a\u0004\b\u0004\u0010\u0005¨\u0006\b"},
d2 = {"LBase;", "", "message", "", "getMessage", "()Ljava/lang/String;", "print", "", "Kotlin2"}
)
public interface Base {
@NotNull
String getMessage();
void print();
}
凡事有一个但是,对于委托继承来讲,真正的纯多态 —— 即「对象是谁,就调用该对象的版本」—— 是不成功的,内部调用将依然调用自己的版本。
class BaseImpl(val x: Int, override val message: String = "base impl") : Base {
override fun print() { println("print: $message") }
}
class Derived(b: Base) : Base by b {
override val message: String
get() = "derived message"
}
fun main() {
val b = BaseImpl(10)
Derived(b).print()
}
上面代码中,BaseImpl 的 print() 打印时引用了 message 域,而 Derived 类覆写了 message 域。我们的期望是,Derived 对象调用 print() 时,将输出自己的 message。不过,结果是:
print: base impl
失败了,输出的还是 BaseImpl 的 message,这是因为, print() 函数是继承它的,内部引用的 message 也就只能是 BaseImpl 的。
对比一下,普通的继承就可以做到上述功能。
// 设为open
open class BaseImpl(val x: Int, override val message: String = "base impl") : Base {
override fun print() { println("print: $message") }
}
// 普通继承类
class ClassicDerived(x: Int) : BaseImpl(x) {
override val message: String
get() = "classic derived message"
}
fun main() {
val b = BaseImpl(10)
Derived(b).print()
println()
b.print()
ClassicDerived(10).print()
}
ClassicDerived 采用了普通继承方式继承了 BaseImpl,内部同样覆写了 message 的值。输出结果:
print: base impl
print: base impl
print: classic derived message
委托继承类对象的 print() 和委托对象一样,而普通继承类的 print() 虽然用了父类的逻辑,但是 message 还是多态到了自己的版本。
委托的背后
说到底,原理上委托其实没什么复杂的,它就是一个语法糖,帮我们实现了继承而已。
interface Base {
val message: String
fun print()
}
open class BaseImpl(val x: Int, override val message: String = "base impl") : Base {
override fun print() { println("print: $message") }
}
class Derived(b: Base) : Base by b {}
上面是最简单的委托,再看看它对应的java码,就知道了这真是单纯的语法糖。
public class BaseImpl implements Base {
private final int x;
@NotNull
private final String message;
public void print() {
String var1 = "print: " + this.getMessage();
boolean var2 = false;
System.out.println(var1);
}
public final int getX() {
return this.x;
}
@NotNull
public String getMessage() {
return this.message;
}
public BaseImpl(int x, @NotNull String message) {
Intrinsics.checkNotNullParameter(message, "message");
super();
this.x = x;
this.message = message;
}
// $FF: synthetic method
public BaseImpl(int var1, String var2, int var3, DefaultConstructorMarker var4) {
if ((var3 & 2) != 0) {
var2 = "base impl";
}
this(var1, var2);
}
}
这是 BaseImpl 的实现,再来看看 Derived:
public final class Derived implements Base {
// $FF: synthetic field
private final Base $$delegate_0;
public Derived(@NotNull Base b) {
Intrinsics.checkNotNullParameter(b, "b");
super();
this.$$delegate_0 = b;
}
@NotNull
public String getMessage() {
return this.$$delegate_0.getMessage();
}
public void print() {
this.$$delegate_0.print();
}
}
kotlin 中的 by b,就体现在了 Derived 的各个成员中:
getMessage()中,调用 b 的 getMessage()print()中,调用 b 的 print()
委托出来的继承,用的都是别人的实现。这就解释了前面的「真多态」为什么失效了,因为根本调用不了自己的覆写版本啊。
我们再添上覆写代码看看:
class Derived(b: Base) : Base by b {
override val message: String
get() = "derived message"
}
java码:
public final class Derived implements Base {
// $FF: synthetic field
private final Base $$delegate_0;
@NotNull
public String getMessage() {
return "derived message";
}
public Derived(@NotNull Base b) {
Intrinsics.checkNotNullParameter(b, "b");
super();
this.$$delegate_0 = b;
}
public void print() {
this.$$delegate_0.print();
}
}
虽然 message 确实覆写成功了,但是 print() 内部,还是用的委托对象 b 的 print(),按照多态的原理,自然也是使用对象 b 的 message 了。
小结
至此看来,委托继承真是朴实无华啊,和普通继承差别不大,没有什么难的。但是其实又不一样,怎么说呢?比如我们在讨论普通继承的多态,需要复用 BaseImpl 的实现时,还非要将它改成 open 以供继承。这一点,对于自己写的类倒还好,如果是继承第三方或者系统库里的 final 类,就做不到了。这就到了委托上场发挥作用的时候了。
另外,从委托继承的设计和写法来看,传统的「接口继承接口」是没法用委托完成的,毕竟,接口无参,已经有实现的委托类是无法添加到接口中去的。不过呢,我们可以将委托对象转移到实现类中,自行实现既有接口传递,又借用委托继承,有兴趣的可以写写看。