Kotlin之泛型(一)

简述:

对于大多数有经验的Android开发者来说,对于Java的泛型使用都有一定经验或者经常接触,而Kotlin的泛型形式上跟Java很相像,但实际上又是非常的大不同。首先,语法上Kotlin上会比Java使用更安全是毫无疑问的;其次,Kotlin泛型出现了一堆相对陌生的名词,类似泛型形参、泛型实参、协变、逆变、不变、星投影等。

一、泛型的概念及它的形参和实参

泛型的概念:

泛型是一种类型层面的抽象,其通过泛型参数实现构造更加通用的类型能力,并且可以让符合继承关系的类型批量实现某些能力。

泛型形参

泛型的基本声明形式:

函数声明泛型:fun <T> f(a:T,b:T): T

类声明泛型:class Generic<T>(var e:T)

T就是代表泛型形参,它具有一种或多种类型的类。

泛型实参

下面我们定义一个泛型声明的类:

class Generic<T>(var e:T){

    fun printElement(){
        println("$e")
    }
}
复制代码

我们定义了一个简单的含有泛型声明的类,只需调用它的构造器传入正常的数据就可以获取实例并使用:

fun main(){
    val generic = Generic<String>("Generic")
    generic.printElement()
}

打印结果:
Generic
复制代码

这里我们使用一个字符串调用构造方法构造Generic<T>的实例,那么对应这个泛型形参T的就是String类型,那么这个String类型就是它的泛型实参。实际上,idea会提示我们不必写成这样:val generic = Generic<String>("Generic"),而是会提示Remove explicit type arguments,这是因为Kotlin有智能的类型推导,当泛型参数可以推断出来时,我们就可以简写成:

val generic = Generic("Generic")
复制代码

总的来说,泛型形参就是定义的时候可以代表一种或多种类型的类型,而泛型实参就是我们实际构造实例时类型参数被替换成泛型实参的具体类型。

二、泛型约束

当我们声明一个泛型时,Kotlin是允许我们给这个泛型类型增加约束条件,即泛型约束限制给定类型参数的所有可能类型的集合。那这么做又什么用呢?我们通过一个比较大小的泛型函数来说明。

我们想定义一个可以比较任意类型大小的maxOf()泛型函数:

fun <T> maxOf(a:T,b:T): T {
    return if(a > b) a else b
}
复制代码

这个时候我们的idea会提示a > b报错,无法比较大小。那这个时候我们只要给这个泛型参数T加一个约束就可以了。

fun <T:Comparable<T>> maxOf(a:T,b:T): T {
    return if(a > b) a else b
}
复制代码

这个时候只要传进来的实参类型是Comparable的子类就可以进行比较了,这个就是泛型的类型约束,也是上界约束。上界约束就是现在我们已经指定了这个TComparable<T>子类,调用代码只能传递Comparable<T>的子类型,这个Comparable<T>就是上界约束。

默认的上界(如果没有声明)是 Any?。在尖括号中只能指定一个上界。 如果同一类型参数需要多个上界(即多个约束),我们需要一个单独的 where-子句:

fun <T> callMax(a: T, b: T)
        where T : Comparable<T>, T : () -> Unit {
    if (a > b) a() else b()
}
复制代码

这里约束了T不但要实现Comparable接口,它还要是一个函数,最后调用a()就是调用了对象的inkove方法。

多个泛型参数的约束

观察下面代码:

fun <T, R> callMax(a: T, b: T): R
        where T : Comparable<T>, T : () -> R, R : Number {
    return if (a > b) a() else b()
}
复制代码

这里定义了T、R两个泛型参数,约定T实现Comparable接口,并是一个返回R类型的无参函数,最后约束R是Number类型。

三、泛型的型变

泛型的型变指的是泛型实参的继承关系对泛型类型的继承关系的影响,或者说是反应泛型中某一类型的对应关系规则。

3.1、类、类型、子类、子类型、超类型以及子类型化关系的概念

Kotlin中的类分为两种:非泛类型泛类型

非泛类型就是我们在开发中定义的一般类,例如声明一个类:class Person(val age:Int),Person类定义一个变量val mPerson:Person,那么mPerson这个变量的类型就是这个类Person,mPerson和Person的类型是一致的。这里我们要注意的是Kotlin中还存在可空类型,如果这样定义:val mPerson:Person?,这里Person类和变量mPerosn的类型Person?就不一样了。所以这里可以看出一个类至少对应着两个类型,并且所有类的非空类型都是该类对应的可空类型的子类型,反之则不成立

泛类型是一种可以对应无限种类型的类型。这是因为我们再定义泛型类的时只会定义泛型形参,当我们要获取一个合法的泛型类型实例需要调用使用的方法传入类型实参替换掉类型形参,那这个时候就可以有很多种定义的实参了,例如:List<String>List<Int>List<Person>

子类、子类型、超类型:

一般来说子类是一个派生类,它会继承它的父类(或是叫基类),例如class B: A(),B就是A的子类。那么子类型和超类型的定义的规则是: 任何时候如果需要的是A类型值的任何地方,都可以使用B类型的值来替换的,那么就可以说B类型是A类型的子类型或者称A类型是B类型的超类型。再举个例子:如String和String?,如果一个函数接收的是String?,我们传入的是String的话,编译器是不会报错的,但是如果一个函数接受的是String,我们传入的是String?的话,编译器就会提示我们可能会存在空指针的问题,所以String就是**String?**的子类型,String?就是String的超类型。

子类型化关系:

