Kotlin内联课程–它们如何工作以及何时应使用它们

Kotlin内联课程-它们如何工作以及何时应使用它们

Kotlin在版本1.3中引入了内联类 实验特征。您应该意识到,它们的实现在将来的发行版中仍会更改,但是现在已经是学习它们的好时机。内联类添加了一个简单的工具,我们可以使用它包装其他类型,而无需通过其他堆分配来增加运行时开销。在本文中,我们希望了解Kotlin中的内联类如何工作以及何时使用它们。

在项目中启用内联类

要在项目中启用内联类,您只需要使用Kotlin版本 > 1.3 which adds the 排队 keyword to the language. Since 排队 classes are still experimental, your IDE will display related warnings when you make use of them. The warnings 能够 be disabled with explicit compiler flags as described 这里.

乍看内联课程

Inline classes are pretty straightforward. To make a class 排队d, you just add the 排队 keyword to your class:

排队 class WrappedInt(val value: Int)

Inline classes have some, more or less apparent, restrictions though: It's required to specify precisely one property in the primary constructor, as shown with value. You 能够't wrap multiple values in one 排队 class. It is also prohibited to have init blocks in 排队 classes, and you 能够not have properties with backing fields. Inline classes 能够 have simple computable properties, however, which we will see later in this 文章.

At runtime, the wrapped type of an 排队 class will be used without its wrapper whenever possible. This is similar to Java's boxed types like Integer or Boolean, which will be represented as their corresponding primitive type whenever the compiler 能够 do that. That exactly is the great selling point for 排队 classes in Kotlin: When you 排队 a class, the class itself won't be used in the 通过 te code unless it's absolutely necessary. Inlining classes drastically reduces space overhead at runtime.

运行时表示

At runtime, an 排队 class 能够 be represented as both, the wrapper type and the underlying type. As mentioned in the previous paragraph, the compiler prefers using the underlying (wrapped) type of an 排队 class to optimize the code as much as possible. This is similar to boxing between int and Integer. In certain situations, however, the compiler needs to use the wrapper itself, so it will be generated during compilation:

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
}

This snippet shows the simplified 通过 te code represented as Java code to show how an 排队 class looks like. Along with some obvious stuff like the value field and its getter, the constructor is private, and new objects will rather be created through constructor_impl which does not actually use the wrapper type but only returns the passed in underlying type. Finally, you 能够 see box_impl and unbox_impl functions which, as you might expect, are used for boxing purposes. Now let's see how this 排队 class wrapper gets utilized when we use the 排队 class in our code.

使用内联类

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

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

In this snippet, a WrappedInt is being created and passed to a function which prints its wrapped value. The corresponding 通过 te code, again as Java code, looks as follows:

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

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

In the compiled code, no instance of WrappedInt is created. Although the static constructor_impl is used, it just returns an int which is then passed to the take function that also does not know anything about the type of the 排队 class which we originally had in our source code. 请注意,接受内联类参数的函数名称会用字节码中生成的哈希码扩展。这样,它们可以与接受基础类型作为参数的重载函数区分开:

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

To make both take methods available in the JVM 通过 te code and avoid signature clashes, the compiler renames the first one to something like take-hqTGqkw. Note that the example above does show a "_" rather than a "-"因为Java不允许方法名称包含破折号,这也是为什么 不能从Java调用接受内联类的方法.

内联课程的拳击

We saw earlier that box_impl and unbox_impl functions are created for 排队 classes, so when do we need them? The Kotlin docs cite a rule of thumb which says:

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

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

排队 class WrappedInt(val value: Int)

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

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

In this code we modified the take function to take a nullable WrappedInt and print the underlying type if the argument is not 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)));
}

In the 通过 te code, take now does not accept the underlying type directly anymore. It has to work with the wrapper type instead. When printing its content, unbox_impl is invoked. On the caller site, we 能够 see that box_impl is used to create a boxed instance of WrappedInt.

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

用例内联类

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

使用内联类更好地打字

想象一下API中的身份验证方法,如下所示:

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

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

auth("12345", "user1")

Since both parameters are of type String, you may mess up their order which gets even more likely with an increasing number of arguments of course. Wrappers around these types 能够 help you mitigate that risk, and therefore 排队 classes are an awesome tool:

排队 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"))
}

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

处理状态,无需额外空间

Let's consider a method that takes a numeric string and parses it into a BigDecimal while also adjusting its scale:

/**
 * 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 类从该函数返回一对值。这些方法虽然显然会分配额外的空间,但仍然是有效的,在特殊情况下应避免使用。 内联类可以帮助您。我们已经注意到,内联类不能具有带有后备字段的多个属性。但是,他们 能够 have simple calculated members in the form of properties and functions. We 能够 create an 排队 class for our use case that wraps the original String and provides a method or a property that, on demand, parses our value. For the user, this will look like a normal data wrapper around two types while it does not add any runtime overhead in the best case:

排队 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)
}

As you 能够 see, the getParsableNumber method returns an instance of our 排队 class which provides two properties original (the underlying type) and parsed (the calculated parsed number). That's an interesting use case that is worth observing on a 通过 te code level again:

更多字节码

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
}

The generated wrapper class ParsableNumber pretty much looks like the earlier shown WrappedInt class. One important difference, however, is the getParsed_impl function, which represents our computable property parsed. As you 能够 see, the function is implemented as a static function that takes a string and returns a BigDecimal. So how is this being utilized in the caller code?

@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);
}

As expected, getParsableNumber does not have any reference to our wrapper type. It simply returns the String without introducing any new type. In the main, we see that the static getParsed_impl is used to parse the given String into a BigDecimal. Again, no usage of ParsableNumber.

缩小扩展功能的范围

A common issue with extension functions is that they may pollute your namespace if defined on general types like String. As an example, you may want to have an extension function that converts a JSON string into a corresponding type:

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

To convert a given string into some data holder JsonData, you would then do:

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

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

"whatever".asJson<JsonData> //will fail

This code will fail since the String does not contain valid JSON data. What 能够 we do to make the extension shown above only available for certain strings? Yep, 排队 classes 能够 help with that:

内联类缩小扩展范围

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

When we introduce a wrapper for strings that holds JSON data and change the extension to using a JsonString receiver accordingly, the issue described above has been solved. The extension won't anymore appear on any arbitrary String but only those we consciously wrapped in a JsonString.

无符号类型

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

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

As you 能够 see, the UInt class is defined as an unsigned class that wraps a normal signed integer data. You 能够 learn more about this feature in the corresponding 保持.

结论

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

如果您想进一步了解Kotlin的精美功能,建议您阅读这本书 行动中的科特林 给你,也想带你去我的 其他文章 ðŸ™,

可以找到关于内联类的另一篇很棒的文章 这里 (由Dave Leeds撰写)。


关于6条想法“Kotlin内联课程–它们如何工作以及何时应使用它们

发表评论

您的电子邮件地址不会被公开。 必需的地方已做标记 *