Kotlin:泛型杂谈(下)

作者: 泪已无痕 | 来源:发表于2018-07-22 10:38 被阅读32次

Kotlin:泛型杂谈(上)中,从泛型扩展属性、非空约束、实例化类型参数三个方面简单介绍了一下Kotlin中泛型相对于Java独有的特性,接下来将继续介绍Kotlin中泛型相关的一些知识点。

  1. 类型和子类型
    定义:如果程序需要的是类型A的值,能够使用类型B的值来当作类型A的值,那么类型B就是类型A的子类型,类型A就是类型B的超类型。
    初次接触这个概念的时候以为就是类与子类的别名,那是否真的如此呢?让我们先看一个例子:
var name: String?

在上面的例子中,我们其实使用String类构造了2种类型:非空类型和可空类型;在此例中我们可以知道:非空类型name是可空类型name的子类型。
上述可知,类型与子类型并非完全等价于类与子类的关系。这个概念有什么作用呢?让我们继续往下看。

  1. 协变
open class Work {

    open fun doSomething() {
        println("doSomething")
    }
}

class Cook: Work() {

    override fun doSomething() {
        println("cook")
    }
}

fun doSomething(works: Array<Work>) {
    works.forEach {
        it.doSomething()
    }
}

fun main(args: Array<String>) {
    val works: Array<Cook> = arrayOf(Cook())
    doSomething(works)
}

在上例中,将Array<Cook>类型的参数传递给了doSomething函数,但该函数期望的是Array<Work>,由于类型不匹配导致了编译错误,那如果想要正确运行又该如何处理呢?

fun main(args: Array<String>) {
    val works: Array<Cook> = arrayOf(Cook())
    doSomething(works as Array<Work>)
}

上面我们通过显式类型转换达到了目的,但是这样做存在两个问题:

  • 代码啰嗦。
  • 无法在编译期间杜绝错误转换问题,比如:
arrayOf("Error") as Array<Work>

这里我们试图将Array<String>转换成Array<Work>,编译期间只会给出

Unchecked cast: Array<String> to Array<Work>

的警告,只有在运行时才会抛出异常。那是否有更好的方法处理此类问题呢?下面让我们一起了解一下Kotlin的协变:
定义:可以使用子类型替换需要父类型的位置,即当参数或返回值需要超类型时传入子类型参数或返回子类型的值。
使用:使用out关键字来修饰泛型类型,从而将泛型类定义为协变。

根据上面的描述,我们可以将上述例子稍作修改,从而解决上面显示类型转换所带来的问题:

fun doSomething(works: Array<out Work>) {
    works.forEach {
        it.doSomething()
    }
}

fun main(args: Array<String>) {
    val works: Array<Cook> = arrayOf(Cook())
    doSomething(works)
}

非常简单对不对?下面思考一个问题,是否可以把任何类型都声明为协变?要回答这个问题,让我们先看一下Kotlin中的两个关键字outin

如果一个类声明了一个类型参数T并有一个使用T的方法,如果方法将T作为返回类型,那么T就在out位,如果将T作为参数类型,那么T就在in位置,比如:

interface A<T> {
     fun getT(): T
     fun setT(t: T)
}

上面例子中,T在getT中位于out位置,在setT中位于in位置。

通过对outin的解释,我们来回答是否可以把任何类型都声明为协变?这个问题:即声明为协变的类型,只能用在上面所说的out位置以保证类型安全。

  1. 逆变
    上面我们对协变进行了详细的介绍,那么逆变又是什么东西呢?看下面的例子:
fun main(args: Array<String>) {
    val anyComparator = Comparator<Any> {
        e1, e2 -> e1.hashCode() - e2.hashCode()
    }
    val names: List<String> = listOf("Tom", "Joy")
    println(names.sortedWith(anyComparator))
}

