Kotlin泛型的高级特性(六)

作者: 阴天吃鱼 | 来源:发表于2020-06-16 12:05 被阅读0次

    泛型的高级特性
    1、泛型实化
    2、泛型协变
    3、泛型逆变

    • 泛型实化

    在Java中(JDK1.5之后),泛型功能是通过泛型擦除来实现的。什么意思呢? 就是在对泛型的约束只是在编译阶段,运行的时候的JVM是识别不出来在代码中指定的类型的。 比如说List<String>,编译阶段限制了就是String,但在运行的时候JVM并不知道它本身只打算包含 “哪种类型”,只能识别它是个List。
      Kotlin也是这样,然而不同的是Kotlin提供了内联函数的概念。内联函数的意思就是,在编译的时候自动将内联函数修饰的代码替换到调用它的地方,这样的话就不会有泛型擦除了,因为代码在编译之后会使用实际的类型来替换内联函数中泛型声明。举个例子说明:

    fun getO() {
        getSome<String>()
    }
    
    inline fun <T> getSome() {
        //dosomething with T 
    }
    
    上面的调用,经过实际替换是:
    fun getO() {
         //dosomething with String
    }
    

    可以看出来O调用了一个带有泛型的内联函数,O调用了getSome。在代码编译后,O函数中的代码将可以获得泛型的实际类型,也就是注释里写的。这意味着在Kotlin中泛型是可以得到实化的。

    基于上述,我们可以思考一下如何获取对应的实化类型,举例:

    fun getO() {
        val type1 = getenericType<String>()
        val type2 = getenericType<Int>()
        println("result:$type1")
        println("result:$type2")
    }
    
    inline fun <reified T> getenericType() = T::class.java
    
    log->
    result:class java.lang.String
    result:class java.lang.Integer
    

    可以看到讲泛型类型定义String,就能获取到String的类型,这就是泛型的实化。
    reified这个函数,是我在这样写了之后,编译必须要加的,否则不通过。我又查阅了一些资料, 由此得到结论,获取泛型的实化必须被reified和inline同时修饰。

    结论
    泛型的实化的前置条件 : 必须被reified和inline同时修饰。

    既然如此我自然想在项目中应用,举例:

    inline fun <reified T> startActivity(context: Context?) {
        val intent = Intent(context, T::class.java)
        context?.startActivity(intent)
    }
    
    inline fun <reified T> startActivity(context: Context?, block: Intent.() -> Unit) {
        val intent = Intent(context, T::class.java)
        intent.block()
        context?.startActivity(intent)
    }
    
    override fun onClick(v: View?) {
            when (v) {
                llOfflineMap -> {
                    startActivity<OfflineMapActivity>(activity)
                }
                llAboutUs -> {
                    startActivity<AboutActivity>(activity) {
                        putExtra("name", "name")
                        putExtra("name2", "name2")
                    }
                }
            }
        }
    
    • 泛型协变

    假设某个方法接收的是Persion类型,但是你传入的是个Student类型。在Java中是不允许这么做,因为Student类型不能成为Persion类型,否则可能存在类型转换的安全隐患。说白了,程序会报 :类转换异常的错误。
      而在Kotlin中,假设MyClass<T>的泛型类,其中A是B的子类型,同时MyClass<A>是MyClass<B>的子类型,那么我们就可以称MyClass在T这个泛型上是“协变”的。
      如果一个泛型类在其泛型类型的数据上是只读的话,那么它是没有类型转换安全隐患的,而要实现这一点,则需要让MyClass<T>类中所有的方法都不能接收T类型的参数,换句话说就是。T只能出现在out位置上,而不能出现在in位置上。举例代码:

    open class Persion(val name: String, val age: String)
    class Student(val sname: String, val sage: String) : Persion(sname, sage)
    

    student是Persion的子类。

    class SimpleData<out T>(val aa: T?) {
        fun getData(): T? {
            return aa
        }
    }
    

    SimpleData的泛型out T是只读,而参数aa是传入的T,不对其具有修改。那么:

    fun main() {
        val student = Student("name", "age")
        val simpleData = SimpleData<Student>(student)
        handleSimpleData(simpleData)
        val data = simpleData.getData()
        println("type:$data")
    }
    
    fun handleSimpleData(data: SimpleData<Persion>) {
        //这里获取的是一个Persion类型,
        //但是因为Persion是Student的父类,向上转型是完全安全的。
        //如果某个方法接收的是List<Persion>类型,而传入的是List<Student>类型,在Java中是不允许这么做的。
        val data1 = data.getData()
        println("type:$data1")
    }
    

    这就是泛型的协变。

    其实Kotlin已经默认给许多内置的API加上了协变声明。其实就包括了各种集合。Kotlin中的List本身就是只读的,如果你想要修改的话,需要使用MutableList才行!!!也就意味着它天然就是可以协变的。
    List部份源码:

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

    可以看到List具有协变。
    contains这个参数,可以看到E出现在了in位置上,实际contains只是为了判断当前集合中是否包含从参数中传入的这个元素,不会修改当前集合中的内容,因此这种操作实质上又是安全的。 但编译器不支持,为了让编译器通过加了@UnsafeVariance 注解。也就是说你想让它在in上,加@UnsafeVariance 注解编译器就不管,但这滥用这种功能导致的类型转换异常,要自己承担咯~

    结论:
    1、协变必须满足 接收参数与传入参数之间是父子关系。
    2、协变的T必须是out位,如果想在in加@UnsafeVariance,但这种也是不涉及修改内容的。

    • 泛型逆变

    依据刚才的协变来说,直观的角度就是 ,MyClass<A>是MyClass<B>的子类型。逆变就是将此处返过来。假设MyClass<T>的泛型类,其中A是B的子类型,同时MyClass<B>是MyClass<A>的子类型,那么我们就可以称MyClass在T这个泛型上是“协变”的。
      那么已知A是B的子类型,怎么让MyClass<B>过来成为MyClass<A>的子类型呢。举例如下:

    open class Persion(val name: String, val age: String)
    class Student(val sname: String, val sage: String) : Persion(sname, sage)
    class Teacher(val tname: String, val tage: String) : Persion(tname, tage)
    

    定义AB类型

    interface TransForms<in T> {
        fun transForms(t: T): String
    }
    
    fun main() {
        val oo = (object : TransForms<Persion> {
            override fun transForms(t: Persion): String {
                return "value: ${t.name}" + "${t.age}"
            }
        })
        setValue(oo)
    }
    
    fun setValue(trans: TransForms<Student>) {
        val student = Student("zhangsan", "12")
        trans.transForms(student)
    }
    

    从代码的角度来说,Student是Persion的子类。而逆变就在接口里:in
    在泛型T声明上加了个in,意味着T现在只能出现在in的位置上,而不能出现在out的位置上。逆变的用法大概就这样了。

    让我们继续深入,为什么逆变的时候泛型T不能出现在out上呢?我们假设它可以。

    interface TransForms<in T> {
        fun transForms(name: String, age: String): @UnsafeVariance T
    }
    

    为了让编译通过,@UnsafeVariance修饰。

    fun main() {
        val oo = (object : TransForms<Persion> {
            override fun transForms(name: String, age: String): Persion {
                return Teacher(name, age)
            }
    
        })
        setValue(oo)
    }
    
    fun setValue(trans: TransForms<Student>) {
        //想要student的返回值
        val transForms = trans.transForms("zhangsan", "12")
    }
    

    上述代码就是典型的违反逆变规则而造成类型转换异常的例子。
    在setValue方法中,我们期望得到的是一个Student的对象的返回,然而实际上
    transForms方法返回的是一个Teacher对象。因此这里会造成类型转换异常的错误。

    Exception in thread "main" java.lang.ClassCastException: com.hdsx.guangxihighway.Teacher cannot be cast to com.hdsx.guangxihighway.Student
        at com.hdsx.guangxihighway.TestKt.setValue(Test.kt:20)
        at com.hdsx.guangxihighway.TestKt.main(Test.kt:15)
        at com.hdsx.guangxihighway.TestKt.main(Test.kt)
    

    也就是说Kotlin在提供协变和逆变的时候,就已经把各种潜在的类转换安全隐患考虑进去了,只要严格按照其语法规则。就不会存在类型转换异常的情况。虽然@UnsafeVariance可以打破,但是也承担着额外的风险。

    逆变的典型例子:

    public interface Comparable<in T> {
        /**
         * Compares this object with the specified object for order. Returns zero if this object is equal
         * to the specified [other] object, a negative number if it's less than [other], or a positive number
         * if it's greater than [other].
         */
        public operator fun compareTo(other: T): Int
    }
    

    为什么这个泛型上是逆变的呢,因为compareTo方法用于实现具体的比较逻辑。如果我们使用Comparable<Persion>实现了让两个Person对象比较大小的逻辑。那么用这段逻辑去比较两个Student对象的大小也一定是成立的。因此让Comparable<Persion>成为Comparable<Student>的子类型合情合理。

    这就是逆变。

    相关文章

      网友评论

        本文标题:Kotlin泛型的高级特性(六)

        本文链接:https://www.haomeiwen.com/subject/bcjoahtx.html