kotlin内联课程–他们如何工作,当你应该使用它们时

kotlin内联课程-他们如何工作,当你应该使用它们时

Kotlin将内联课程推出,版本1.3作为一个 实验特征。您应该意识到他们的实施仍然可以在未来的发布中改变,但现在已经很好地了解它们。内联类添加了一个简单的工具,我们可以使用它来包装其他类型而不通过额外的堆分配添加运行时开销。在本文中,我们希望了解Kotlin工作中的内联类以及使用它们有意义。

在项目中启用内联课程

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

第一次浏览内联课程

Inline classes are pretty straightforward. To make a class inlined, you just add the inline keyword to your class:

inline 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 can't wrap multiple values in one inline class. It is also prohibited to have init blocks in inline classes, and you cannot have properties with backing fields. Inline classes can have simple computable properties, however, which we will see later in this article.

At runtime, the wrapped type of an inline 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 can do that. That exactly is the great selling point for inline classes in Kotlin: When you inline a class, the class itself won't be used in the byte code unless it's absolutely necessary. Inlining classes drastically reduces space overhead at runtime.

运行时表示

At runtime, an inline class can 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 inline 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 byte code represented as Java code to show how an inline 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 can see box_impl and unbox_impl functions which, as you might expect, are used for boxing purposes. Now let's see how this inline class wrapper gets utilized when we use the inline class in our code.

使用内联课程

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

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

In this snippet, a WrappedInt is being created and passed to a function which prints its wrapped value. The corresponding byte 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 inlined = 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 inline 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 byte 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 inline classes, so when do we need them? The Kotlin docs cite a rule of thumb which says:

每当用作另一种类型时,盒装盒装盒装类。

例如,当您将内联类作为通用类型或空缺类型时,拳击会发生拳击:

inline 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 byte 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 can 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 can help you mitigate that risk, and therefore inline classes are an awesome tool:

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

参数列表令人困惑,并且在调用者站点上,编译器不允许不匹配。前面描述的可能是使用内联类的最常见场景。它们为您提供简单的类型安全的包装,而不会引入额外的堆分配。对于这些情况,尽可能优选内联课程。然而,内联类甚至更聪明,下一个用例演示。

在没有额外空间的情况下处理状态

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 can create an inline 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:

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

As you can see, the getParsableNumber method returns an instance of our inline 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 byte 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 can 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:

inline 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 can we do to make the extension shown above only available for certain strings? Yep, inline classes can help with that:

缩小扩展范围与内联类

inline 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 inline class UInt @PublishedApi internal constructor(@PublishedApi internal val data: Int) : Comparable<UInt>

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

结论

内联类是一个很好的工具,可用于减少包装器类型的堆分配,并帮助我们解决不同类型的问题。但是,请注意某些方案,例如使用内联类作为可用类型需要拳击。由于内联课程仍然是实验的,因此您必须希望您的代码因其行为的变化而导致的未来发布。请记住这一点。然而,我认为它是有效的,可以立即开始使用它们。

如果您想了解有关Kotlin的美丽功能的更多信息,我推荐这本书 kotlin在行动中 对你而且还要指导你到我的 其他文章 🙂

可以找到关于内联类的另一个伟大文章 这里 (by Dave Leeds).


5 thoughts on “kotlin内联课程–他们如何工作,当你应该使用它们时

发表评论

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