代码编织梦想

初看内联类

内联类非常的简单,您只需要在类的前面加上inline关键字就可以:

inline class WrappedInt(val value: Int)

内联类有一些或多或少明显的限制:需要在主构造函数中精确指定一个属性,如value所示。 您不能在一个内联类中包装多个值。 内联类中也禁止包含init块,并且不能具有带有幕后字段的属性。 内联类可以具有简单的可计算属性,但是我们将在本文后面看到。

在运行时,将尽可能使用内联类的包装类型而不使用其包装。 这类似于Java的框式类型,例如Integer或Boolean,只要编译器可以这样做,它们就会被表示为它们对应的原始类型。 这正是Kotlin中内联类的一大卖点:内联类时,除非绝对必要,否则类本身不会在字节码中使用。 内联类大大减少了运行时的空间开销。

运行时

在运行时,可以将内联类表示为包装类型和基础类型。 如前一段所述,编译器更喜欢使用内联类的基础(包装)类型来尽可能地优化代码。 这类似于int和Integer之间的装箱。 但是,在某些情况下,编译器需要使用包装器本身,因此它将在编译期间生成:

public final class WrappedInt {
   private final int value;

   public final int getValue() { return this.value; }

   // $FF: synthetic method
   private WrappedInt(int value) { this.value = value; }

   public static int constructor_impl(int value) { return value; }

   // $FF: synthetic method
   @NotNull
   public static final WrappedInt box_impl(int v) { return new WrappedInt(v); }

   // $FF: synthetic method
   public final int unbox_impl() { return this.value; }

   //more Object related implementations
}

此代码段显示了内联类简化的Java字节码。 除了一些显而易见的东西,例如value字段及其getter之外,构造函数是私有的,而新对象将通过Constructor_impl创建,该对象实际上并不使用包装器类型,而仅返回传入的基础类型。 最后,您可以看到box_impl和unbox_impl函数,可能如您所期望的,它们的目的在于拆装箱的操作。 现在,让我们看看在代码中如何使用内联类。

使用内联类

fun take(w: WrappedInt) {
    println(w.value)
}

fun main() {
    val inlined = WrappedInt(5)
    take(inlined)
}

在此代码段中,正在创建WrappedInt并将其传递给打印其包装值的函数。 相应的Java字节码,如下所示:

public static final void take_hqTGqkw(int w) {
    System.out.println(w);
}

public static final void main() {
    int inlined = WrappedInt.constructor_impl(5);
    take_hqTGqkw(inlined);
}

在已编译的代码中,没有创建WrappedInt实例。 尽管使用了静态的builder_impl函数,它只是返回一个int值,然后将其传递给take函数,该函数也对我们最初在源代码中拥有的内联类的类型一无所知。 请注意,接受内联类参数的函数名称会用字节码中生成的哈希码扩展。 这样,它们可以与接受基础类型作为参数的重载函数区分开:

fun take(w: WrappedInt) = println(w.value)
fun take(v: Int) = println(v.value)

为了使这两种take方法在JVM字节码中可用并避免签名冲突,编译器将第一个方法重命名为take-hqTGqkw之类的东西。 注意,上面的示例确实显示了“ _”而不是“-”,因为Java不允许方法名称包含破折号,这也是为什么不能从Java调用接受内联类的方法的原因。

内联类的装箱

前面我们看到过,box_impl和unbox_impl函数是为内联类创建的,那么什么时候需要它们? Kotlin的文档引用了一条经验法则:

内联类在用作其他类型时会被装箱

例如,当您将内联类用作通用类型或可为空的类型时,就会发生装箱:

inline class WrappedInt(val value: Int)

fun take(w: WrappedInt?) {
    if (w != null) println(w.value)
}

fun main() {
    take(WrappedInt(5))
}

在此代码中,我们修改了take函数以采用可为空的WrappedInt,并在参数不为null时显示基础类型。

public static final void take_G1XIRLQ(@Nullable WrappedInt w) {
    if (Intrinsics.areEqual(w, (Object)null) ^ true) {
        int var1 = w.unbox_impl();
        System.out.println(var1);
    }
}

