Back
Featured image of post 👨🏼‍💻关于 Kotlin 内联(值)类的一切

👨🏼‍💻关于 Kotlin 内联(值)类的一切

翻译文章 - Kotlin 内联类详解

本文是对发布在 Medium 上的文章 👨🏼‍💻 All About Kotlin Inline(Value) Class 的翻译。

请注意,虽然本翻译按照 CC BY-SA 4.0 分发,但没有原作者的授权,我本质上不享有翻译权。所以如果想转载本文,你首先应该取得原作者的同意。

大家好✋。在这篇文章中,我将细致入微地解释 Kotlin 内联类(Inline Class)的细节。


什么是内联类❓

译注:内联类(Inline Class)也叫值类(Value Class),Inline 是仅在 JVM 平台上的做法,实际上在 Kotlin/Native 平台,可以通过 struct 来实现值类

在某些情况下,我们创建 Wrapper 提供特定的业务逻辑。但这可能导致性能损失,因为 Wrapper 需要在运行时创建额外的对象。如果你封装的是原始类型,对性能的打击是毁灭性的。为了解决这种情况。Kotlin 引入了一种名为内联类(Inline Class)的新类。

使用内联类,不仅提供编译时类型安全,而且在运行时充当原始类型,节省了不必要的性能损失。它还提供了更清晰、更易于理解、更可读的代码结构。

性能损失在哪里? 🤔

https://www.javatpoint.com/stack-vs-heap-java

当创建原始变量时,它存储在 JVM 栈内存中。但是创建一个新对象时,它存储在堆内存中。在存储和使用对象时,操作将更加昂贵。可以查看链接(译注:原文的外部链接失效了)以了解堆栈的更多信息。此处的损失微不足道。但如果在项目视野考量,后者明显会产生额外开销。此外,作为程序员,不应该忘记我们需要更干净、更清晰、更高效地编写代码。定义原始类型一定比定义对象更高效。

因此,若在为原始类型编写包装类时使用 Inline 类。我们即可受益于包装类的好处,例如更灵活、可读性和类型安全,也不会损失性能。

https://quickbirdstudios.com/blog/kotlin-value-classes/

额外地,上图显示了做同一件事的 4 种方法,以及速度。


用例和限制

// Inline Class Definition
@JvmInline
value class Distance(val valueAsMeter: Int)
  • value 关键字足以定义一个内联类。
  • 如果编译目标包括 JVM,还需要添加 @JvmInline 注解,否则无法通过编译。
@JvmInline
value class Distance(val valueAsMeter: Int, val otherValue: String) // ❌

@JvmInline
value class Distance1() // ❌

@JvmInline
value class Distance2(val valueAsMeter: Int) {
  constructor() // ❌
}

@JvmInline
value clas Distance3(var valueAsMeter: Int) // ❌
  • 只能提供主构造器。
  • 和 data class 类似,需要定义构造器参数
  • 可以在主构造器中定义有且仅有一个只读属性 (var 不行,只能 val)
@JvmInline
value class Distance1(val valueAsMeter: Int): Measure() // ❌

class Measure: Distance1() // ❌

interface Calculate {
  fun calculate()
}

// 编译通过
@JvmInline
value class Distance2(val valueAsMeter: Int) : Calculate {
  override fun calculate() {
    TODO("")
  }
}
  • 内联类不能继承或被继承
  • 内联类可以实现接口

Mangling

由于原文比较难翻译,这里解释下,Mangling 可以理解为去语法糖,或者真正发生的事情

Mangling

上方图片中有两个函数。一个采用值类参数,另一个采用 int 类型的参数。可以看到反编译后的代码有一点不同。值类首先被编译为原始类型。这可能会导致某些错误。就比如这里的两个函数,如果不做任何处理,会导致编译错误,因为类型签名和函数名一样。为了避免这些错误,在编译时,内联类的函数名被打乱为 getPrice<hashCode>

如果想从 Java 中调用第一个函数,我们需要禁用 Mangling 行为。可以通过添加 @JvmName 注解来做到这一点。我们给出的 name 参数将是编译后的函数名。可以用该名称从 Java 中调用。


类型别名(typealias) vs 内联类

内联类与类型别名非常相似,但也存在差异。尽管两者似乎都创建了新类型,但类型别名仅仅替换类型的名称。他们的使用场景是不同的,例如,我们可以考虑为 String 类型分配一个不同的名称。在使用内联类时,我们创建了一个新类型。 这为我们提供了更清晰、更易理解的代码结构,以及类型安全。

typealias NameTypeAlias = String

@JvmInline
value class NameInlineClass(val s: String)

fun accpetString(s: String) {}
fun accpetNameTypeAlias(s: NameTypeAlias) {}
fun accpetNameInlineClass(s: NameInlineClass) {}

fun main() {
  val nameAlias: NameTypeAlias = ""
  val nameInlineClass: NameInlineClass = NameInlineClass("")
  val string = ""

  acceptString(nameAlias) // OK
  acceptString(nameInlineClass) // ❌
  acceptString(nameInlineClass.s) // OK

  // 反之亦然
  acceptNameTypeAlias(string) // OK
  acceptNameInlineClass(string) // ❌
}

结论

在本文中,我们尝试了解有关内联类的每一个细节。内联类的使用提供了一些好处,可以更清晰易读地编写代码,并在性能和类型安全方面更具优势。下一篇文章见👋👋。

comments powered by Disqus