Kotlin:比private更加自私的private to this
可见性修饰符 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")
}
毕竟 String
是 CharSequence
的子类型,如果一个东西是字符串,那么这个东西也可以是一个字符序列。然后一个消费 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
调用 somethingToConsume
(this
省略了);在 consumeOthers
函数里调用 somethingToConsume
用的不是 this
,所以失败了。仅对 this
可见,字面意思。
为什么会有这样的限制呢?原因很简单,consumeOthers
的代码其实是违反了逆变泛型参数的安全限制,other.somethingToConsume
这里实际上是 other
在对外输出 T
,眼尖的同学可能早就发现了,somethingToConsume
的 T
是处在输出的位置上的。
但是“输出”是相对的,一个 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/