public static final void main() {
    take_G1XIRLQ(WrappedInt.box_impl(WrappedInt.constructor_impl(5)));
}

在字节码中,take函数现在不再直接接受基础类型。 它必须改为使用装箱类型。 打印其内容时,将调用unbox_impl。 在调用的地方,我们可以看到box_impl用于创建WrappedInt的装箱实例。

显然,我们希望尽可能避免装箱。 请记住,内联类以及原始类型的特定用法通常都依赖于此技术,因此可能必须重新考虑是否该这么做。

使用案例

我们看到内联类具有巨大的优势:在最佳情况下,由于避免了额外的堆分配,它们可以大大减少运行时的开销。 但是我们什么时候适合使用这种包装类型呢?

更好的区分类型

假如有一个身份验证方法API,如下所示:

fun auth(userName: String, password: String) { println("authenticating $userName.") }

在一个美好的世界中,每个人都会用用户名和密码来称呼它。 但是,某些用户将以不同的方式调用此方法并不困难:

auth("12345", "user1")

由于这两个参数均为String类型,因此您可能会弄乱它们的顺序,当然,随着参数数量的增加,这种顺序的可能性更大。 这些类型的包装类型可以帮助您减轻这种风险,因此内联类是一个很棒的工具:

inline class Password(val value: String)
inline class UserName(val value: String)

fun auth(userName: UserName, password: Password) { println("authenticating $userName.")}

fun main() {
    auth(UserName("user1"), Password("12345"))
    //does not compile due to type mismatch
    auth(Password("12345"), UserName("user1"))
}

参数列表变得越来越混乱,并且在调用方来看,编译器不允许出现不匹配的情况。 先前描述的可能是使用内联类的最常见方案。 它们为您提供了简单的类型安全的包装器,而无需引入其他堆分配。 对于这些情况,应尽可能选择内联类。 但是,内联类甚至可以更智能,这将在下一个用例中演示。

无需额外空间

让我们考虑一个采用数字字符串并将其解析为BigDecimal并同时调整其比例的方法:

/**
 * parses string number into BigDecimal with a scale of 2
 */
fun parseNumber(number: String): BigDecimal {
    return number.toBigDecimal().setScale(2, RoundingMode.HALF_UP)
}

fun main() {
    println(parseNumber("100.12212"))
}

该代码非常简单,可以很好地工作,但是一个要求可能是您需要以某种方式跟踪用于解析该数字的原始字符串。 为了解决这个问题,您可能会创建一个包装类型,或者使用现有的Pair类从该函数返回一对值。 这些方法虽然显然会分配额外的空间,但仍然是有效的,在特殊情况下应避免使用。 内联类可以帮助您。 我们已经注意到,内联类不能具有带有幕后字段的多个属性。 但是,它们可以具有属性和函数形式的简单计算成员。 我们可以为我们的用例创建一个内联类,该类包装原始的String并提供按需分析我们的值的方法或属性。 对于用户而言,这看起来像是围绕两种类型的普通数据包装器,而在最佳情况下它不会增加任何运行时开销

inline class ParsableNumber(val original: String) {
    val parsed: BigDecimal
        get() = original.toBigDecimal().setScale(2, RoundingMode.HALF_UP)
}

fun getParsableNumber(number: String): ParsableNumber {
    return ParsableNumber(number)
}

fun main() {
    val parsableNumber = getParsableNumber("100.12212")
    println(parsableNumber.parsed)
    println(parsableNumber.original)
}

如您所见,getParsableNumber方法返回我们内联类的实例,该实例提供原始(基础类型)和已分析(计算的已分析数量)两个属性。 这是一个有趣的用例,值得再次在字节码级别上观察:

public final class ParsableNumber {
   @NotNull
   private final String original;

   @NotNull
   public final String getOriginal() { return this.original; }

   // $FF: synthetic method
   private ParsableNumber(@NotNull String original) {
      Intrinsics.checkParameterIsNotNull(original, "original");
      super();
      this.original = original;
   }

   @NotNull
   public static final BigDecimal getParsed_impl(String $this) {
      BigDecimal var10000 = (new BigDecimal($this)).setScale(2, RoundingMode.HALF_UP);
      Intrinsics.checkExpressionValueIsNotNull(var10000, "original.toBigDecimal().…(2, RoundingMode.HALF_UP)");
      return var10000;
   }

   @NotNull
   public static String constructor_impl(@NotNull String original) {
      Intrinsics.checkParameterIsNotNull(original, "original");
      return original;
   }

   // $FF: synthetic method
   @NotNull
   public static final ParsableNumber box_impl(@NotNull String v) {
      Intrinsics.checkParameterIsNotNull(v, "v");
      return new ParsableNumber(v);
   }

   // $FF: synthetic method
   @NotNull
   public final String unbox_impl() { return this.original; }

    //more Object related implementations
}

生成的包装类ParsableNumber几乎类似于前面显示的WrappedInt类。 但是,一个重要的区别是getParsed_impl函数,该函数表示已解析的可计算属性。 如您所见,该函数被实现为静态函数,该静态函数接受字符串并返回BigDecimal。 那么在调用者代码中如何利用呢?

@NotNull
public static final String getParsableNumber(@NotNull String number) {
    Intrinsics.checkParameterIsNotNull(number, "number");
    return ParsableNumber.constructor_impl(number);
}

public static final void main() {
    String parsableNumber = getParsableNumber("100.12212");
    BigDecimal var1 = ParsableNumber.getParsed_impl(parsableNumber);
    System.out.println(var1);
    System.out.println(parsableNumber);
}

不出所料,getParsableNumber没有引用我们的包装类型。 它只是返回String而不引入任何新类型。 在主体中,我们看到静态的getParsed_impl用于将给定的String解析为BigDecimal。 同样,不使用ParsableNumber。

缩小扩展函数的范围

扩展函数的一个常见问题是,如果在诸如String之类的常规类型上进行定义,它们可能会污染您的命名空间。 例如,您可能需要一个扩展函数,将JSON字符串转换为相应的类型:

inline fun <reified T> String.asJson() = jacksonObjectMapper().readValue<T>(this)

要将给定的字符串转换为数据JsonData,您可以执行以下操作:

val jsonString = """{ "x":200, "y":300 }"""
val data: JsonData = jsonString.asJson()

但是,扩展功能也可用于表示其他数据的字符串,尽管可能没有多大意义:

"whatever".asJson<JsonData> //将会失败

由于字符串不包含有效的JSON数据,因此此代码将失败。 我们该怎么做才能使上面显示的扩展名仅适用于某些字符串? 不错,您需要的是内联类:

缩小扩展范围

inline class JsonString(val value: String)
inline fun <reified T> JsonString.asJson() = jacksonObjectMapper().readValue<T>(this.value)

当我们引入用于保存JSON数据的字符串的包装器并相应地将扩展名更改为使用JsonString接收器时,上述问题已得到解决。 该扩展名将不再出现在任何任意String上,而是仅出现在我们有意识地包装在JsonString中的那些字符串上。

无符号类型

当查看版本1.3中添加到语言中的无符号整数类型时,内联类的另一个很好的案例就变得显而易见了,这也是一个实验功能:

public inline class UInt @PublishedApi internal constructor(@PublishedApi internal val data: Int) : Comparable<UInt>

如您所见,UInt类被定义为包装常规的带符号整数数据的无符号类。

总结

内联类是一个很棒的工具,可用于减少包装类型的堆分配,并帮助我们解决各种问题。 但是,请注意,某些情况(例如将内联类用作可空类型)会进行装箱。 由于内联类仍处于Alpha阶段,因此您必须接受未来代码会由于其行为的更改而在将来的版本中失效。 这一点我们要记住。 不过,我认为现在就开始使用它们是有合理的。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接: https://blog.csdn.net/codeyanbao/article/details/111084434

Kotlin入门知识续-爱代码爱编程

