Kotlin泛型

与 Java 类似,Kotlin 中的类也有类型参数:

class Box<T>(t: T) {
    var value = t
}

一般来说,要创建这样类的实例,我们需要提供类型参数:

val box: Box<Int> = Box<Int>(1)

但是如果类型参数可以推断出来,例如从构造函数的参数或者从其他途径,允许省略类型参数:

val box = Box(1) // 1 具有类型 Int,所以编译器知道我们说的是 Box<Int>。

什么是型变

java 中的泛型是不型变的,List<String> 并不是 List<Object> 的子类型。为了保证运行安全,才这样设计,可以看看下面的代码

List<String> strs = new ArrayList<String>();
List<Object> objs = strs;
objs.add(1);
String s = strs.get(0);//在这里我们将面临 ClassCastException:无法将整数转换为字符串

但这样我们又会遇到这样的问题。例如,考虑 Collection 接口中的 addAll() 方法。

interface Collection<E> …… {
  void addAll(Collection<E> items);
}

但之后我们无法做到以下这个事情

void copyAll(Collection<Object> to, Collection<String> from) {
  to.addAll(from);
  // !!!对于这种简单声明的 addAll 将不能编译:
  // Collection<String> 不是 Collection<Object> 的子类型
}

这就是为什么 addAll() 的实际签名是以下这样:

interface Collection<E> …… {
  void addAll(Collection<? extends E> items);
}

熟悉 Java 肯定知道, Java 中在处理型变时使用的是通配符类型参数 extendsuper ,通配符的出现是为了保证类型安全。

Kotlin相关

声明处型变

Kotlin 中为什么不直接使用 Java 的通配符方式,而使用声明处型变的方式,我们可以看看下面这个例子:

定义一个泛型接口:

interface Source<T> {
  T nextT();
}

那么,在 Source<Object> 类型的变量中存储 Source<String> 实例的引用是极为安全的——没有消费者-方法可以调用。但是 Java 并不知道这一点,并且仍然禁止这样操作:

// Java
void demo(Source<String> strs) {
  Source<Object> objects = strs; // !!!在 Java 中不允许
  // ……
}

为了修正这一点,我们必须声明对象的类型为 Source<? extends Object>,这是毫无意义的,因为我们可以像以前一样在该对象上调用所有相同的方法,所以更复杂的类型并没有带来价值。但编译器并不知道。

out (协变)

所以在 Kotlin 中,有一种方法向编译器解释这种情况。这称为声明处型变:我们可以标注 Source类型参数 Tout 来确保它仅从 Source<T> 成员中返回,并从不被消费。

interface Source<out T> {
    fun nextT(): T //T 只出现在返回位置
}

fun demo(strs: Source<String>) {
    val objects: Source<Any> = strs // 这个没问题,因为 T 是一个 out-参数
}

当一个类型参数 T 被声明为 out 时,它就只能出现在输出位置就是只能在函数的返回位置,作为回报 C<Base> 可以安全地作为 C<Derived>的超类进行转换。

由于被声明为 out 的 T 只存在于返回位置,所以在使用时如果我们定义Source<String>则返回的是 String ,定义的是Source<Any> 返回的则是 Any。这时我们把 Source<String>对象赋值给Source<Any>,则Source<Any>中真正意义上存放的是String,但函数返回的是 Any ,现在想想实际存的 String 是不是拥有所有 Any 的属性和方法,获取到返回的 Any 在使用过程中是没有任何问题的。

所以当一个类型参数 T 被声明为 out 时,只能出现在输出位置。他就有 C<Base> 可以安全地作为 C<Derived>的超类的能力。

in (逆变)

in 使得一个参数 T :只可以被消费而不可以被生产。逆变类型的一个很好的例子是 Comparable

interface Comparable<in T> {
    operator fun compareTo(other: T): Int
}

fun demo(x: Comparable<Number>) {
    x.compareTo(1.0) // 1.0 拥有类型 Double,它是 Number 的子类型
    // 因此,我们可以将 x 赋给类型为 Comparable <Double> 的变量
    val y: Comparable<Double> = x // OK!
}

