阅读 730

Kotlin系列——泛型型变

本文章已授权微信公众号郭霖(guolin_blog)转载。

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

BooleanExtensionDemo

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

List<String> strings = new ArrayList<String>();
// Java中禁止这样的操作
List<Object> objects = strings;
复制代码

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

val strings: List<String> = arrayListOf()
val anys: List<Any> = strings
复制代码

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

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

形式参数和实际参数

函数中的形参和实参

代码如下:

fun add(firstNumber: Int, secondNumber: Int): Int =
    firstNumber + secondNumber
复制代码

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

val first = 1
val second = 2
add(first, second)
复制代码

firstsecond就是add函数实际参数

泛型中的形参和实参

代码如下:

class Fruit<T>(var item: T)
复制代码

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

val fruit = Fruit<Int>(100)
复制代码

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

val fruit = Fruit(100)
复制代码

在这种情况下,Int依然是Fruit类型实参

还有以下情况,请看代码:

// Collections.kt
public interface MutableList<E> : List<E>, MutableCollection<E> {
    // 省略部分代码
}
复制代码

这里的EListMutableCollection类型实参,同时是MutableList类型形参

结论

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

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

子类继承超类,例如class Apple: Fruit()Apple就是Fruit子类Fruit就是Apple超类,那什么是子类型超类型呢?它们的规则比子类超类更加宽松,如果需要A类型的地方,都可以用B类型来代替,那么B类型就是A类型的子类型,A类型就是B类型的超类型,例如StringString?,如果一个函数接收的是String?,我们传入的是String的话,编译器是不会报错的,但是如果一个函数接受的是String,我们传入的是String?的话,编译器就会提示我们可能会存在空指针的问题,所以String就是**String?**的子类型,String?就是String的超类型。

子类型化关系

如果需要A类型的地方,都可以用B类型来代替,那么B类型就是A类型的子类型,B类型到A类型之间的映射关系就是子类型化关系,举个例子:ListList子类型,所以ListList之间存在子类型化关系ListList<String?>子类型,所以ListList<String?>之间存在子类型化关系,**MutableListMutableList**之间就没有关系,这个会在下面解释。

协变

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

因为List协变StringAny子类型StringString?子类型,所以ListList子类型ListList<String?>子类型

out协变点

以下代码是标准out协变点

// T被声明为out
interface Producer<out T> {

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

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

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

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

}
复制代码

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

源码分析

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

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

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

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

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

    // 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>

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

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

}
复制代码

逆变

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

因为Comparable逆变StringAny子类型StringString?子类型,所以ComparableComparable子类型Comparable<String?>Comparable子类型

in逆变点

以下代码是标准in逆变点

// T被声明为in
interface Consumer<in T> {

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

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

}
复制代码

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

源码分析

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

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

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

}
复制代码

不型变

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

因为MutableList不型变,虽然StringAny子类型StringString?子类型,但是MutableListMutableList之间没有任何关系MutableListMutableList<String?>之前没有任何关系

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

源码分析

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

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

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

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

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

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

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

    // E作为函数形参的类型Collection泛型的类型实参
    override fun retainAll(elements: Collection<E>): 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>

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

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

}
复制代码

@UnsafeVariance

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

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

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

使用处型变和声明处型变

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

public interface IGeneric<T> {
    // 省略部分代码
}
复制代码

Java禁止这样的操作的:

private void setData(IGeneric<String> item) {
    // Java禁止这样的操作
    IGeneric<Object> newItem = item;
}
复制代码

我们应该写成如下这样:

private void setData(IGeneric<String> item) {
    IGeneric<? extends Object> newItem = item;
}
复制代码

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

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

有如下方法

private fun setData(item: IGeneric<String>) {

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

}
复制代码

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

interface IGeneric<T> {
    // 省略部分代码
}
复制代码

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

private fun setData(item: IGeneric<String>) {

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

}
复制代码

星投影

定义

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

语法

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

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

  • Function<*, String>表示Function<in Nothing, String>
  • *Function<String, >表示Function<String, out Any?>
  • *Function<*, >表示Function<in Nothing, out Any?>

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

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

public class Nothing private constructor()
复制代码

应用

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

package com.tanjiajun.booleanextensiondemo

/**
 * Created by TanJiaJun on 2020-01-28.
 */
sealed class BooleanExt<out T>

class TransferData<T>(val data: T) : BooleanExt<T>()
object Otherwise : BooleanExt<Nothing>()

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

inline fun <T> BooleanExt<T>.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子句了。

我的GitHub:TanJiaJunBeyond

Android通用框架:Android通用框架

我的掘金:谭嘉俊

我的简书:谭嘉俊

我的CSDN:谭嘉俊

文章分类
Android
文章标签