27

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

 4 years ago
source link: https://aisia.moe/2020/07/17/kotlin-private-to-this/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

可见性修饰符 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 的原因。

本文完。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK