Advertisement

Kotlin的独门秘籍Reified实化类型参数(下篇)

阅读量:

Kotlin系列文章,欢迎查看:

原创系列:

翻译系列:

实战系列:

简述:
本节我们将延续上一期的讨论,并探讨Kotlin泛型中的reified实化类型参数这一主题。鉴于当前内容的深度与复杂性,在本节中我们不会选择翻译的方式进行介绍;相反地,在上一篇推送中我们已经对kotlin语言中的reified实化类型参数有了初步的认识与理解。为了使读者能够更加全面地掌握这一知识点,在本节中我们将深入探讨Kotlin泛型中的reified实化类型参数这一主题,并从源码实现角度出发详细解析其工作原理以及实际应用场景。通过系统性地阐述这一知识点与相关技术实现细节的关系,在后续章节中我们将带领大家深入学习并灵活运用这些知识。

一、泛型类型擦除

如上文所述, 如何实现Java虚拟机(JVM)中的泛型机制一直是编程语言领域的重要研究课题之一. 其核心机制是通过类型擦除技术实现的, 即使得泛型类实例的具体类型实参在编译阶段被去除, 因此在运行阶段也不会保留这些信息. 这种设计思路具有深远的历史渊源, 主导因素之一是为了保证与JDK 1.5及之前版本兼容, 同时这一做法也带来了一定的优势: 在运行时阶段丢弃了部分类型实参的信息, 这不仅有助于降低内存占用量, 而且还能一定程度上减少资源浪费. 尽管如此, 这种基于"伪泛型"的设计理念也在一定程度上限制了其应用范围. 具体而言, 在编译完成后所有类型的实参都会被统一替换为Object对象或指定类型的界约束类; 比如List<Float>、List<String>、List<Student>等列表类实例在运行时都会被替换成Object对象; 只有当泛型定义为List<T extends Student>形式时, 类实参才会被替换成具体的学生类Student. 这些细节均可通过Reflection Erasure类模块进行验证

虽然不需要像Java那样追溯旧版本的历史兼容性,但因为编译后的类依然需在与Java相同的JVM环境中运行,而JVM中的泛型通常通过消除法来处理,这也限制了Kotlin的发展进度。然而,Kotlin是一门充满追求的语言,不希望像C#那样对Java提出质疑,尤其是关于其对泛型集合的理解能力,所以它巧妙地利用了内联函数实现了这一目标

二、泛型擦除会带来什么影响?

深入探讨泛型擦除带来的影响是一个值得研究的方向。作为一个具体的案例,在这里我们选择使用Kotlin来展示这一问题。由于Java中存在相关问题,在Kotlin编程语言中也面临着类似的挑战。让我们通过一个具体的示例来说明这一情况。

复制代码
    fun main(args: Array<String>) {
    val list1: List<Int> = listOf(1,2,3,4)
    val list2: List<String> = listOf("a","b","c","d")
    println(list1)
    println(list2)
    }

上面两个集合各自包含了Int类型的数据以及String类型的数据,在编译后的class文件中它们被替换了List原生类型。进一步查看反编译后的Java代码

复制代码
    @Metadata(
       mv = {1, 1, 11},
       bv = {1, 0, 2},
       k = 2,
       d1 = {"\u0000\u0014\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0010\u0011\n\u0002\u0010\u000e\n\u0002\b\u0002\u001a\u0019\u0010\u0000\u001a\u00020\u00012\f\u0010\u0002\u001a\b\u0012\u0004\u0012\u00020\u00040\u0003¢\u0006\u0002\u0010\u0005¨\u0006\u0006"},
       d2 = {"main", "", "args", "", "", "([Ljava/lang/String;)V", "Lambda_main"}
    )
    public final class GenericKtKt {
       public static final void main(@NotNull String[] args) {
      Intrinsics.checkParameterIsNotNull(args, "args");
      List list1 = CollectionsKt.listOf(new Integer[]{1, 2, 3, 4});//List原生类型
      List list2 = CollectionsKt.listOf(new String[]{"a", "b", "c", "d"});//List原生类型
      System.out.println(list1);
      System.out.println(list2);
       }
    }

我们观察到,在编译后listOf函数接收的是Object类型,并不再局限于具体的String和Int类型。

1、类型检查问题:

在Kotlin中进行类型的验证(即所谓的is操作),通常情况下无法识别传入的实际参数的完整类型信息(特别说明的是后面会有例外的情况将详细讨论),例如下文将详细讨论。

复制代码
    if(value is List<String>){...}//一般情况下这样的代码不会被编译通过

分析: 虽然在运行时我们能确定variable是一个List集合, 但无法得知该集合中存储的具体数据元素类型, 这是因为泛型类的实参和界元形参约束导致了信息丢失. 为了判断variable是否为List, 可以通过以下方法来判断.

Java中的解决办法: 针对之前提到的问题, Java有一种非常简单直接的解决方法, 也就是采用了List的原生类型.

复制代码
    if(value is List){...}

Kotlin中的解决办法:
众所周知,在Kotlin语言中并不存在类似于Java原生类型的机制。为了应对这一挑战,在Kotlin中我们可以利用星投影List<*>(后续将详细讲解这一机制)来解决问题。目前暂且将其视为一个泛型类型,并假设该类型未指定具体的数据类型。它的功能类似于Java中的List<?>通配符的作用。

复制代码
    if(value is List<*>){...}

特殊情况: 我们常说is检查通常无法检测类型实参, 但是有一种特殊的例外情况就是Kotlin的编译器能够自动推理(不得不赞叹Kotlin编译器的强大)

复制代码
    fun printNumberList(collection: Collection<String>) {
    if(collection is List<String>){...} //在这里这样写法是合法的。
    }

分析: 该编译器可基于当前作用域环境自动推断出参数的实际类型。其中,在涉及集合函数时,默认参数会被识别为String类型的。因此,在上述情形下,参数必须指定为String。若误将其他类型的变量作为输入,则会引发错误。

2、类型转换问题:

在Kotlin语言中,默认情况下我们可以调用.as().as?(...)的方法来完成数据类型的转换。需要注意的是,在调用.as()时,默认情况下仍然支持常见的泛型操作。只有当该泛型基类的本体数据类型正确时(即使提供错误的实际参数),程序也会顺利地通过编译过程,并会触发相应的警告信息以提示潜在的问题存在。让我们举个例子说明这一过程的具体实现细节吧!

复制代码
    package com.mikyou.kotlin.generic
    
    
    fun main(args: Array<String>) {
    printNumberList(listOf(1, 2, 3, 4, 5))//传入List<Int>类型的数据
    }
    
    fun printNumberList(collection: Collection<*>) {
    val numberList = collection as List<Int>//强转成List<Int>
    println(numberList)
    }

运行输出

复制代码
    package com.mikyou.kotlin.generic
    
    
    fun main(args: Array<String>) {
    printNumberList(listOf("a", "b", "c", "d"))//传入List<String>类型的数据
    }
    
    fun printNumberList(collection: Collection<*>) {
    val numberList = collection as List<Int>
    //这里强转成List<Int>,并不会报错,输出正常,
    //但是需要注意不能默认把类型实参当做Int来操作,因为擦除无法确定当前类型实参,否则有可能出现运行时异常
    println(numberList)
    }

运行输出

如果我们把调用地方改成setOf(1,2,3,4,5)

复制代码
    fun main(args: Array<String>) {
    printNumberList(setOf(1, 2, 3, 4, 5))
    }
    
    fun printNumberList(collection: Collection<*>) {
    val numberList = collection as List<Int>
    println(numberList)
    }

运行输出

分析: 深入思考一下, 得到这样一个结果也不算奇怪, 我们了解泛型组件的内核机制, 其中泛型类的内态参数虽然在编译阶段被消除, 不会直接影响基类的表现。尽管我们不清楚某个列表集合具体存储的是什么类型的元素, 但至少能够判断这是一个List类型的集合而不是Set类型的集合, 因此后者肯定会触发异常处理流程。至于前者的情况, 尽管在运行时无法明确知道具体的内态参数是什么样的, 但可以通过基类进行推断: 只有当基类相同时, 内态参数是否匹配才会取决于实际情况。为此,Kotlin语言设计团队选择通过向编译器发出警告信号来提醒开发者注意潜在的问题。

注意: 避免采用这种写法可能导致安全隐患。由于编译器只会给出一个警告,并没有卡死函数返回值的情况发生。一旦默认将该变量作为强转的实参使用时,并且调用方提供的参数与指定的强转类型不符,则会导致错误。

复制代码
    package com.mikyou.kotlin.generic
    
    
    fun main(args: Array<String>) {
    printNumberList(listOf("a", "b", "c", "d"))
    }
    
    fun printNumberList(collection: Collection<*>) {
    val numberList = collection as List<Int>
    println(numberList.sum())
    }

运行输出

三、什么是reified实化类型参数函数?

经我们分析可知Kotlin和Java都面临泛型类型擦除的问题。特别地,在现代编程语言中,开发者们意识到Java擦除所带来的问题,并因此采取了相应的措施来解决这些问题。其中一项解决方案是通过inline函数来保证泛型类的类型实参在运行时能够保留这一关键操作,在Kotlin中将其称为"实化"(Reification),并为此提供相应的关键字"reified"来实现这一过程。

1、满足实化类型参数函数的必要条件

在定义泛型参数时,应当使用reified关键字进行修饰。

2、带实化类型参数的函数基本定义
复制代码
    inline fun <reified T> isInstanceOf(value: Any): Boolean = value is T

在上述示例中,我们可以认为类型形参T即为泛型函数isInstanceOf实现类型的参数。

3、关于inline函数补充一点

我们对inline函数大致了解。它的主要优势在于通过使其调用得到性能上的优化与提升。不过需要注意的是,在这里使用inline函数并不是为了性能方面的考虑。此外,在运行时它能够实现泛型函数类型实参的显式实化以便于获取类型实参的信息

四、实化类型参数函数的背后原理以及反编译分析

在Kotlin语言中,类型实化参数本质上属于语法层面的一种技巧。此时恰逢其会是一个揭示这些机制的最佳时机。不可否认的是内联函数在其中扮演了关键角色。若无内联函数则这项技巧将不复存在。

1、原理描述

众所周知内联函数是一种编译优化技术它将实现内联函数的字节码动态插入到目标代码中以提高程序运行效率。其原理正是基于这一机制具体表现为当调用带实化类型参数的函数时编译器能够识别此次调用中的具体类型参数从而生成相应的字节码并将其插入到对应的调用点位置上。这样做的好处在于编译器无需预先知道调用的具体类型信息即可高效地处理各种类型的函数调用因此避免了因泛型擦除而产生的性能损失。

2、reified的例子

带有显式类型参数的函数在应用中常见于Kotlin开发,在一些标准Kotlin库中采用Anko库(简化Android开发的Kotlin官方库)中的一个精简版startActivity函数。

复制代码
    inline fun <reified T: Activity> Context.startActivity(vararg params: Pair<String, Any?>) =
        AnkoInternals.internalStartActivity(this, T::class.java, params)

从上述实例可以看出,在声明一个被实现的类型参数T时,并非没有相关的限制条件;其中它带有明确的上界约束为Activity;这使得可以直接将其视为普通类型的实例进行使用

3、代码反编译分析

为了便于进行反编译分析,我们可以单独提取库中的那个函数代码,并命名为startActivityKt以便进一步分析

复制代码
    class SplashActivity : BizActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.biz_app_activity_welcome)
        startActivityKt<AccountActivity>()//只需这样就直接启动了AccountActivity了,指明了类型形参上界约束Activity
    }
    }
    
    inline fun <reified T : Activity> Context.startActivityKt(vararg params: Pair<String, Any?>) =
        AnkoInternals.internalStartActivity(this, T::class.java, params)

编译后关键代码

复制代码
    //函数定义反编译
     private static final void startActivityKt(@NotNull Context $receiver, Pair... params) {
      Intrinsics.reifiedOperationMarker(4, "T");
      AnkoInternals.internalStartActivity($receiver, Activity.class, params);//注意点一: 由于泛型擦除的影响,编译后原来传入类型实参AccountActivity被它形参上界约束Activity替换了,所以这里证明了我们之前的分析。
       }
    //函数调用点反编译
    protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      this.setContentView(2131361821);
      Pair[] params$iv = new Pair[0];
      AnkoInternals.internalStartActivity(this, AccountActivity.class, params$iv);
      //注意点二: 可以看到这里函数调用并不是简单函数调用,而是根据此次调用明确的类型实参AccountActivity.class替换定义处的Activity.class,然后生成新的字节码插入到调用点。
    }

让我们稍微在函数加点输出就会更加清晰

复制代码
    class SplashActivity : BizActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.biz_app_activity_welcome)
        startActivityKt<AccountActivity>()
    }
    }
    
    inline fun <reified T : Activity> Context.startActivityKt(vararg params: Pair<String, Any?>) {
    println("call before")
    AnkoInternals.internalStartActivity(this, T::class.java, params)
    println("call after")
    }

反编译后

复制代码
    private static final void startActivityKt(@NotNull Context $receiver, Pair... params) {
      String var3 = "call before";
      System.out.println(var3);
      Intrinsics.reifiedOperationMarker(4, "T");
      AnkoInternals.internalStartActivity($receiver, Activity.class, params);
      var3 = "call after";
      System.out.println(var3);
       }
    
       protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      this.setContentView(2131361821);
      Pair[] params$iv = new Pair[0];
      String var4 = "call before";
      System.out.println(var4);
      AnkoInternals.internalStartActivity(this, AccountActivity.class, params$iv);//替换成确切的类型实参AccountActivity.class
      var4 = "call after";
      System.out.println(var4);
       }

五、实化类型参数函数的使用限制

这里说的使用限制主要有两点:

1、Java调用Kotlin中的实化类型参数函数限制

明确指出Kotlin语言中的实化类型参数函数不具备在Java语言中被调用的能力。为了更好地理解这一现象,我们可以通过深入分析来探讨其中的原因。其背后的核心机制源于支持内立式的inline关键字,这一特性使得开发者能够在代码编译阶段直接嵌入相关操作而不需依赖外部工具或库的支持。然而,在Java中,则不具备直接支持普通内立式inline关键字的能力,这种缺失导致了相应的操作无法实现其预期效果。由此可见,在不同编程语言中实现特定功能时存在诸多限制

2、Kotlin实化类型参数函数的使用限制
  • 非实现类型的构造函数不得用于调用带实现了接口类型的函数。
  • 不得将实现了接口类型的参数用于创建该接口类型的实例对象。
  • 不得对实现接口类型的参数执行其伴生对象的方法的操作。
  • reified关键字仅标注内联函数而无法作用于类和属性。

**诚挚邀请您关注Kotlin开发者联盟!本联盟汇聚了丰富的内容资源,为您提供最新的Kotlin技术资讯。我们定期会从国外技术新闻中挑选一篇具有代表性的文章进行转介或分享。如果你也热爱Kotlin开发,并希望加入我们的开发团队,请随时联系我们!感谢您的关注与支持!~~~

**诚挚邀请您关注Kotlin开发者联盟!本联盟汇聚了丰富的内容资源,为您提供最新的Kotlin技术资讯。我们定期会从国外技术新闻中挑选一篇具有代表性的文章进行转介或分享。如果你也热爱Kotlin开发,并希望加入我们的开发团队,请随时联系我们!感谢您的关注与支持!~~~

Kotlin系列文章,欢迎查看:

Kotlin邂逅设计模式系列:

数据结构与算法系列:

翻译系列:

原创系列:

  • 深入解析Kotlin类型系统的方法
  • 探索如何提升Kotlin回调的功能性
  • 深入解读Kotlin 1.3新增特性——以内联类为例
  • 全面解析Kotlin 1.3中合同与协程功能
  • 体验Kotlin 1.3与Native结合的新鲜感
  • 攻克Kotlin泛型应用中的难点(实践篇)
  • 攻克Kotlin泛型应用中的难点(下篇)
  • 攻克Kotlin泛型应用中的难点(上篇)
  • Reified实化技术在Kotlin中的高级应用(下篇)
  • 了解Kotlin属性代理的核心机制
  • 剖析Kotlin中Sequences源码实现细节
  • 全面解析集合与函数式API设计原理(上篇)
  • 彻底解析Lambda编译为字节码的过程
  • 彻底解析Lambda表达式的实现机制
  • 扩展函数的实现技巧与应用方法
  • 顶层函数、中缀调用及解构声明的实践技巧
  • 优化函数调用策略的最佳实践方法
  • 理解变量与常量的基本使用原则
  • 掌握基础语法要点及实际应用技巧

Effective Kotlin翻译系列

Effective Kotlin系列

实战系列:

全部评论 (0)

还没有任何评论哟~