用AI赚第一桶💰低成本搭建一套AI赚钱工具,源码可二开。 广告
# 泛型 与 Java 中一样,Kotlin 中的类可以有类型参数: ``` kotlin class Box<T>(t: T) { var value = t } ``` 通常,要创建一个类的实例,我们需要提供类型实参: ``` kotlin val box: Box<Int> = Box<Int>(1) ``` 但是如果参数可以推断,例如从构造器实参或通过其它的手段,都允许省略类型实参: ``` kotlin val box = Box(1) // 1 有类型 Int,因此编译器会以整数看待,我们称之为 Box<Int> ``` ## 变化 Java 类型系统最复杂部分的其中之一的通配符类型(查看[Java 泛型问答](http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html)) Kotlin 并没有。作为替代,它有两种其它的东西:声明场所差异和类型预测。 首先,让我们想一想关于为什么 Java 需要那些诡异的通配符。问题就是在 [Effective Java](http://www.oracle.com/technetwork/java/effectivejava-136174.html),Item 28:*Use bounded wildcards to increase API flexibility*。首先,Java 中的泛型是不变的,意思就是 `List<String>` 不是 `List<Object>` 的子类型。为什么是这样?如果 List 不是不变的,它应该几乎等于 Java 的数组,因此下面的代码会通过编译然后在运行时引发一个错误: ``` java // Java List<String> strs = new ArrayList<String>(); List<Object> objs = strs; // !!! 这里即将引发一个问题。Java 禁止这样 objs.add(1); // 这里我们放置一个 Integer 到一个 String list 中 String s = strs.get(0); // !!! ClassCastException: Cannot cast Integer to String ``` 因此,Java 为了保证运行时的安全禁止这样的东西。但这有一些隐患。例如,考虑 `Collection` 接口的 `addAll()` 方法,这个方法的签名是什么?凭直觉我们应该这样放置它: ``` java // Java interface Collection<E> ... { void addAll(Collection<E> items); } ``` 但另一方面,我们应该不能做到下面这样简单的事(完全是安全的): ``` java // Java void copyAll(Collection<Object> to, Collection<String> from) { to.addAll(from); // !!! 这种幼稚的 addAll 声明不能被编译 // Collection<String> 不是 Collection<Object> 的子类型 } ``` (Java 中,我们都学过这种困难的课程,查看 [Effective Java](http://www.oracle.com/technetwork/java/effectivejava-136174.html), Item 25: *Prefer lists to arrays*) 那就是为什么实际的 `addAll()` 签名是下面这样: ``` java // Java interface Collection<E> ... { void addAll(Collection<? extends E> items); } ``` **通配符类型实参** `? extends T` 指示这个方法接受*类型为 `T` 的一些子类型,而非 `T` 本身*的对象的集合。这个意思是我们可以从项目中安全地**读** `T`(这个集合的元素都是一个 T 子类的实例),但**不能写**进它,由于我们不知道什么对象才是 `T` 的未知子类型。在这种限制的返回中,我们有了所期望的行为: `Collection<String>` *是* `Collection<? extends T>` 的子类型。在“clever words”中,通配符与扩展边界(上层边界)一起构成了类型**协变**。 理解的关键在于为什么这个工作策略如此简单:如果你可以仅仅从一个集合接受项目,然后很好地使用一个集合的 `String` 并读取 `Object`。反过来,如果你可以仅仅_放置_项目到集合里,它可以很好地从拿取一个集合的 `Object` 并放置 `String` 到它里面:Java 中我们有 `List<Object>` 的超类 `List<? super String>`。 后者称为**抗变性**,而且你可以在 `List<? super String>` 上只像实参一样调用方法来拿取 String (例如,你可以调用 `add(String)` 或 `set(int, String)`),在此期间如果你调用什么在 `List<T>` 中返回了 `T`,那么你得不到一个 `String`,而是一个 `Object`。 Joshua Bloch 称那些对象你只能从**生产者** **读取**,并且只能 **写入** 到 **消费者**。他推荐:“*为了弹性最大化,要在输入的参数上使用通配符来表示生产者和消费者*”,并提出下列助记词: *PECS 代表 生产者 Extends,消费者 Super* *注意*:如果你使用一个生产者对象,就是说 `List<? extends Foo>`,你不允许在这个对象上调用 `add()` 或者 `set()`,但这个意思不是说这个对象是**不变的**:例如,没有什么阻止你调用 `clear()` 从 list 移除所有的项目,由于 `clear()` 根本不需要任何参数。仅仅只通过通配符(或其它可变类型)确保**类型安全**。不变性完全是另一回事。 ### 声明场所差异 假设我们有一个泛型接口 `Source<T>`,没有任何方法而只有一个 `T` 作为参数,仅有的方法是返回 `T`: ``` java // Java interface Source<T> { T nextT(); } ``` 然后,它应该是完美地在一个类型为 `Source<Object>` 中安全地存储一个 `Source<String>` 实例引用 —— 没有消费者方法可以被调用。但 Java 并不知道这一点,而仍然会禁止它: ``` java // Java void demo(Source<String> strs) { Source<Object> objects = strs; // !!! 在 Java 中是不允许的 // ... } ``` 要修复这一点,我们不得不以无意义的方式声明类型为 `Source<? extends Object>` 的对象,因为我们可以像前面那样调用这种变量上所有相同的方法,因此没有值通过更复杂的类型被加入。但编译器并不知道。 在 Kotlin 中,有一种途径向编译器说明这类东西。称之为**声明场所差异**:我们可以注解源码的**类型参数** `T` 来确保它是只从 `Source<T>` 的成员**返回**(生产),而从不消费。对这个我们提供了 `out` 标识符: ``` kotlin abstract class Source<out T> { abstract fun nextT(): T } fun demo(strs: Source<String>) { val objects: Source<Any> = strs // 由于 T 是一个 out 参数,这样就 OK 了 // ... } ``` 通用的规则是:当一个 `C` 类的类型参数 `T` 被声明为 `out`,它会仅在 `C` 类成员的 `out` 位置存在,但返回的 `C<Base>` 可以安全地成为 `C<Derived>` 的超类。 在 clever words 中他们会说类 `C` 在参数 `T` 中是协变的,或者 `T` 是一个协变的类型参数。你可以想到 `C` 如同是 `T` 的**生产者**,并非 `T` 的**消费者**。 `out` 标识符被称作**差异注解**,并且由于它是在声明类型参数的场所提供的,我们就会说**声明场所差异**。这是相对于 Java 的在通配符在类型协变用法中的**使用场所差异**而言的。 除 `out` 之外,Kotlin 提供了一个配套的真协变注解:`in`。它使得一个类型参数**逆变**:它仅能作为消费者并从不生产。逆变的一个好例子是类 `Comparable`: ``` kotlin abstract class Comparable<in T> { abstract fun compareTo(other: T): Int } fun demo(x: Comparable<Number>) { x.compareTo(1.0) // 1.0 有 Number 的子类 Double // 因此,我们可以把 x 赋值给一个类型为 Comparable<Double> 的变量 val y: Comparable<Double> = x // OK! } ``` 我们相信单词 `in` 和 `out` 是自解释的(就像它们有时也能成功地在 C# 中使用一样),因此上面所提到的情况中并非真正需要,而是为了更高级的目的: **[The Existential](http://en.wikipedia.org/wiki/Existentialism) 变成:消费者 `in`,生产者 `out`!**:-) ## 类型投影 ### 使用场所可变:类型投影 这非常适合于声明一个类型参数 T 为 `out` 并且不会在子类使用场所中遇到麻烦。当类考虑**能**被真正地限制为只返回 `T` 时是这样,但如果不能呢?这个数组是个好例子: ``` kotlin class Array<T>(val size: Int) { fun get(index: Int): T { /* ... */ } fun set(index: Int, value: T) { /* ... */ } } ``` 这个类在 `T` 中既不能合作也不可逆变,并强制成不可变的。考虑下面的函数: ``` kotlin fun copy(from: Array<Any>, to: Array<Any>) { assert(from.size == to.size) for (i in from.indices) to[i] = from[i] } ``` 这个函数假定从一个数组复制项目到另一个。让我们尝试在实践中应用它: ``` kotlin val ints: Array<Int> = arrayOf(1, 2, 3) val any = Array<Any>(3) copy(ints, any) // 错误:要求 (Array<Any>, Array<Any>) ``` 这里我们遇到一样的熟悉的问题:`Array<T>` 的 `T` 是不可变的,因此 `Array<Int>` 和 `Array<Any>` 都不是彼此的子类型。为什么呢?再说一遍,因为复制可能会做坏事,意即它会尝试写入,就是说一个字符串给 `from`,而如果我们在那里又传递一个 `Int` 数组,那么 `ClassCastException` 会在后面某时被抛出。 此时,我们要做的事仅仅是确保 `copy()` 不会做什么坏事。我们阻止它向 `from` 写入,于是我们可以: ``` kotlin fun copy(from: Array<out Any>, to: Array<Any>) { // ... } ``` 所发生的这些事称之为**类型投影**:我们说过 `from` 不是简单的一个数组,但它是一个受限制(**投影**)的数组:我们只能调用那些返回类型参数 `T` 的方法,在这种情况下它的意思是我们只能调用 `get()`。这是我们近似于**使用场所差异**,并符合 Java 的 `Array<? extends Object>`,但更简单一点的方式。 你还可以设计一个与 `in` 一起的类型: ``` kotlin fun fill(dest: Array<in String>, value: String) { // ... } ``` `Array<in String>` 相当于 Java 的 `Array<? super String>`,意即你可以传递一个 `CharSequence` 数组或者 `Object` 数组到 `fill()` 函数。 ### 星号投影 有时你会说你对类型冲突一无所知,但仍想以安全的方式使用它。这个安全的方式在这里就是定义这种泛型的投影,泛型的每个具体的实例应该都是投影的一个子类型。 Kotlin 为这个提供了这个称之为**星号投影**的语法 - 对于 `Foo<out T>`,`T` 是一个带有上界 `TUpper` 的协变的类型参数,`Foo<*>` 等价于 `Foo<out TUpper>`。意思是当是 `T` 的时候无法你可以安全地从 `Foo<*>` *读取* `TUpper` 的值。 - 对于 `Foo<in T>`,`T` 是一个协变的类型参数,`Foo<*>` 等价于 `Foo<in Nothing>`。意思是当 `T` 未知时没有任何你可以安全**写入**到 `Foo<*>`的方式。 - 对于 `Foo<T>`,`T` 是一个带有上界 `TUpper` 的不变的类型参数,`Foo<*>`对于值读取等价于 `Foo<out TUpper>`,对于值写入等价于 `Foo<in Nothing>`。 如果一个泛型有几个类型参数则它们每一个都能独立地投影。例如,如果这个类型是声明为 `interface Function<in T, out U>`则我们可以想像下面的星号投影: - `Function<*, String>` 意思是 `Function<in Nothing, String>`; - `Function<Int, *>` 意思是 `Function<Int, out Any?>`; - `Function<*, *>` 意思是 `Function<in Nothing, out Any?>`。 注意:星号投影与 Java 的原始类型非常相似,但很安全。 # 泛型函数 并非只有类可以有类型参数。函数也可以。类型参数放置在函数名前面: ``` kotlin fun <T> singletonList(item: T): List<T> { // ... } fun <T> T.basicToString() : String { // extension function // ... } ``` 如查类型参数在调用场所被明确地传递,它们要在函数名**后面**指定: ``` kotlin val l = singletonList<Int>(1) ``` # 泛型约束 可以替代给出的类型参数的可能的类型可以通过**泛型约束**加以限制。 ## 上界 大多数限制通用约束的类型是**上界**,与 Java 的 *extends* 关键字相符: ``` kotlin fun <T : Comparable<T>> sort(list: List<T>) { // ... } ``` 在冒号后面指定的类型就是**上界**:只有 `Comparable<T>` 的子类型可以代替 `T`。例如 ``` kotlin sort(listOf(1, 2, 3)) // OK。Int 是 Comparable<Int> 的子类型 sort(listOf(HashMap<Int, String>())) // 错误:HashMap<Int, String> 不是 Comparable<HashMap<Int, String>> 的子类型 ``` 默认的上界(如果未指定)是 `Any?`。尖括号里面只能指定一个上界。如果相同的类型参数需要多于一个上界,我们需要一个独立的 **where** 子句: ``` kotlin fun <T> cloneWhenGreater(list: List<T>, threshold: T): List<T> where T : Comparable, T : Cloneable { return list.filter { it > threshold }.map { it.clone() } } ```