Kotlin之泛型(二)–星投影和内联特化

UnsafeVariance注解

在上一篇文章中,我们重新学习了一下Kotlin的型变,尤其是对协变和逆变的理解和使用。我们知道,型变的的主要目的是为了我们在写代码的时避免类型安全的问题,防止在调用传入某些实例操作时存在不当的读写操作,所以协变的定义就是泛型类型内部不存在写操作,而逆变一般只存在写操作。但在我们实际的开发场景中,不能一成不变的按照协变和逆变点规则来用,这样大大降低了代码的灵活性,因为我们在某些情况不得不在需要在协变的情况下来使用泛型参数类型作为方法参数的情况,比如:

public interface Collection<out E> : Iterable<E> {
    
    public operator fun contains(element: @UnsafeVariance E): Boolean

}
复制代码

所以,对于协变和逆变来说:协变点和逆变点的位置规则在大多数情况下和规定是一致的,但我们要根据实际的开发场景以及对这个泛型类型的设计分析,适当在不违背其根本宗旨的情况调整协变和逆变点的规则。由上面代码也可以看出,泛型协变类型参数E作为实参传入了给contains方法,它出现的地方并非协变点,这个时候编译时编译器会报错,因为它认为这是不合法的。这个时候我们需要使用@UnsafeVariance注解告诉编译器,这个地方的使用安全性我自己来把握,我知道自己在干嘛,你让我编译通过就行。

星投影

星投影的意思就是有时候可以用*号来替代我们的泛型参数,星投影的主要特征是:

*可用在变量类型声明的位置;

*用来描述一个未知的类型;

*所替换在的类型:协变点返回泛型参数上限类型;逆变点接受泛型参数下限类型。

下面看一下用*替换协变参数的情况:

fun main(){
    //协变取类型限制上限
    val queryMap:QueryMap<*,*> = QueryMap<String,Int>()
    queryMap.getKey()
    queryMap.getValue()
}


class QueryMap<out K:CharSequence,out V:Any>{
    fun getKey():K = TODO()
    fun getValue():V = TODO()
}
复制代码

这里协变需要取上限值,getKey()返回的就是CharSequence,getValue()返回的就是Any。

*在逆变情况中替换泛型参数的情况:

class Function<in P1,in P2>{

    fun invoke(p1:P1,p2:P2) = Unit
}

fun main()
    val f:Function<*,*> = Function<Number,Any>()
    f.invoke() //调用invoke方法报错
}

复制代码

在逆变泛型参数的约束中是取类型的下限,但在Kotlin中的定义泛型时是无法定义下限的,对于所有类型来说下限就是Nothing,这样就意味Function中的invoke()方法无法被调用,因为Nothing并没有实例。

根据上面的分析可以得出星投影的投影规则:当*星投影用在协变的时候,对应位置的泛型类型参数取其类型的上限,当用在逆变的时候就取对应类型的下限,但Kotlin中的下限比较特殊而无法定义,所有类型的下限都是Nothing,所以对应的下限就是Nothing。

星投影的适用范围

*不能直接或间接应用在属性或函数上,例如以下这种情况是不被允许的:

QueryMap<String,*>() 

maxOf<*>(1,3)
复制代码

*适用于作为类型描述的场景

val queryMap:QueryMap<*,*> 

//判断变量是某一个类型
if(f is Function<*,*>){}

//List作为一个参数,但是List泛型中的*是作为一个描述,这个时候也是可以作为星投影
HashMap<String,List<*>>
复制代码

泛型实现原理

看看下面一个比较大小的带有泛型参数的函数,其编译前是下面这样的:

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

经过编译器编译后的字节码代码是这样的:

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

函数maxOf()参数类型直接取了泛型参数类型上限,而不是我们原来的T类型。假如我们传一个String类型实参进去,经过编译后,其实这个String类型已经不存在了,这个就叫类型擦除

**伪泛型:**编译时擦除类型,运行时无实际类型生成看,Java、Kotlin都是如此。

Java、Kotlin泛型实现过程,以List<String>为例:

编译前:List<String>

编译后:List,类型已被擦除

运行时:List,在内存中。

所以,当我们定义带有泛型参数的函数时,这个泛型类型是无法当做真实类型来用的,观察下面代码:

fun <T> genericMethod(t:T){
   val t = T()     //报错 Type parameter T cannot be called as function
   val tArr = Array<T>(3){ TODO()} //报错 Cannot use 'T' as reified type parameter. Use a class instead.
   val jclass = T::class.java //报错 Cannot use 'T' as reified type parameter. Use a 
   val list = ArrayList<T>() //可以声明,因为ArrayList在编译后类型会被擦除掉
}
复制代码

内联特化

如果在上面genericMethod(t:T)方法中想要用T的类型有什么办法呢?在Kotlin中有一个很好的特性可以解决这个问题:内联特化,关键字是:reified。内联特化,显而易见,首先是内联,然后是特化,内联就是内联方法,内联的特性就是在调用该函数的时其内部的逻辑代码会被搬到调用处替换掉,那这样T就会变成我们实际调用时传的类型,这个泛型参数T变成实际类型的过程就叫特化。看下面代码:

inline fun <reified T> genericMethod(t:T){
    //这个依旧是报错的,很好理解,虽然我们再使用内联特化时候可以确定类型,但是不能确定是否有无参构造函数
    val t = T()
    
    //以下几个执行都会通过
    val tArr = Array<T>(3){ TODO()}
    val jclass = T::class.java
    val list = ArrayList<T>()
}
复制代码

内联特化的实际应用

在使用Gson解析数据时,我们需要拿到传入的类型参数去构造对象,如果我们写成一般泛型的时候的是无法直接使用泛型去拿到对象类型的,也无法构造对象,类似下面:

fun <T> Gson.fromJson(json:String):T
                    = fromJson(json,T::class.java)
复制代码

根据我们前面的代码示例可以知道T::class.java是无法获取到类型的,因为T会被擦除掉,这个时候就要用到内联特化了。

inline fun <reified T>  Gson.fromJson(json:String):T
                    = fromJson(json,T::class.java)
复制代码

最后,我们一起学习分析一下Bennyhuo大神写的基于泛型实现model实例注入的案例,加深对泛型的常见概念和用法的掌握,并提高结合Kotlin其他知识点的综合使用。这个案例的具体效果就是通过initModels()方法把所有要用的model创建出来,然后放到一个HashMap里面,要用的时候通过代理的方式在这里获取出来即可。观察以下代码:

//定义一个抽象类 所有的具体model类都继承该类,其init块执行单例Models.run方法
abstract class AbsModel {
    init {
        //register方法时AbsModel的扩展方法,this@AbsModel就是AbsModel的子类
        Models.run { this@AbsModel.register() }
    }
}

//声明一个DatabaseModel 继承AbsModel 
class DatabaseModel : AbsModel() {
    fun query(sql: String): Int = 0
}

//声明一个NetworkModel 继承AbsModel 
class NetworkModel : AbsModel() {
    fun get(url: String): String = """{"code": 0}"""
}

//声明一个SpModel 继承AbsModel
class SpModel : AbsModel() {
    init {
        Models.run { register("SpModel2") }
    }

    fun hello() = println("HelloWorld")
}

//Models是用来存储和获取实例化的AbsModel子类
object Models {
    private val modelMap = ConcurrentHashMap<String, AbsModel>()

    //register是AbsModel的扩展方法
    fun AbsModel.register(name: String = this.javaClass.simpleName) {
        modelMap[name] = this
    }

    fun <T: AbsModel> String.get(): T {
        return modelMap[this] as T
    }
}

fun initModels() {
    DatabaseModel()
    NetworkModel()
    SpModel()
}

//属性代理
object ModelDelegate {
    operator fun <T: AbsModel> getValue(thisRef: Any, property: KProperty<*>): T {
        return Models.run {
            //最后的get就是调用了Models里面的String.get()扩展方法,返回了AbsModel子类的实例
            //capitalize()是让实例化时类名字首字母小写转大写
            property.name.capitalize().get()
        }
    }
}

class MainViewModel {
    val databaseModel: DatabaseModel by ModelDelegate
    val networkModel: NetworkModel by ModelDelegate
    val spModel: SpModel by ModelDelegate
    val spModel2: SpModel by ModelDelegate
}

fun main() {
    initModels()
    val mainViewModel = MainViewModel()
    mainViewModel.databaseModel.query("select * from mysql.user").let(::println)
    mainViewModel.networkModel.get("https://www.imooc.com").let(::println)
    mainViewModel.spModel.hello()
    mainViewModel.spModel2.hello()
}
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享