初看内联类
内联类非常的简单,您只需要在类的前面加上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阶段,因此您必须接受未来代码会由于其行为的更改而在将来的版本中失效。这一点我们要记住。不过,我认为现在就开始使用它们是有合理的。
推荐阅读
扫码关注我
一起感悟技术魅力
点分享
点收藏
点点赞
点在看