Kotlin 中 out 背后隐藏着什么?回答常见问题
\n\n如果你曾经在 Kotlin 中编写过泛型并遇到过难以理解的编译错误,那么你很可能会遇到 out 这个词。这个小小的修饰符即使在经验丰富的开发者中也引发了许多争议。我们收集了来自 Stack Overflow 的最常见问题,并将其转化为一份易懂的指南。今天你将了解什么是 out,它的作用是什么,以及如何避免在协变中迷失方向。
让我们一劳永逸地弄清楚——没有枯燥的理论,只有鲜活的代码和实践。
\n\n为什么 Kotlin 要发明 out?(简要历史回顾)
\n\n在 Java 中,有通配符——问号 ? extends T。它表示集合可以包含类型 T 或其子类型的对象。问题在于,Java 并不禁止你意外地将错误类型的对象放入这样的集合中——你只会在运行时得到错误。Kotlin 在类型层面解决了这个问题。
Kotlin 中的关键字 out 是协变的声明。如果你写 Source<out T>,这意味着:“我可以从这个 Source 中读取类型 T 的对象,但不能向其中写入”。编译器会严格检查这一点。
问题 1:out 和 in 有什么区别?
\n\n这是 Stack Overflow 上最常见的问题。让我们想象一个盒子:
\n\n- \n
out—— 一个只能从中取出物品的盒子(生产者)。 \n in—— 一个只能向其中放入物品的盒子(消费者)。 \n
示例:
\n\n// 协变:可以读取,但不能写入\ninterface Producer<out T> {\n fun produce(): T\n}\n\n// 逆变:可以写入,但不能读取\ninterface Consumer<in T> {\n fun consume(item: T)\n}\n\nfun main() {\n val stringProducer: Producer<String> = object : Producer<String> {\n override fun produce(): String = "Hello"\n }\n \n val anyProducer: Producer<Any> = stringProducer // 有效!out 允许这样做\n println(anyProducer.produce())\n}\n\n\n请注意:Producer<out T> 允许将 Producer<String> 赋值给类型为 Producer<Any> 的变量。没有 out,这是不可能的。
问题 2:为什么 Kotlin 在使用 out 时禁止 fun set(value: T)?
\n\n这是一个经典的陷阱。如果你声明了 out,编译器将不允许你拥有接受 T 作为参数的函数。为什么?
想象一下,你有一个 MutableList<out Number>。你可以向其中放入 Int,但编译器不知道这个列表实际上是什么。也许它是一个 MutableList<Double>?如果允许写入,你就会破坏类型。out 保证了安全性:你只能读取。
// 不会编译通过!\ninterface BadList<out T> {\n fun get(index: Int): T // OK\n fun add(item: T) // 错误!T 处于 in 位置\n}\n\n\n解决方案:如果你需要同时读取和写入,请分别使用 in 和 out,或者使用不变类型(不带修饰符)。
\n\n问题 3:out 与 Java 中的通配符 ? extends 有何关联?
\n\n直接对应关系:Kotlin 中的 Source<out T> = Java 中的 Source<? extends T>。但有一个重要区别:
- \n
- 在 Java 中,通配符是类型的使用点变型(use-site variance)。 \n
- 在 Kotlin 中,
out是类型的声明点变型(declaration-site variance)。 \n
Java 示例:
\n\n// Java:在声明变量时使用通配符\nList<? extends Number> numbers = new ArrayList<Integer>();\nnumbers.a