Kotlin Vocabulary | 内联类 inline class
fun myStringResUsage(@StringRes string: Int){ }
// 错误: 需要 String 类型的资源
myStringResUsage(1)
扩展阅读:
利用注释改进代码检查
https://developer.android.google.cn/studio/write/annotations
inline class DoggoId(val id: Long)
data class Doggo(val id: DoggoId, … )
// 用法
val goodDoggo = Doggo(DoggoId(doggoId), …)
fun pet(id: DoggoId) { … }
内联类
https://kotlinlang.org/docs/reference/inline-classes.html
内联
内联类的唯一作用是成为某种类型的包装,因此 Kotlin 对其施加了许多限制:
最多一个参数 (类型不受限制)
没有 backing fields 不能有 init 块 不能继承其他类
从接口继承 具有属性和方法
interface Id
inline class DoggoId(val id: Long) : Id {
val stringId
get() = id.toString()
fun isValid()= id > 0L
}
⚠️ 注意: Typealias 看起来与内联类相似,但是类型别名只是为现有类型提供了可选名称,而内联类则创建了新类型。
backing fields
https://kotlinlang.org/docs/reference/properties.html#backing-fields
声明对象 —— 包装还是不包装?
val doggo1 = DoggoId(1L)
val doggo2 = DoggoId(2L)
doggo1 == doggo2 — doggo1 和 doggo2 都没有被装箱
doggo1.equals(doggo2) — doggo1 是原生类型但是 doggo2 被装箱了
工作原理
让我们实现一个简单的内联类:
interface Id
inline class DoggoId(val id: Long) : Id
完整的反编译代码
https://gist.github.com/florina-muntenescu/2a9d07edbe8fc701bfcb32143bf2a090#file-ids-decompiled-java
原理 —— 构造函数
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
public final class DoggoId implements Id {
// $FF: synthetic method
private DoggoId(long id) {
this.id = id;
}
public static long constructor_impl/* $FF was: constructor-impl*/(long id) {
return id;
}
}
私有合成构造函数 DoggoId(long id) 公共构造函数
val myDoggoId = DoggoId(1L)
// 反编译过的代码
static final long myDoggoId = DoggoId.constructor-impl(1L);
如果尝试使用 Java 创建 Doggo ID,则会收到一个错误:
DoggoId u = new DoggoId(1L);
// 错误: DoggoId 中的 DoggoId() 方法无法使用 long 类型
您无法在 Java 中实例化内联类。
有参构造函数是私有的,第二个构造函数的名字中包含了一个 "-",其在 Java 中为无效字符。这意味着无法从 Java 实例化内联类。
原理 —— 参数用法
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
public final class DoggoId implements Id {
private final long id;
public final long getId() {
return this.id;
}
// $FF: synthetic method
@NotNull
public static final DoggoId box_impl/* $FF was: box-impl*/(long v) {
return new DoggoId(v);
}
}
通过 getId() 作为原生类型; 作为一个对象: box_impl 方法会创建一个 DoggoId 实例。
如果在可以使用原生类型的地方使用内联类,则 Kotlin 编译器将知道这一点,并会直接使用原生类型:
fun walkDog(doggoId: DoggoId) {}
// 反编译后的 Java 代码
public final void walkDog_Mu_n4VY(**long** doggoId) { }
当需要一个对象时,Kotlin 编译器将使用原生类型的包装版本,从而每次都创建一个新的对象,例如:
fun pet(doggoId: DoggoId?) {}
// 反编译后的 Java 代码
public static final void pet_5ZN6hPs/* $FF was: pet-5ZN6hPs*/(@Nullable InlineDoggoId doggo) {}
集合
// 反编译后的 Java 代码
doggos = CollectionsKt.listOf(DoggoId.box-impl(myDoggoId));
fun <T> listOf(element: T): List<T>
因为此方法需要一个对象,所以 Kotlin 编译器将原生类型装箱,以确保使用的是对象。
基类
fun handleId(id: Id) {}
fun myInterfaceUsage() {
handleId(myDoggoId)
}
// 反编译后的 Java 代码
public static final void myInterfaceUsage() {
handleId(DoggoId.box-impl(myDoggoId));
}
因为这里需要的参数类型是超类: Id,所以这里使用了装箱的实现。
原理 —— 相等性检查
Kotlin 编译器会在所有可能的地方使用非装箱类型参数。为了达到这个目的,内联类有三个不同的相等性检查的方法的实现: 重写的 equals 方法和两个自动生成的方法:
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
public final class DoggoId implements Id {
public static boolean equals_impl/* $FF was: equals-impl*/(long var0, @Nullable Object var2) {
if (var2 instanceof DoggoId) {
long var3 = ((DoggoId)var2).unbox-impl();
if (var0 == var3) {
return true;
}
}
return false;
}
public static final boolean equals_impl0/* $FF was: equals-impl0*/(long p1, long p2) {
return p1 == p2;
}
public boolean equals(Object var1) {
return equals-impl(this.id, var1);
}
}
DoggoId.equals-impl(doggo1, DoggoId.box-impl(doggo2))
DoggoId.equals-impl0(doggo1, doggo2)
覆盖使用原生类型和内联类作为参数的函数
定义一个方法时,Kotlin 编译器允许使用原生类型和不可空内联类作为参数:
fun pet(doggoId: Long) {}
fun pet(doggoId: DoggoId) {}
// 反编译的 Java 代码
public static final void pet(long id) { }
public final void pet_Mu_n4VY(long doggoId) { }
在 Java 中使用内联类
我们已经讲过,不能在 Java 中实例化内联类。那可不可以使用呢?
✅ 可以将内联类传递给 Java 函数
void myJavaMethod(DoggoId doggoId){
long id = doggoId.getId();
}
如果我们将内联类声明为顶层对象,就可以在 Java 中以原生类型获得它们的引用,如下:
// Kotlin 的声明
val doggo1 = DoggoId(1L)
// Java 的使用
long myDoggoId = GoodDoggosKt.getU1();
fun pet(doggoId: DoggoId) {}
// Java
void petInJava(doggoId: DoggoId){
pet(doggoId)
// 编译器报错: pet(long) cannot be applied to pet(DoggoId) (pet(长整形) 不能用于 pet(DoggoId))
}
fun pet(doggoId: DoggoId) {}
// Java
void petInJava(doggoId: DoggoId){
pet(doggoId.getId)
}
fun pet(doggoId: Long) {}
fun pet(doggoId: DoggoId) {}
// Java
TestInlineKt.pet(1L);
Error: Ambiguous method call. Both pet(long) and pet(long) match
内联类: 使用还是不使用,这是一个问题
类型安全可以帮助我们写出更健壮的代码,但是经验上来说可能会对性能产生不利的影响。内联类提供了一个两全其美的解决方案 —— 没有额外消耗的类型安全。所以我们就应该总是使用它们吗?
内联类带来了一系列的限制,使得您创建的对象只能做一件事: 成为包装器。这意味着未来,不熟悉这段代码的开发者,也没法像在数据类中那样,可以给构造函数添加参数,从而导致类的复杂度被错误地增加。
在性能方面,我们已经看到 Kotlin 编译器会尽其所能使用底层类型,但在许多情况下仍然会创建新对象。
在 Java 中使用内联类时仍然有诸多限制,如果您还没有完全迁移到 Kotlin,则可能会遇到无法使用的情况。
最后,这仍然是一项实验性功能。它是否会发布正式版,以及正式版发布时,它的实现是否与现在相同,都还是未知数。
因此,既然您了解了内联类的好处和限制,就可以在是否以及何时使用它们的问题上做出明智的决定。
推荐阅读