Kotlin中关于泛型型变的那些事

/   今日科技快讯   /

华为荣耀将在下周面向欧洲市场发布搭载HMS服务(华为移动服务)的荣耀V30系列手机,这也将是华为旗下首款预装HMS生态的智能手机。荣耀V30去年年底已在国内市场上市,这是荣耀首款5G智能手机。那么问题来了,不能使用Google移动服务的手机在欧洲会不会有市场呢?我们拭目以待。

/   作者简介   /

本篇文章来自谭嘉俊的投稿,分享了他对Kotlin开发中泛型型变的理解,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。

谭嘉俊的博客地址:

https://juejin.im/user/593f7b33fe88c2006a37eb9b

/   开始   /

本文章讲解的内容是泛型的型变,我写一个扩展Boolean的示例代码来应用我要讲的内容,示例代码如下:

https://github.com/TanJiaJunBeyond/BooleanExtensionDemo

先看下以下例子,代码如下:

List strings = new ArrayList();
// Java中禁止这样的操作
List objects = strings;

 
   

在Java中是禁止这样的操作的,我们看下Kotlin的写法,代码如下:

val strings: List = arrayListOf()
val anys: List = strings

在Kotlin中是允许这样的操作的,这是为什么呢?下面会详细解释。

在List中,List是基础类型,String是类型实参,现有两个List集合,分别是List和List,它们都具有相同的基础类型,但是类型实参不相同,并且String和Any存在父子关系,型变就是指List和List这两者存在什么关系。

/   参数和实际参数   /

函数中的形参和实参

代码如下:

fun add(firstNumber: Int, secondNumber: Int): Int =
    firstNumber + secondNumber

firstNumber和secondNumber就是形式参数,然后去调用这个函数,代码如下:

val first = 1
val second = 2
add(first, second)

first和second就是add函数的实际参数。

泛型中的形参和实参

代码如下:

class Fruit(var item: T)

T就是类型形参,然后使用这个类,代码如下:

val fruit = Fruit(100)

Int就是Fruit的类型实参,因为Kotlin具有类型推导特性,不必明确指明类型,所以其实可以写成如下代码:

val fruit = Fruit(100)

在这种情况下,Int依然是Fruit的类型实参。还有以下情况,请看代码:

// Collections.kt
public interface MutableList : List, MutableCollection {
    // 省略部分代码
}

这里的E是List和MutableCollection的类型实参,同时是MutableList的类型形参。

结论

定义在里面就是形式参数,定义在外面就是实际参数。

/   子类、超类、子类型、超类型   /

子类会继承超类,例如class Apple: Fruit(),Apple就是Fruit的子类,Fruit就是Apple的超类,那什么是子类型和超类型呢?它们的规则比子类和超类更加宽松,如果需要A类型的地方,都可以用B类型来代替,那么B类型就是A类型的子类型,A类型就是B类型的超类型。

例如String和String?,如果一个函数接收的是String?,我们传入的是String的话,编译器是不会报错的,但是如果一个函数接受的是String,我们传入的是String?的话,编译器就会提示我们可能会存在空指针的问题,所以String就是String?的子类型,String?就是String的超类型。

子类型化关系

如果需要A类型的地方,都可以用B类型来代替,那么B类型就是A类型的子类型,B类型到A类型之间的映射关系就是子类型化关系。

举个例子:List是List的子类型,所以List到List之间存在子类型化关系,List是List的子类型,所以List到List之间存在子类型化关系,MutableList和MutableList之间就没有关系,这个会在下面解释。

协变

协变(convariant)就是保留子类型化关系,保证泛型内部操作该类型时是只读的,在Java中,带extends限定(上界)的通配符类型使得类型是协变的。

因为List是协变,String是Any的子类型,String是String?的子类型,所以List是List的子类型,List是List的子类型。

out协变点

以下代码是标准的out协变点:

// T被声明为out
interface Producer {

    // T作为只读属性的类型
    val value: T

    // T作为函数返回值的类型
    fun produce(): T

    // T作为只读属性的类型List泛型的类型实参
    val list: List

    // T作为函数返回值的类型List泛型的类型实参
    fun produceList(): List

}

out协变点基本特征:出现的位置是只读属性的类型或者函数的返回值类型,它作为生产者的角色,请求向外部输出。

源码分析

在源码中,最为代表性就是List,代码如下:

// Collections.kt
// E被声明为out
public interface List : Collection {

    override val size: Int
    override fun isEmpty(): Boolean

    // E作为函数形参类型,而且还加上了@UnsafeVariance注解,下面会解释
    override fun contains(element: @UnsafeVariance E): Boolean

    // E作为函数返回值的类型Iterator泛型的类型实参
    override fun iterator(): Iterator

    // E作为函数形参的类型Collection泛型的类型实参,而且还加上了@UnsafeVariance注解,下面会解释
    override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean

    // E作为函数返回值的类型
    public operator fun get(index: Int): E

