Kotlin Vocabulary | 内联类 inline class

* 特定条件和状况 这篇博客描述了一个 Kotlin 试验性功能,它还在调整之中。本文基于 Kotlin 1.3.50 撰写。

类型安全帮助咱们防止出现错误以及避免回过头去调试错误。对于 Android 资源文件,好比 String、Font 或 Animation 资源,咱们可使用 androidx.annotations,经过使用像 @StringRes、@FontRes 这样的注解,就可让代码检查工具 (如 Lint) 限制咱们只能传递正确类型的参数:html

fun myStringResUsage(@StringRes string: Int){ }
 
// 错误: 须要 String 类型的资源
myStringResUsage(1)
复制代码

扩展阅读:java

若是咱们的 ID 对应的不是 Android 资源,而是 Doggo 或 Cat 之类的域对象,那么就会很难区分这两个同为 Int 类型的 ID。为了实现类型安全,须要将 ID 包装在一个类中,从而使狗与猫的 ID 编码为不一样的类型。这样作的缺点是您要付出额外的性能成本,由于原本只须要一个原生类型,可是却实例化出来了一个新的对象。android

经过 Kotlin 内联类您能够建立包装类型 (wrapper type),却不会有额外的性能消耗。这是 Kotlin 1.3 中添加的实验性功能。内联类只能有一个属性。在编译时,内联类会在可能的地方被替换为其内部的属性 (取消装箱),从而下降常规包装类的性能成本。对于包装对象是原生类型的状况,这尤为重要,由于编译器已经对它们进行了优化。因此将一个原始数据类型包装在内联类里就意味着,在可能的状况下,数据值会以原始数据值的形式出现。git

inline class DoggoId(val id: Long)
data class Doggo(val id: DoggoId, … )
 
// 用法
val goodDoggo = Doggo(DoggoId(doggoId), …)
fun pet(id: DoggoId) { … }
复制代码

内联

内联类的惟一做用是成为某种类型的包装,所以 Kotlin 对其施加了许多限制:github

  • 最多一个参数 (类型不受限制)
  • 没有 backing fields
  • 不能有 init 块
  • 不能继承其余类

不过,内联类能够作到:数组

  • 从接口继承
  • 具备属性和方法
interface Id
inline class DoggoId(val id: Long) : Id {
  val stringId
  get() = id.toString()

  fun isValid()= id > 0L

}
复制代码

⚠️ 注意: Typealias 看起来与内联类类似,可是类型别名只是为现有类型提供了可选名称,而内联类则建立了新类型。安全

声明对象 —— 包装仍是不包装?

因为内联类相对于手动包装类型的最大优点是对内存分配的影响,所以请务必记住,这种影响很大程度上取决于您在何处以及如何使用内联类。通常规则是,若是将内联类用做另外一种类型,则会对参数进行包装 (装箱)。bash

参数被用做其余类型时会被装箱。app

好比,须要在集合、数组中用到 Object 或者 Any 类型;或者须要 Object 或者 Any 做为可空对象时。根据您比较两个内联类结构的方式的不一样,会最终形成 (内联类) 其中一个参数被装箱,也或者全部参数都不会被装箱。函数

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
复制代码

让咱们逐步分析反编译后的 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 有两个构造函数:

  • 私有合成构造函数 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);
   }
}
复制代码

参数 id 经过两种方式暴露给外界:

  • 经过 getId() 做为原生类型;
  • 做为一个对象: box_impl 方法会建立一个 DoggoId 实例。

若是在可使用原生类型的地方使用内联类,则 Kotlin 编译器将知道这一点,并会直接使用原生类型:

fun walkDog(doggoId: DoggoId) {}
 
// 反编译后的 Java 代码
public final void walkDog_Mu_n4VY(**long** doggoId) { }
复制代码

当须要一个对象时,Kotlin 编译器将使用原生类型的包装版本,从而每次都建立一个新的对象。

当须要一个对象时,Kotlin 编译器将使用原生类型的包装版本,从而每次都建立一个新的对象,例如:

可空对象

fun pet(doggoId: DoggoId?) {}
 
// 反编译后的 Java 代码
public static final void pet_5ZN6hPs/* $FF was: pet-5ZN6hPs*/(@Nullable InlineDoggoId doggo) {}

复制代码

由于只有对象能够为空,因此使用被装箱的实现。

