Kotlin:比private更加自私的private to this

Author Avatar
Dexlind 7月 17, 2020
  • 在其它设备中阅读本文章

可见性修饰符 private,对于顶层声明来说是该文件内可见,对于类内部的成员来说是该类内部可见,这是大家都知道的事。不过 Kotlin 里还存在着可见范围更小的,那就是 private to this,仅对 this 可见 。

我们先从泛型逆变(contravariant)说起。比如说我们有一个消费者:

interface Consumer<in T> {
   fun consume(t: T): Unit
}

它的类型参数 T 是逆变的。这意味着 T 只能出现在成员的输入位置,如例子中 consume 函数的参数位置,而不能出现在成员的输出位置(比如说返回值位置)。这个限制是为了确保类型安全。

然后因为是逆变的,这使得 Consumer<CharSequence>Consumer<String> 的子类型:

fun test(c: Consumer<CharSequence>) {
   val consumer : Consumer<String> = c
   consumer.consume("test")
}

毕竟 StringCharSequence 的子类型,如果一个东西是字符串,那么这个东西也可以是一个字符序列。然后一个消费 CharSequence 的消费者当然可以拿一个 String 当做 CharSequence 来消费,所以说 “消费 String 的消费者” 可以用 “消费 CharSequence 的消费者” 来代替。

这个替代关系很好地阐释了 Consumer<CharSequence>Consumer<String> 的子类型。

现在我们改一下消费者的逻辑,把接口删了换成类,并且让她在出生的时候就能拿到消费品,这样并不会改变逆变的性质:

class Consumer<in T>(t: T) {
   private val somethingToConsume: T = t

   fun consumeMyThing(): Unit = println(somethingToConsume)

   fun consume(t: T): Unit = println(t)
}

fun test() {
   val consumer : Consumer<String> = Consumer<Any>(Any())
   consumer.consume("test")
}

是时候回归主题了,我们的消费者小姐,她的 somethingToConsume,可见性就是 private to this,仅对 this 可见。

比如说消费者小姐看中了别人的消费品,想要抢过来玩:

class Consumer<in T>(t: T) {
   private val somethingToConsume: T = t

   fun consumeMyThing(): Unit = println(somethingToConsume)

   fun consumeOthers(other: Consumer<String>): Unit {
      val string = other.somethingToConsume
      println(string)
      // Error: Cannot access 'somethingToConsume'
      // It is private/*private to this*/ in 'Consumer'
   }
}

编译器看到了这样的违法行为,马上阻止了她:你只能玩你自己的东西。

之所以 consumeMyThing 可以通过编译,是因为通过 this 调用 somethingToConsumethis 省略了);在 consumeOthers 函数里调用 somethingToConsume 用的不是 this,所以失败了。仅对 this 可见,字面意思。

为什么会有这样的限制呢?原因很简单,consumeOthers 的代码其实是违反了逆变泛型参数的安全限制,other.somethingToConsume 这里实际上是 other 在对外输出 T,眼尖的同学可能早就发现了,somethingToConsumeT 是处在输出的位置上的。

但是“输出”是相对的,一个 private 的东西,自产自销自己用,那不算输出,是安全的。但是像上面那样从别人家里那东西,那就相当于是别人在输出了。

可以演示一下如果不存在 private to this 的限制会发生什么问题。

class Consumer<in T>(t: T) {
   private val somethingToConsume: T = t

   fun consumeMyThing(): Unit = println(somethingToConsume)

   fun consumeOthers(other: Consumer<String>): Unit {
      @Suppress("INVISIBLE_MEMBER")
      val string = other.somethingToConsume // dangerous!!
      println(string)
   }
}

fun test() {
   val intConsumer = Consumer(42)
   val anyConsumer = Consumer(Any())
   // 因为是逆变的,所以 Consumer<Any> 是 Consumer<String> 的子类型
   intConsumer.consumeOthers(anyConsumer)

   // intConsumer 想要从别人手里拿到一个 String,但是实际上拿到的是 Any
}

这里使用了我的那篇文章介绍的技巧,使用 @Suppress("INVISIBLE_MEMBER") 强行无视可见性的限制。

运行代码然后就得到了一个类型转换异常:

Exception in thread "main" java.lang.ClassCastException: java.lang.Object cannot be cast to java.lang.String
    at Consumer.consumeOthers

所以现在来总结一下。当一个类、接口的逆变的泛型参数出现在 private 成员的输出位置时(比较常见的是返回值位置),那么那个 private 成员,实际上可见性是 private to this。举例:

class Test<in T> {
   private val foo: T = TODO()
   private var bar: T = TODO()

   private fun bas(): T = TODO()
}

为了允许上面这些代码合法存在,但是又要禁止不安全的调用,这就是为什么要有 private to this 的原因。

本文完。

知识共享许可协议
本作品采用知识共享 署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可,转载请注明出处。

本文链接:https://aisia.moe/2020/07/17/kotlin-private-to-this/