如果需要A类型的地方,都可以用B类型来代替,那么B类型就是A类型的子类型,B类型到A类型之间的映射关系就是子类型化关系。例如List<String>类型的函数实参可以传递给List<Any>类型的函数形参,所以List<String>类型是List<Any>类型的子类型。

3.2、不型变、协变、逆变

不型变就是泛型参数不被声明为out,也不被声明为in的情况。

因为MutableList<E>是可读写操作的,其是不型变,所以即使StringAny的子类型,StringString? 的子类型,但是MutableList<String>MutableList<Any>之间没有任何继承关系,MutableList<String>MutableList<String?>之间也没有任何关系,所以不型变的基本特点是:<T>既可以添加,也可以读取。

协变

协变是一种保留子类型化关系,并保证泛型内部操作该类型时是只读的。要声明在某个类型参数是可以协变的,使用out修饰即可。

interface Producer<out T> {
    fun produce(): T
}
复制代码

泛型协变主要考察的是泛型参数作为生产者的情况,作为泛型参数类型实例输出者提供的类型是要一致的,子类提供子类,父类提供父类,可以提供子类就可以提供父类,下面我们一段代码解释:

//普通的书
interface Book
//指定书籍
interface EduBook : Book

//定义一个书店 类型是至少是书
class BookStore<out T:Book>{ //接受一个协变的泛型参数,约束为Book的子类
    fun getBook() : T{ //协变点T
        TODO()
    }
}

fun main() {

    val eduBookStore:BookStore<EduBook> = BookStore<EduBook>()
    val bookStore:BookStore<Book> = eduBookStore 

    val book:Book = bookStore.getBook()
    //val book:Book = eduBookStore.getBook() 可以提供一本书,子类提供给父类
    val eduBook:EduBook = eduBookStore.getBook() //子类提供子类的情况
    
    val eduBook:EduBook = bookStore.getBook() //报错,因为bookStore提供的是父类,不一定能提供指定的EduBook
}

复制代码

在上面案例我们也可以看到,T被称为协变点,所以协变类中函数的返回类型或者是只读属性的类型就称之为协变点,它作为一个生产者角色,只有可读属性,向外提供类型输出。

逆变

逆变相对协变就是反过来的概念,所以逆变就是一种反转子类型化关系,并保证泛型内部操作该类型时是只写操作。要声明在某个类型参数是可以逆变的,使用in关键字修饰即可。

前面我们提到协变类是一个生产者的角色,那逆变类就是一个消费者角色了。如果B是A的子类型,在逆变类中,那么Consumer<A>就是Consumer<B>的子类型,即A和B的关系位置互换了,也就是反转了子类型化关系。下面我们用
Comparable接口来举例说明一下

public interface Comparable<in T> {
    public operator fun compareTo(other: T): Int
}
复制代码

我们用Number类型和Int类型比较一下,Int类型是Number类型子类,但是很明显Comparable<Number>一定是可以比较Int类型的,但Comparable<Int>类型不一定能比较Number类型,这个时候子类Int不能替代父类Numer,反而是
Comparable<Number>可以替代Comparable<Int>,所以子类型化关系反转了…

逆变点

逆变点就是函数参数为泛型参数

interface Consumer<in T> {
    
    fun consume(value: T) // T作为函数形参类型

    fun consumeList(list: List<T>) //这里T作为形参List泛型中的实参
}
复制代码

下面用一个垃圾分类的案例探究一下逆变点的意义:

生活中,假如我们会把垃圾分成:垃圾干垃圾,那垃圾就包含了干垃圾,干垃圾就是垃圾的子类;我们又把垃圾桶分为:垃圾桶干垃圾桶,那干垃圾桶就是垃圾桶的子类。通常,我们可以把干垃圾扔到干垃圾桶或者垃圾桶,但垃圾只能扔到垃圾桶,不一定能扔到干垃圾桶。下面我们划分一下对应关系:

垃圾 —-> 垃圾桶

干垃圾 —-> 干垃圾桶

干垃圾 —-> 垃圾桶

所以在扔干垃圾上就是它一个父类了,因为它可以代替垃圾扔到任何一种垃圾桶里面,存在反转子类型化关系。

//定义垃圾的类
open class Waste
//干垃圾
class DryWaste:Waste()

//定义一个垃圾桶接受垃圾 
class Dustbin<in T:Waste>{ //约束泛型必须是Waste及其子类
    fun put(t:T){ //逆变点 接受T类型参数

    }
}

fun main() {
    
    //垃圾桶
    val dustbin:Dustbin<Waste> = Dustbin<Waste>()
    //干垃圾桶
    val dryDustbin:Dustbin<DryWaste> = Dustbin<DryWaste>()

    val waste = Waste()
    val dryWaste = DryWaste()
    
    //垃圾桶可以把垃圾和干垃圾都放进去
    dustbin.put(waste)
    dustbin.put(dryWaste)

    //干垃圾桶只能放干垃圾
    dryDustbin.put(dryWaste)
    dryDustbin.put(waste) // 报错,cast expression "waste" to DryWate
}
复制代码

小结:Kotlin泛型的作用很重要,尤其是我们开发封装一些基础类库或者sdk,其重要性不言而喻,所以我们还得多多深入了解。Kotlin泛型型变概念性的东西在理解上确实比较抽象,小弟也是看了好几遍一点点的理解,即使到现在碰到一些复杂的情况依旧第一时间懵逼,但理解了概念我们可以慢慢分解。这里要感谢一下bennyHuo大神的视频教学,以及网上无私贡献的博客。接下来,继续复习Kotlin泛型中的星投影、泛型实现原理与内联特化,并在案例中介绍实际开发场景如何应用Kotlin泛型。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享