    // E作为函数形参类型,而且还加上了@UnsafeVariance注解,下面会解释
    public fun indexOf(element: @UnsafeVariance E): Int

    // E作为函数形参类型,而且还加上了@UnsafeVariance注解,下面会解释
    public fun lastIndexOf(element: @UnsafeVariance E): Int

    // E作为函数返回值的类型ListIterator泛型的类型实参
    public fun listIterator(): ListIterator

    // E作为函数返回值的类型ListIterator泛型的类型实参
    public fun listIterator(index: Int): ListIterator

    // E作为函数返回值的类型List泛型的类型实参
    public fun subList(fromIndex: Int, toIndex: Int): List

}

逆变

逆变(contravariance)就是反转子类型化关系,保证泛型内部操作该类型时是只写的,在Java中,带super限定(下界)的通配符类型使得类型是逆变的。

因为Comparable是逆变,String是Any的子类型,String是String?的子类型,所以Comparable是Comparable的子类型,Comparable是Comparable的子类型。

in逆变点

以下代码是标准的in逆变点:

// T被声明为in
interface Consumer {

    // T作为函数形参类型
    fun consume(value: T)

    // T作为函数形参的类型List泛型的类型实参
    fun consumeList(list: List)

}

in逆变点基本特征:出现的位置是函数形参类型,它作为消费者,请求外部输入。

源码分析

在源码中,最为代表性就是Comparable,代码如下:

// Comparable.kt
// T被声明为in
public interface Comparable {

    // T作为函数形参类型
    public operator fun compareTo(other: T): Int

}

不型变

不型变就是既不被声明为out,也不被声明为in的泛型。

因为MutableList是不型变,虽然String是Any的子类型,String是String?的子类型,但是MutableList和MutableList之间没有任何关系,MutableList和MutableList之前没有任何关系。

不型变的基本特征:可以出现在任何位置。

源码分析

在源码中,最为代表性就是MutableList,代码如下:

// Collections.kt
public interface MutableList : List, MutableCollection {

    // E作为函数形参类型
    override fun add(element: E): Boolean

    // E作为函数形参类型
    override fun remove(element: E): Boolean

    // E作为函数形参的类型Collection泛型的类型实参
    override fun addAll(elements: Collection): Boolean

    // E作为函数形参的类型Collection泛型的类型实参
    public fun addAll(index: Int, elements: Collection): Boolean

    // E作为函数形参的类型Collection泛型的类型实参
    override fun removeAll(elements: Collection): Boolean

    // E作为函数形参的类型Collection泛型的类型实参
    override fun retainAll(elements: Collection): Boolean
    override fun clear(): Unit

    // E作为函数形参类型
    public operator fun set(index: Int, element: E): E

    // E作为函数形参类型
    public fun add(index: Int, element: E): Unit

    // E作为函数返回值的类型
    public fun removeAt(index: Int): E

    // E作为函数返回值的类型MutableListIterator泛型的类型实参
    override fun listIterator(): MutableListIterator

    // E作为函数返回值的类型MutableListIterator泛型的类型实参
    override fun listIterator(index: Int): MutableListIterator

    // E作为函数返回值的类型MutableList泛型的类型实参
    override fun subList(fromIndex: Int, toIndex: Int): MutableList

}

@UnsafeVariance

在上面说的List源码中,我们发现虽然List是协变的,但是有时出现的位置是逆变的位置,这是为什么呢?其实是可以出现在任何位置上,但是要保证以下两点定义:协变保证泛型内部操作类型时是只读的,逆变保证泛型内部操作类型时是只写的,大体上要遵循上面说的那几个out协变点和in逆变点。

我们可以通过加上@UnsafeVariance注解告诉编译器这个地方是合法、安全,让其通过编译,如果不加的话,编译器会认为你这里是不合法,编译不通过。

例如上面说的List源码中,有一个contains函数,这个函数的作用是检查此元素是否包含在此集合中,它的实现方法没有出现写操作,所以这里就可以加上@UnsafeVariance注解,让其通过编译器。

使用处型变和声明处型变

Java是使用使用处型变,有如下接口:

public interface IGeneric {
    // 省略部分代码
}

Java是禁止这样的操作的:

private void setData(IGeneric item) {
    // Java禁止这样的操作
    IGeneric newItem = item;
}

 
   

我们应该写成如下这样:

private void setData(IGeneric item) {
    IGeneric newItem = item;
}

我们必须把newItem声明为IGeneric,类型变得更复杂了,复杂的类型并没有给我们带来任何价值,这种就叫做使用处型变,我们看下Kotlin的写法吧,有如下接口:

// T被声明为out
interface IGeneric {
    // 省略部分代码
}

有如下方法:

private fun setData(item: IGeneric) {

    // 泛型IGeneric的类型实参是Any
    val newItem: IGeneric = item

}

这种就做声明处型变,我们只需要在用out修饰符修饰T即可,语义简单了很多,当然Kotlin也可以使用使用处型变的,我们不再用out修饰符修饰T,代码如下:

interface IGeneric {
    // 省略部分代码
}

然后我们在声明类型的时候加上out修饰符,代码如下:

private fun setData(item: IGeneric) {

    // 泛型IGeneric的类型实参Any被声明为out
    val newItem: IGeneric = item

}

/   星投影   /

定义

有时候,我们对类型参数一无所知,但是仍然希望以安全的方式使用它,我们可以使用星投影,这个泛型类型的每个具体实例化是这个投影的子类型。

语法

  • 对于Function,T是泛型Function的一个具有上界String的协变类型参数,Function<*>等价于Function,这意味着当T为未知时,我们可以安全地从Function<*>读取String的值。

  • 对于Function,T是泛型Function的一个逆变类型参数,Function<*>等价于Function,这意味着当T为未知时,我们不能安全地写入Function<*>。

  • 对于Function,T是泛型Function的一个不型变类型参数,Function<*>读取值时等价于Function,写入值时等价于Function。

如果一个泛型类型具有多个类型参数,那么它们每个类型参数都可以单独投影,例如:如果类型被声明为Function,那么它的星投影就如下:

  • Function<*, String>表示Function

  • Function表示Function

  • Function<*, >表示Function

要注意的是星投影非常像Java的原始类型,但是是安全的。

这里解释一下Nothing,Nothing是所有类型的子类型,源码如下:

public class Nothing private constructor()

应用

我们可以扩展Boolean,让其更具有函数式编程的味道,让链式调用更加顺滑,代码如下:

package com.tanjiajun.booleanextensiondemo

/**
 * Created by TanJiaJun on 2020-01-28.
 */
sealed class BooleanExt

class TransferData(val data: T) : BooleanExt()
object Otherwise : BooleanExt()

inline fun  Boolean.yes(block: () -> T): BooleanExt =
    when {
        this -> TransferData(block.invoke())
        else -> Otherwise
    }

inline fun  BooleanExt.otherwise(block: () -> T): T =
    when (this) {
        is Otherwise -> block()
        is TransferData -> data
    }

调用地方,代码如下:

package com.tanjiajun.booleanextensiondemo

import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity

/**
 * Created by TanJiaJun on 2020-01-28.
 */
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 第一个例子
        val name = "谭嘉俊"
        (name == "谭嘉俊")
            .yes { Log.i("TanJiaJun", name) }
            .otherwise { Log.i("TanJiaJun", "苹果") }

        // 第二个例子
        val strings = mutableListOf(2, 4, 6, 8, 10)
        (strings
            .filter { it % 2 == 0 }
            .count() == strings.size)
            .yes { Log.i("TanJiaJun", "是偶数集合") }
            .otherwise { Log.i("TanJiaJun", "不是偶数集合") }
    }

}

我们可以看到密封类BooleanExt,它是个泛型,T是一个协变类型参数,为什么要用到协变呢?我们可以观察到T都出现在out协变点,所以T可以被声明为out。

我们还看到对象Otherwise继承密封类BooleanExt,我使用了Nothing,为什么要使用Nothing呢?因为在Boolean的扩展函数yes中返回的是BooleanExt,如果要返回Otherwise,我们就只能使用Nothing,因为Nothing是所有类型的子类型,上面也提及过,所以这样就符合协变的定义了。

/   题外话   /

PECS原则

PECS原则是指Producer-Extends, Consumer-Super,它是Effective Java提出来的,如果泛型的类型实参是生产者,那么就应该用extends;如果泛型的类型实参是消费者,那么就应该用super。

密封类

在我的示例代码中,我用到了sealed这个修饰符,它可以声明一个密封类,我这里大概说下密封类:

  • 密封类用来表示受限的类继承结构,意思就是当一个值为有限几种的类型,而不能有任何其他类型,其实他们在某种意义上,有点像枚举类的扩展,不过枚举类型的值集合是受限的,每个枚举常量只存在一个实例,而密封类的一个子类可以有包含状态的多个实例。

  • 密封类可以有子类,但是所有子类必须在与密封类自身相同的文件中声明。

  • 密封类自身是抽象的,它不能直接实例化,但是可以有抽象成员。

  • 密封类不允许有非private构造函数,它的构造函数就是private的。

  • 扩展密封类子类的类可以放在任何位置,而不需要放在同一个文件中。

  • 使用密封类还有个好处在于使用when表达式的时候,当我们用when作为表达式的时候,也就像上面示例代码中的otherwise,我是使用了它的结果,而不是作为语句,如果能够验证语句覆盖了所有情况的时候,我们就不需要再为语句添加一个else子句了。

推荐阅读:

Dart语言快速入门

为了KPI,对APK进行极限优化!

Google官方提供的分页加载解决方案

欢迎关注我的公众号

学习技术或投稿

长按上图,识别图中二维码即可关注

你可能感兴趣的:(Kotlin中关于泛型型变的那些事)