Kotlin编程实战:创建优雅、富于表现力和高性能的JVM与Android应用程序
上QQ阅读APP看书,第一时间看更新

6.5.2 使用协变

Kotlin防止我们传递的是Array<Banana>,而预期的是Array<Fruit>,从而防止我们无意中将一些任意的水果添加到Banana数组中。这很好,但有时我们想告诉Kotlin稍微放宽一些规则,当然,在不损害类型安全的情况下。换句话说,我们希望Kotlin编译器允许协变——接受派生类型的泛型,而预期的是基类型的泛型。这就是类型预测的用武之地。

让我们创建一个例子,其中Kotlin将阻止我们的方法,然后寻找办法来取得进展,但不会降低类型安全性。

在编写本章其余部分中的示例时,在需要使用Fruit、Banana和Orange类的任何地方,请将它们与我们在上一节创建的示例一起使用。下面是一个copyFromTo()函数,它使用了两个Fruit数组:

copyFromTo()函数遍历from参数中的对象,并将它们复制到to数组中。它假设两个数组的大小相等,因为这个细节与我们感兴趣的内容无关。现在,让我们创建两个Fruit数组,并将其中一个数组的内容复制到另一个数组中:

copyFromTo()方法期望两个Array<Fruit>,而我们传递的正是这些类型。没有问题。

现在,让我们修改传递给copyFromTo()函数的参数:

Kotlin阻止我们传递Array<Banana>,而期望的是Array<Fruit>,因为它担心copyFromTo()方法可能会将一个不是香蕉的水果添加到Array<Banana>中,这是不行的,正如我们前面所讨论的,Array<T>的类型是不变的。

我们可以告诉Kotlin,我们只打算读取传递给from参数的数组,没有传递任何Array<T>的风险,其中T是Fruit类型或Fruit的派生类。此意图称为参数类型的协变——来接受类型本身或任何其派生类型。

语法from: Array<out Fruit>用于传递Fruit参数类型的协变。Kotlin将断言没有对允许传入数据的from引用进行任何方法调用。Kotlin将通过检查被调用方法的签名来确定这一点。

让我们通过使用协变参数类型来修复代码:

Kotlin现在将验证在copyFromTo()函数中,不会对协变的参数调用发送参数类型为Fruit的实参。换句话说,如果以下两个调用出现在copyFromTo()的循环中,则编译将失败:

通过只有从from参数中读取的代码以及设置为to参数的代码,我们可以轻松地传递Array<Banana>、Array<Orange>或Array<Fruit>,其中Array<Fruit>是from参数所期望的:

Array<T>类具有读取和设置类型为T的对象的方法。任何使用Array<T>的函数都可以调用这两种方法中的任何一种。但要使用协变,我们向Kotlin编译器保证,不会调用任何方法,来对Array<T>发送具有给定参数类型的任何值。这种在使用泛型类时使用协变的行为称为“使用点型变”或“类型预测”。

“使用点型变”对于泛型类的用户传递协变的意图很有用。但是,在更广泛的层面上,泛型类的作者可以为该类的所有用户制定协变的意图,即任何用户只能读取泛型类,而不能写入泛型类。在泛型类型的声明中而不是在使用时指定协变,称为“声明点型变”。“声明点型变”的一个很好的例子可以在列表接口的声明中找到,它被声明为List<out T>。“声明点型变”的使用允许将List<Banana>传递给receiveFruits(),而不允许传递Array<Banana>。

换句话说,List<out T>根据定义向Kotlin保证receiveFruits()或任何与此相关的方法都不会写入类型List<T>的参数。另一方面,Array<out T>向Kotlin保证该协变参数的接收方不会写入该参数。“声明点型变”实现了“使用点型变”通常在战术上只为其所应用的参数实现的效果。

使用协变,可以告诉编译器接受派生的参数类型来代替参数类型。你也可以要求编译器接受基类型。这就是逆变,我们接下来将对此进行探讨。