关于kotlin:Kotlin-Vocabulary-内联类-inline-class

40次阅读

共计 5951 个字符,预计需要花费 15 分钟才能阅读完成。

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

类型平安帮忙咱们防止出现谬误以及防止回过头去调试谬误。对于 Android 资源文件,比方 String、Font 或 Animation 资源,咱们能够应用 androidx.annotations,通过应用像 @StringRes、@FontRes 这样的注解,就能够让代码查看工具 (如 Lint) 限度咱们只能传递正确类型的参数:

fun myStringResUsage(@StringRes string: Int){ }
 
// 谬误: 须要 String 类型的资源
myStringResUsage(1)

扩大浏览:

  • 利用正文改良代码查看

如果咱们的 ID 对应的不是 Android 资源,而是 Doggo 或 Cat 之类的域对象,那么就会很难辨别这两个同为 Int 类型的 ID。为了实现类型平安,须要将 ID 包装在一个类中,从而使狗与猫的 ID 编码为不同的类型。这样做的毛病是您要付出额定的性能老本,因为原本只须要一个原生类型,然而却实例化进去了一个新的对象。

通过 Kotlin 内联类 您能够创立包装类型 (wrapper type),却不会有额定的性能耗费。这是 Kotlin 1.3 中增加的试验性功能。内联类只能有一个属性。在编译时,内联类会在可能的中央被替换为其外部的属性 (勾销装箱),从而升高惯例包装类的性能老本。对于包装对象是原生类型的状况,这尤其重要,因为编译器曾经对它们进行了优化。所以将一个原始数据类型包装在内联类里就意味着,在可能的状况下,数据值会以原始数据值的模式呈现。

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

内联

内联类的惟一作用是成为某种类型的包装,因而 Kotlin 对其施加了许多限度:

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

不过,内联类能够做到:

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

  fun isValid()= id > 0L}

⚠️ 留神: Typealias 看起来与内联类类似,然而类型别名只是为现有类型提供了可选名称,而内联类则创立了新类型。

申明对象 —— 包装还是不包装?

因为内联类绝对于手动包装类型的最大劣势是对内存调配的影响,因而请务必记住,这种影响很大水平上取决于您在何处以及如何应用内联类。个别规定是,如果将内联类用作另一种类型,则会对参数进行包装 (装箱)。

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

比方,须要在汇合、数组中用到 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,则可能会遇到无奈应用的状况。

最初,这依然是一项试验性功能。它是否会公布正式版,以及正式版公布时,它的实现是否与当初雷同,都还是未知数。

因而,既然您理解了内联类的益处和限度,就能够在是否以及何时应用它们的问题上做出理智的决定。

正文完
 0