kotlin基础十二:泛型 协变 逆变 类型投影 星投影

229 阅读5分钟

前言

在实际开发中我们可能很少去写一部分代码。很多时候在源码里看到,但不太理解。需要我们熟悉源码中泛型 协变 逆变 类型投影 星投影等知识

协变 逆变的约定

如果将泛型定义一个方法的参数时,泛型在in位置。将泛型作为方法返回值,称泛型在out位置。

class BaseClass<T>{
    private val data:T?=null
    // 泛型在in位置
    fun setData(data:T){}
    // 泛型在out位置
    fun getData():T?{return data}
}

只能从中读取的对象为生产者,只能写入的对象为消费者。

泛型的协变

String是Any的子类,List<String>不是List<Any>的子类。为了使List<String>List<Any>子类,需要关键字out修饰泛型。例如List接口

public interface List<out E>:Collection<E>{}

泛型E在接口声明中用了out关键字修饰,下面代码才通过编译器验证:

fun test() {
    val listAny = mutableListOf<Any>()
    val listString = mutableListOf<String>()
    // List<String>使List<Any>的子类
    listAny.addAll(listString)
}

将泛型E作为方法的返回值,该参数类型是只读的。如果强行的将泛型作为方法的参数,Kotlin则提示语法错误

class BaseClass<out T>{// T类型只读
    private val data:T?=null
    // 泛型在in位置,编译报错,因为T类型只读,不能写入
    fun setData(data:T){}
    // 泛型在out位置
    fun getData():T?{return data}
}

泛型类或接口中,如果A是B的子类,同时Class<A>又是Class<B>的子类,我们称类Class在该泛型上是协变的。A-->B class<A> --协变-> Class<B>。协变的目的为防止类型转换的隐患

fun test() {
    val baseClass = BaseClass<Student>()
    val student = Student()
    baseClass.setData(student)
    // 编译不通过,把baseClass给了方法syncData,但是内部baseClass通过setData传了teacher
    syncData(baseClass)
}
private fun syncData(baseClass:BaseClass<Person>){
    val teacher = Teacher()
    baseClass.setData(teacher)
}
open class Person
class Student:Person()
class Teacher:Person()
class BaseClass<T>{
    private val data:T?=null
    fun setData(data:T){}
    fun getData():T?{return data}
}

所以生命力泛型是协变的,该泛型就应该只读,只能放在out位置。特殊情况,我们可以将一个协变放到in位置。例如我们只用这个反射进行类型上的判断或属性读取,不涉及类型的转换。例如List的接口contains()

public interface List<out E>:Collection<E>{
    override val size: Int
    override fun isEmpty(): Boolean
    override fun contains(element: @UnsafeVariance E): Boolean
    override fun iterator(): Iterator<E>
    override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean 
}

强行声明一个out泛型在in位置,需要使用@UnsafeVariance注解来标记。告诉编译器确定这样用,带来类型转换的风险也需要自己承担。

泛型的逆变

如果类A是类B的子类,而Class<B>又是Class<A>的子类,我们称该类是逆变的。 某个类在某泛型上逆变用关键字in

fun test() {
    val transform = object :Transform<Person>{
        override fun transform(t: Person): Int {
            return t.hashCode()
        }
    }
    // 编译异常
    transformAction(transform)
}
interface Transform<T>{
    fun transform(t:T):Int
}
open class Person
class Student:Person()
fun transformAction(transform:Transform<Student>){
    val student = Student()
    transform.transform(student)
}

用关键字in处理上面异常

fun test() {
    val transform = object :Transform<Person>{
        override fun transform(t: Person): Int {
            return t.hashCode()
        }
    }
    // 编译通过
    transformAction(transform)
}
interface Transform<in T>{
    fun transform(t:T):Int
}
open class Person
class Student:Person()
fun transformAction(transform:Transform<Student>){
    val student = Student()
    transform.transform(student)
}

