引言:为什么泛型不可怕,反而很方便
\n\n想象一下,你正在编写一个“盒子”类。里面可以放任何东西:书、苹果、手机。但如果你把它设计成针对 Object,那么每次取出时都不得不做 as? Book 转换,并祈祷不要遇到 ClassCastException。熟悉吗?
这时 泛型(generics) 就派上用场了——它是 Kotlin 中一种强大的类型化机制,让你能够编写灵活的代码,同时又不牺牲类型安全性。在本文中,我们将由浅入深地讲解 Kotlin 中的泛型:基础类、函数、变型(in/out)、具体化类型和星投影。你将学会如何让代码更整洁,让编译器成为你最重要的盟友。
\n\n本文为教育门户网站 pythonlib.ru 编写——它是你通往现代编程语言世界的向导。
\n\n1. 基础:什么是 Kotlin 中的泛型?
\n\n泛型(泛型类型) 允许将类、接口和函数参数化为在使用时指定的类型。这避免了代码重复,并在编译阶段提供了对类型的控制。
\n\n来看一个最简单的例子:
\n\nclass Box<T>(val item: T)\n\nfun main() {\n val bookBox = Box<String>("Kotlin in Action")\n val intBox = Box<Int>(42)\n // 编译器知道类型!\n val title: String = bookBox.item\n}\n\n关键点:
\n- \n
<T>— 类型参数(可以是任何字母,但按惯例使用 T、R、E、K、V)。 \n - 类型在创建对象时固定——运行时无需任何强制转换。 \n
- 泛型仅在编译阶段工作(类型擦除),但 Kotlin 添加了“具体化”(稍后介绍)。 \n
泛型函数
\nKotlin 中的类型化也适用于函数。例如,一个返回列表第一个元素的函数:
\n\nfun <T> firstItem(list: List<T>): T? = list.firstOrNull()\n\nfun main() {\n val numbers = listOf(1, 2, 3)\n val first: Int? = firstItem(numbers) // T = Int\n println(first) // 1\n}\n\n请注意:Kotlin 能够根据上下文推断类型——不必写成 firstItem<Int>(numbers)。
2. 变型:in 和 out —— 如何不破坏类型安全
\n\n对于初学者来说,最复杂的主题是 变型(variance)。它回答的问题是:List<String> 能否是 List<Any> 的子类型?
在 Java 中,这通过通配符(? extends / ? super)解决。在 Kotlin 中,则是 out 和 in。
out(协变)—— 只读
\n标记 out 意味着泛型类型只能被“产出”(从方法中返回)。例如 List<out T>。
interface Source<out T> {\n fun next(): T // 正确:T 在输出位置\n // fun add(item: T) // 错误!T 在输入位置被禁止\n}\n\nfun demo(source: Source<String>) {\n val anySource: Source<Any> = source // out 允许这样做\n}\n\n为什么这是安全的?如果我们只能从源中读取 String,那么可以将其视为 Any 的源。
in(逆变)—— 只写
\nin 则相反:类型只能出现在输入位置。经典例子是 Comparable。
interface Comparable<in T> {\n operator fun compareTo(other: T): Int\n}\n\nfun demo(comp: Comparable<Number>) {\n val intComp: Comparable<Int> = comp // in 允许这样做\n}\n\n如果 Comparable<Number> 能够比较任何数字,那么它当然也能比较 Int。
in out(不变)—— 默认情况
\n如果既没有指定 in 也没有指定 out,那么类型是不变的(invariant)。这意味着 Box<String> 既不是 Box<Any> 的子类型也不是其父类型。这是最严格的模式,也是默认模式。