in 使得一个参数 T 只能存在于消费位置,即方法中参数的类型。所以我们定义 Comparable<Number> 在使用这个类型参数时我们只能获取到 NumberComparable<Double> 在使用这个类型参数时我们只能获取到 Double,我们都知道 NumberDouble 父类,DoubleNumber 没有的属性和方法。在使用中我们是不是可以可以说 Double 拥有 Number 的所有能力。所以在传入 Number 参数的地方我们传个 Double 肯定是没有问题的。

所以我们可以用 Comparable<Double> 来接受一个 Comparable<Number>,因为在使用的时候我们当然是方法和参数更多的子类受用范围大。

类型投影

类型投影:使用处型变,在使用的地方使用 out/in 声明

看个例子:

class Array<T>(val size: Int){
    fun get(index: Int): T{ /*...*/}
    fun set(index: Int , value: T){/*...*/}
}

可以看出 Array 既不是协变的,也不是逆变的。如果他定义了如下的函数:

fun copy(from: Array<Any> , to: Array<Any>){
    assert(from.size == to.size)
    for(i in from.indices)
        to[i] = from[i]
}

在编辑如下代码是时会提示错误:

fun main() {
    val ints: Array<Int> = arrayOf(1, 2, 3)
    val any = Array<Any>(3) { "" }
    copy(ints, any)//编辑器报错,Type mismatch Required:Array<Any> Found: Array<Int>
}

为什么编辑器报错,我们假设上面的 main 方法是可以执行的,这个时候如果由于编写错误,我们在 copy 函数中调用了 set 方法,这将导致结果错误,明明是 copy 但却得到一个全是 0 的数组。为了阻止 copy 做坏事,我们直接声明使用处型变,约束 from 只能调用返回函数。如下定义了 out 再去调用 from[i] = 0 编辑器提示错误。

image-20190306104501086.png

使用时的例子

fun copy(from: Array<out Any>, to: Array<Any>) {
    assert(from.size == to.size)
    for (i in from.indices)
        to[i] = from[i]

    println(to)
}

fun fill(dest: Array<in Number>, value: Number) {

}

fun main() {
    val ints: Array<Int> = arrayOf(1, 2, 3)
    val anys: Array<Any> = arrayOf("", "", "")

    copy(ints, anys)
    fill(anys, 1.0)
}

使用 copy 函数 Array<out Any> 是协变的相当于用一个 Array<Any> 来接收 Array<Int>

使用 fill 函数 Array<in Number> 是逆变的相当于用一个 Array<Number> 来接受 Array<Any>

星投影

有时你想说,你对类型参数一无所知,但仍然希望以安全的方式使用它。 这里的安全方式是定义泛型类型的这种投影,该泛型类型的每个具体实例化将是该投影的子类型。

Kotlin 为此提供了所谓的星投影语法:

  • 对于 Foo <out T : TUpper>,其中 T 是一个具有上界 TUpper 的协变类型参数,Foo <*> 等价于 Foo <out TUpper>。 这意味着当 T 未知时,你可以安全地从 Foo <*> 读取 TUpper 的值。
  • 对于 Foo <in T>,其中 T 是一个逆变类型参数,Foo <*> 等价于 Foo <in Nothing>。 这意味着当 T 未知时,没有什么可以以安全的方式写入 Foo <*>
  • 对于 Foo <T : TUpper>,其中 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 的原始类型,但是安全。

@UnsafeVariance

当我们的使用违反了 inout 的使用,但是我们自己是知道这样做事没有问题的,我们就可以使用 @UnsafeVariance 告诉编辑器,这个你不用管了。例子如下:

public interface List<out E> : Collection<E> {
    ...
    public operator fun get(index: Int): E
    public fun indexOf(element: @UnsafeVariance E): Int
    ...
}

这是官方的 List 类 ,泛型是 <out E> 但是在 indexOf 中作为入参使用了,但我们知道这完全不会对我们的 list造成任何的影响,我们就可以用 @UnsafeVariance 让编辑器不要管了。

参考

[Kotlin 官网_泛型相关

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容