Kotlin:浅谈委托

172 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 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()
}

上面代码中,BaseImplprint() 打印时引用了 message 域,而 Derived 类覆写了 message 域。我们的期望是,Derived 对象调用 print() 时,将输出自己的 message。不过,结果是:

print: base impl

失败了,输出的还是 BaseImplmessage,这是因为, 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 类,就做不到了。这就到了委托上场发挥作用的时候了。

另外,从委托继承的设计和写法来看,传统的「接口继承接口」是没法用委托完成的,毕竟,接口无参,已经有实现的委托类是无法添加到接口中去的。不过呢,我们可以将委托对象转移到实现类中,自行实现既有接口传递,又借用委托继承,有兴趣的可以写写看。