集合

val doggos = listOf(myDoggoId)
 
// 反编译后的 Java 代码
doggos = CollectionsKt.listOf(DoggoId.box-impl(myDoggoId));
复制代码

CollectionsKt.listOf 的方法签名是:

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);
   }
}
复制代码

doggo1.equals(doggo2)

这种状况下,equals 方法会调用另外一个生成的方法: equals_impl(long, Object)。由于 equals 方法须要一个 Object 参数,因此 doggo2 的值会被装箱,而 doggo1 将会使用原生类型:

DoggoId.equals-impl(doggo1, DoggoId.box-impl(doggo2))
复制代码

doggo1 == doggo2

使用 == 会生成:

DoggoId.equals-impl0(doggo1, doggo2)
复制代码

因此在使用 == 时,doggo1 和 doggo2 都会使用原生类型。

doggo1 == 1L

若是 Kotlin 能够肯定 doggo1 事实上是长整型,那这里的相等性检查就应该是有效的。不过,由于咱们为了它们的类型安全而使用的是内联类,因此,接下来编译器会首先对两个对象进行类型检查,以判断咱们拿来比较的两个对象是否为同一类型。因为它们不是同一类型,咱们会看到一个编译器报错: "Operator == can’t be applied to long and DoggoId" (== 运算符没法用于长整形和 DoggoId)。对编译器来讲,这种比较就好像是判断 cat1 == doggo1 同样,毫无疑问结果不会是 true。

doggo1.equals(1L)
复制代码

这里的相等检查能够编译经过,由于 Kotlin 编译器使用的 equals 方法的实现所须要的参数能够是一个长整形和一个 Object。可是由于这个方法首先会进行类型检查,因此相等检查将会返回 false,由于 Object 不是 DoggoId。

覆盖使用原生类型和内联类做为参数的函数

定义一个方法时,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) { }
复制代码

在反编译出的代码中,咱们能够看到这两种函数,它们的参数都是原生类型。

为了实现此功能,Kotlin 编译器会改写函数的名称,并使用内联类做为函数参数。

在 Java 中使用内联类

咱们已经讲过,不能在 Java 中实例化内联类。那可不可使用呢?

✅ 可以将内联类传递给 Java 函数

咱们能够将内联类做为参数传递,它们将会做为对象被使用。咱们也能够获取其中包装的属性:

void myJavaMethod(DoggoId doggoId){
    long id = doggoId.getId();
}
复制代码

在 Java 函数中使用内联类实例

若是咱们将内联类声明为顶层对象,就能够在 Java 中以原生类型得到它们的引用,以下:

// Kotlin 的声明
val doggo1 = DoggoId(1L)
 
// Java 的使用
long myDoggoId = GoodDoggosKt.getU1();

复制代码

✅ & ❌调用参数中含有内联类的 Kotlin 函数

若是咱们有一个 Java 函数,它接收一个内联类对象做为参数。函数中调用一个一样接收内联类做为参数的 Kotlin 函数。这种状况下,咱们会看到一个编译器报错:

fun pet(doggoId: DoggoId) {}

// Java
void petInJava(doggoId: DoggoId){
    pet(doggoId)
    // 编译器报错: pet(long) cannot be applied to pet(DoggoId)  (pet(长整形) 不能用于 pet(DoggoId))
}
复制代码

对于 Java 来讲,DoggoId 是一个新类型,可是编译器生成的 pet(long) 和 pet(DoggoId) 并不存在。

可是,咱们仍是能够传递底层类型:

fun pet(doggoId: DoggoId) {}

// Java
void petInJava(doggoId: DoggoId){
    pet(doggoId.getId)
}
复制代码

若是在一个类中,咱们分别覆盖了使用内联类做为参数和使用底层类型做为参数的两个函数,当咱们从 Java 中调用这些函数时,就会报错。由于编译器会不知道咱们到底想要调用哪一个函数:

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,则可能会遇到没法使用的状况。

最后,这仍然是一项实验性功能。它是否会发布正式版,以及正式版发布时,它的实现是否与如今相同,都仍是未知数。

所以,既然您了解了内联类的好处和限制,就能够在是否以及什么时候使用它们的问题上作出明智的决定。

点击这里了解更多关于用 Kotlin 进行 Android 开发的相关资料

相关文章
相关标签/搜索