上面例子中,我们声明了一个Comparator<Any>比较器,但sortedWith却希望一个Comparator<String>比较器,根据上面类型与子类型的描述,这里Comparator<Any>成了Comparator<String>的子类型,与协变(Comparator<String>Comparator<Any>的子类型)相比,它的子类型化关系发生了逆转,这就是所谓的逆变
让我们看一下sortedWith的定义:

public fun <T> Iterable<T>.sortedWith(comparator: Comparator<in T>): List<T> {
   //doSomething...
}

从定义中可知,只需要使用in关键字来修饰泛型类型,就可以将泛型类定义为逆变;并且只能用在上面所说的in位置。

上面我们对协变逆变进行了介绍,这两个特性的主要目的是提高程序安全性,将一些运行时错误(比如上面的类型转换)杜绝在编译期;但也有一些我们需要注意的细节:

  • 只读属性,类型参数用在了out位置,可变属性用在了out位置和in位置。
  • 位置规则只覆盖了类外部可见(public、protected、internal)API,私有方法的参数既不在in位置也不在out位置;这是因为协变逆变规则设计的目的是防止外部使用者对类的误用,因此不会对类自己的实现起作用。
  • 构造方法的参数既不在in位置,也不在out位置;即使类型参数声明为协变,仍然可以在构造方法参数的声明中使用它;这是因为协变逆变规则设计的目的是防止外部使用者对类的误用,但是类的构造方法属于类实例创建后无法再次调用的方法,所以不存在误用的风险。
  1. 星号投影
    有些时候我们不知道或不关心泛型类型实参,这个时候我们可以使用星号投影;它的语法很简单,比如List<*>,由于星号投影并不知道实际的类型实参,因此该特性只能调用类中生产值的方法,且不需要关心值的类型。

好了,上面我们从类型和子类型、协变、逆变、星号投影对Kotlin中泛型剩下的特性进行了介绍,如有错误疏漏之处,还希望能与大家一起探讨。^ _ ^

相关文章

  • Kotlin:泛型杂谈(下)

    在Kotlin:泛型杂谈(上)中,从泛型扩展属性、非空约束、实例化类型参数三个方面简单介绍了一下Kotlin中泛型...

  • Kotlin:泛型杂谈(上)

    Kotlin泛型抛弃语法上的差异,在一般情况下与Java都是相近的,所以在这里简单介绍一下Kotlin泛型中独有的...

  • 泛型

    与Java泛型相同,Kotlin同样提供了泛型支持。对于简单的泛型类、泛型函数的定义,Kotlin 与 Java ...

  • Kotlin---泛型

    Kotlin不变型泛型 Kotlin的不变型泛型和Java一样,通过声明泛型类型来使用泛型类。而该种泛型声明后,则...

  • Kotlin 泛型 VS Java 泛型

    建议先阅读我的上一篇文章 -- Java 泛型 和 Java 泛型一样,Kotlin 泛型也是 Kotlin 语言...

  • Kotlin for android学习六:泛型

    前言 kotlin官网和kotlin教程学习教程的笔记。 1. 声明泛型 2. 泛型约束 : 对泛型的类型上限进行...

  • 泛型

    Kotlin 泛型详解 声明一个泛型类 声明一个泛型方法 泛型约束 List 和 List 是...

  • Kotlin 泛型

    Kotlin 支持泛型, 语法和 Java 类似。例如,泛型类: 泛型函数: 类型变异 Java 的泛型中,最难理...

  • 【Android】 Kotlin(七)泛型

    深入理解Kotlin泛型 Kotlin 的泛型与 Java 一样,都是一种语法糖,即只在源代码中有泛型定义,到了c...

  • Kotlin 泛型

    说起 kotlin 的泛型,就离不开 java 的泛型,首先来看下 java 的泛型,当然比较熟悉 java 泛型...

网友评论

    本文标题:Kotlin:泛型杂谈(下)

    本文链接:https://www.haomeiwen.com/subject/fozxmftx.html