使用"in"检查集合和区间的成员** 使用in来检查一个值是否在区间中,或者是它的逆运算,!n来检查这个值是否不在区间中, fun main(){ println(idLetter('a')) } fun idLetter(c:Char)=c in 'a'..'z' || c in 'A'..'Z' 区间不仅限于字符,也支持比较操作的任意类

Kotlin新手入坑:DSL构建专有的语法结构...|...ST-爱代码爱编程

DSL构建专有的语法结构 前言概述示例使用 抓住今天,尽可能少的信赖明天。 喝汤能补 (* ^ ▽ ^ *) 前言   该文章作为学习交流,如有错误欢迎各位大佬指正 (* ^ ▽ ^ *) 自身技能 (1)已具备计算机的基本知识本文简介 主要讲解:DSL的概念,kotlin中如何实现该语法。概述   DSL的全称是领域特定语言(Doma

kotlin学习(1) 认识kotlin中的关键字和基本用法-爱代码爱编程

kotlin kotlin 是一门可以运行在jvm上的语言, 兼容java的代码。 和java的谨慎保守不同, kotlin在编码规范上进行了大量的改进, 拥有了许多java语言不支持的特性, 并且kotlin还是安卓开发的优先语言, 所以值得学习。 val 和var kotlin 声明属性变量可以使用这两个关键字来设置, 其中 val a:Int

Kotlin协程介绍-爱代码爱编程

前言 1. Kotlin协程是啥? 2. Kotlin协程具体介绍   2.0 Kotlin协程的优点   2.1 添加依赖   2.2 启动协程   2.3 协程作用域(CoroutineScope)   2.4 Job(作业)   2.5 CoroutineDispatcher(调度器)   2.6 suspen

Java与Kotlin代码的转换-爱代码爱编程

Java与Kotlin代码的转换 Java代码转Kotlin Java代码转Kotlin 利用AndroidStudio进行代码转化。不足点:只能按照基本的语法进行转换,不会自动应用Kotlin各种特性,这些特性还是需要手动去完成。 转化一段java代码:直接复制一段Java代码,然后复制到Kotlin文件进行粘贴,AndroidStud

[实战] Android 发短信 - SMS-爱代码爱编程

短信有两种方式:一种是获得授权然后直接发送,pendingIntent监控结果,二是交给系统调用默认软件预填。 一. 短信-获得授权,直接发送 1. AndroidManifest.xml 注册授权   <uses-permission android:name="android.permission.SEND_SMS"/>  

Kotlin专题「二十」:内联类(Inline classes)-爱代码爱编程

前言:生活不是等暴风雨过去,而是学会在风雨中跳舞。 一、概述   无论你是编写执行在云端的数据流程还是低功耗手机的应用程序,大多数的开发者都希望他们的代码能快速运行。现在,Kotlin 最新实验性的特性内联类允许创建我们想要的数据类型,并且还不会损失我们需要的性能。 比如在管理系统中有这样一个需求: 向新用户发送电子邮件 - 在注

[译]记一次kotlin官方文档翻译的pr(内联类)-爱代码爱编程

简述: 这几天突然没更新文章了,可能有的小伙伴认为寒冬将至,是不是认为我跑路了(哈哈,确实不是哈,这几天感冒挺厉害的,再加上前几天连续熬夜写文章,感觉快扛不住了,所以暂时休息停更了一周。这不这篇内联类官网文档的翻译,已经拖

[译]kotlin中内联类的自动装箱和高性能探索(二)_熊喵先生的博客-爱代码爱编程

翻译说明: 原标题: Inline Classes and Autoboxing in Kotlin 原文地址: https://typealias.com/guides/inline-classes-and-autob

Kotlin 1.5 新特性 Inline classes,还不了解一下?-爱代码爱编程

kotlin 1.5 中的 Inline classes 我在CSDN的第一篇文章就是介绍的 Kotlin1.4 的新特性,如今在写了200多篇原创之后,终于可以写Kotlin 1.5 的新特性了 如果你正在使用Android Studio 4.2.0 、IntelliJ IDEA 2020.3 或更高的版本,近期就会收到 Kotlin