将泛型T放在in位置(方法参数),不会放在out位置(方法返回值)。泛型T放到out位置的隐患:

fun test() {
    val transform = object :Transform<Person>{
        override fun transform(t: Person): Person {
            return Teacher()
        }
    }
    transformAction(transform)
}
interface Transform<in T>{
    fun transform(t:T):@UnsafeVariance T
}
open class Person
class Student:Person()
class Teacher:Person()
fun transformAction(transform:Transform<Student>){
    val student = Student()
    transform.transform(student)
}

和协变一样,可以将注解in关键字修饰的泛型放到out位置,带来的风险需要自己承担。in位置只能作为方法的参数来使用,out位置只能作为方法的返回值来使用。如果强行使用注解逃避编译器的检查,类型转换的风险就需要自己承担。逆变在Kotlin内部API的Comparable示例:

fun test() {
    val student = Person(20)
    compare(student)
}
// 逆变
public interface Comparable<in T>{
    public operator fun compareTo(other: T):Int
}
open class Person(private val age:Int):Comparable<Person>{
    override fun compareTo(other: Person): Int {
        return if (this.age==other.age)0
        else if (this.age>other.age)1
        else -1
    }
}
class Student(private val age:Int):Person(age)
fun compare(student :Comparable<Student>){
    val jack = Student(18)
    val result = student.compareTo(jack)
    println(result)
}

使用逆变让Comparable<Person>变成了Comparable<Student>的子类。

类型投影 -> 使用处形变

在声明泛型类的时候,该泛型类在该泛型上既不是逆变也不是协变。Kotlin中的Array

class Array<T>(val size:Int){
    fun get(index:Int):T{}
    fun set(index:Int,value:T){}
}

开发中不太灵活,实际开发中定义copy

fun test() {
    val arrayFrom = arrayOf(1,2,3)
    val arrayTo = Array<Any>(3){}
    // 编译报错
    copy(arrayFrom,arrayTo)
}
fun copy(from:Array<Any>,to:Array<Any>){
    assert(from.size==to.size)
    for (i in from.indices){
        to[i] =from[i]
    }
}

Int是Any的子类。但是Array<Int>并不是Array<Any>的子类,编译器不允许这么操作。类型投影可以使Array<Int>成为Array<Any>子类型,这样的使用场景称类型投影:

fun test() {
    val arrayFrom = arrayOf(1,2,3)
    val arrayTo = Array<Any>(3){}
    // 编译报错
    copy(arrayFrom,arrayTo)
}
// 使用out投影一个类型
fun copy(from:Array<out Any>,to:Array<Any>){
    assert(from.size==to.size)
    for (i in from.indices){
        to[i] =from[i]
    }
}

使用out投影一个类型。该类使用处可以调get()方法,而不能调用set()方法。因为copy()函数中给from数组存放一个String,随时造成类型转换异常。int是Any的子类,String也是Any子类。当我们使用out投影Array,那么Array使用将受限,同时也规避了类型转换的风险。同样可以使用in来投影一个类型

fun test() {
    val arrayFill = Array<Any>(3){}
    fill(arrayFill,"str")
}
fun fill(dest:Array<in String>,value:String){
    dest[0] = value
    val str = dest[0]
    println(str)
}

Array<Any>成了Any<String>的子类,将Array<Any>传递给fill()函数。在fill()函数中,只能设置String类型给到dest数组,取出来的也是String,虽然将Array<Any>传递给了fill()数组,使用处,只能存放String数组

星投影->子类型的投影

interface Factory<out T:Person>{
    fun create():T
}

接口Factory中,T是一个拥有上界的Person协变类型参数,T未知,可以安全的从中读取Person的值

interface Factory<in T:Person>{
    fun setInfo(info:T)
}

T是一个拥有上界Person逆变类型参数,T未知,可以安全方式写入Factory<in T:Person>

总结

泛型知识相对比较难理解,掌握泛型基础知识,就能解决比较复杂的场景。