原创

Kotlin型变

Java的泛型不支持型变,Java采用通配符来解决这个问题;Kotlin采用安全的型变代替了Java的通配符。

一、泛型型变的需要

Java泛型的通配符语法默认是有上限的,只不过其上限是Object类。

泛型存在如下规律:

  • 通配符上限(泛型协变)意味着从中取出(out)对象是安全的,但传入对象(in)则不可靠。
  • 通配符下限(泛型逆变)意味着向其中传入(in)对象是安全的,但取出对象(out)则不可靠。

Kotlin利用上面两个规律,抛弃了泛型通配符语法,而是利用in、out来让泛型支持型变。

二、声明处型变

Kotlin处理泛型型变的规则如下:

  • 如果泛型只需要出现在方法的返回值声明中(不出现在形参声明中),那么该方法就只是取出泛型对象,因此该方法就支持泛型协变(相当于通配符上限);如果一个类的所有方法都支持泛型协变,那么该类的泛型参数可使用out修饰。
  • 如果泛型只需要出现在方法的形参声明中(不出现在返回值声明中),那么该方法就只是传入泛型对象,因此该方法就支持泛型逆变(相当于通配符下限);如果一个类的所有方法都支持泛型逆变,那么该类的泛型参数可使用in修饰。

下例中定义一个支持泛型协变的类。

package test0709

class User<out T> {
    //此处不能用var,否则就有setter方法
    //setter方法会导致T出现在方法形参中
    val info: T

    constructor(info: T) {
        this.info = info
    }

    fun test(): T {
        println("执行test方法")
        return info
    }
}

fun main(args: Array<String>) {
    //此时T的类型是String
    var user = User<String>("Kotlin")
    println(user.info)
    //对于u2而言,它的类型是User<Any>,此时T的类型是Any
    //由于程序声明了T支持协变,因此User<String>可当成User<Any>使用
    var u2: User<Any> = user
    println(u2.info)
}

输出结果:

Kotlin
Kotlin

下例中定义一个支持泛型逆变的类。

package test0709

class Item<in T> {
    fun foo(t: T) {
        println(t)
    }
}

fun main(args: Array<String>) {
    //此时T的类型是Any
    var item = Item<Any>()
    item.foo(20)
    var im2:Item<String> = item
    //im2的实际类型是Item<Any>,因此它的参数只要是Any即可
    //声明了im2的类型为Item<String>
    //因此传入的参数只可能是String
    im2.foo("Kotlin")
}

输出结果:

20
Kotlin
  • 如果泛型T只出现在该类的方法的返回值声明中,那么该类泛型形参即可使用out修饰T。
  • 如果泛型T只出现在该类的方法的形参声明中,那么该泛型形参即可使用in修饰T。

上面的类都是在声明时使用out或in指定泛型支持型变的,这种方式被称为“声明处型变”。

三、使用处型变:类型投影

如果不能使用声明处型变,则可以使用Kotlin提供的“使用处型变”。所谓使用处型变,就是在使用泛型时对其使用out或in修饰。

下例为使用处协变:

package test0709

fun copy(from: Array<out Any>, to: Array<Any>) {
    val len = if (from.size < to.size) from.size else to.size
    for (i in 0 until len) {
        to[i] = from[i]
    }
}

fun main(args: Array<String>) {
    var arr1 = arrayOf(2, 3, 5, 7)
    var arr2: Array<Any> = arrayOf(4, 12, 15, 19, 100)
    copy(arr1, arr2)
    println(arr2.contentToString())
}

输出结果:

[2, 3, 5, 7, 100]

下例为使用处逆变:

package test0709

fun fill(dest: Array<in String>, value: String) {
    if (dest.size > 0) {
        dest[0] = value
    }
}

fun main(args: Array<String>) {
    var arr1: Array<CharSequence> = arrayOf("a", "b", StringBuilder("cc"))
    fill(arr1, "Kotlin")
    println(arr1.contentToString())

    var intArr: Array<in Int> = arrayOf(2, 5, 39)
    println(intArr.contentToString())
    intArr.set(0, 34)

    var numArr: Array<Number> = arrayOf(3, 4.1, 10.4)
    intArr = numArr
    println(intArr.contentToString())
}

输出结果:

[Kotlin, b, cc]
[2, 5, 39]
[3, 4.1, 10.4]

Array相当于Java的泛型下限:Array<? super Int>,因此这种泛型型变用于支持逆变,还可以保证向其中安全地添加元素。

四、星号投影

星号投影是为了处理Java的原始类型。

fun main(args: Array<String>) {
    //<*>必不可少,相当于Java的原始类型
    var list: ArrayList<*> = arrayListOf(1, "Kotlin")
    println(list)
}

输出结果:

[1, Kotlin]

如果泛型类型具有多个类型参数,那么每个类型参数都可以单独指星号投影。

假如声明了支持两个泛型参数的Foo<in T,out U>类型,关于星号投影的解释如下:

  • 对于Foo<*,String>,相当于Foo<in Nothing,String>
  • 对于Foo<Int,*>,相当于Foo<Int,out Any?>
  • 对于Foo<,>,相当于Foo<in Nothing,out Any?>

学海无涯苦作舟

我的微信公众号.jpg

基本语法
  • 作者:HunterArley (联系作者)
  • 发表时间:2019-12-02 13:30
  • 版权声明:本网站部分内容转载于合作站点或其他站点,但都会注明作/译者和原出处。如有不妥之处,敬请指出。
  • 公众号转载:请在文末添加作者公众号二维码