前言
Kotlin
的语法糖用起来很爽,但我们不应只满足于会用的状态。本系列文章介绍 Kotlin
关键字的使用以及其背后的实现
本文摘自 Kotlin Vocabulary 系列文章,原文请移步 Alter type with typealias 和 Zero-cost* abstractions in Kotlin
typealias 的使用
使用 Java 开发一段时间可能觉得 Java 中的变量名太长了!虽然的命名好是望文知义,但一堆很长的变量很影响可读性。
C 和 C++ 中提供了 typedef 关键字来定义别名,而 Kotlin
中也有类似的存在
typealias
允许在不引入新类型的情况下为类或函数类型提供别名
可以使用 typealias
命名函数类型
typealias TeardownLogic = () -> Unit
fun onCancel(teardown : TeardownLogic){ }
private typealias OnDoggoClick = (dog: Pet.GoodDoggo) -> Unit
val onClick: OnDoggoClick
复制代码
这样做的缺点是名称隐藏了传递给函数的参数,从而降低了可读性
typealias TeardownLogic = () -> Unit
typealias TeardownLogic = (exception: Exception) -> Unit
fun onCancel(teardown : TeardownLogic){
// 不能直接看到 TeardownLogic 内部的逻辑
}
复制代码
typealias
允许缩短长泛型的名称
typealias Doggos = List<Pet.GoodDoggo>
fun train(dogs: Doggos){ … }
复制代码
当然也可以缩短长类名
typealias AVD = AnimatedVectorDrawable
复制代码
不过上面的场景使用 import alias 更合适
import android.graphics.drawable.AnimatedVectorDrawable as AVD
复制代码
这种情况下使用短命名并不能帮助我们提高可读性并且 IDE 会自动为我们补全类名
但是,如果需要区分来自不同包的同名类时,导入别名变得特别有用
import io.plaidapp.R as appR
import io.plaidapp.about.R
复制代码
以上用例来自 Alter type with typealias
typealias 背后的实现
typealias D = Data
fun add(item: D) {
}
fun usage() {
add(D("name"))
}
复制代码
将 Data 声明别名 D 并使用,Decompiled 为 Java
可以看到 typealias
并没有声明新的类型
您不应该依赖类型别名来进行编译时类型检查。 相反,您应该考虑使用 inline class
例如我们的 play 方法需要传递 dog 的 id
fun play(dogId: Long)
复制代码
在尝试传递错误的 id 时,为 Long 创建类型别名不会帮助我们防止错误
typealias DogId = Long
fun play(dogId: DogId) { … }
fun usage() {
val cat = Cat(1L)
// 实际传递猫的 id ,但是可以编译通过
play(cat.catId)
}
复制代码
inline class 的使用
我们知道声明方法时可以指定传入参数的类型范围
// 只允许传入 layout 资源
public AppCompatActivity(@LayoutRes int contentLayoutId) {
super(contentLayoutId);
}
复制代码
但是如果我们限制使用的不是 Android 的资源,而是 Dog Cat 这样实体类的 id,我们需要将其包装到一个类中。这样做的缺点就是需要付出额外的性能成本,原本可能只需要一个基本数据类型,现在使用时额外实例化了一个对象
Kotlin
inline classes 允许您创建包装类型并且没有性能损耗。这是 Kotlin 1.3
中添加的实验功能。inline class
必须有且仅有一个属性。 在编译时,将 inline class
实例替换为其内的属性(没装箱的基本数据类型),从而降低常规包装类的性能损耗。 对于包装对象是基本类型的情况,基本数据类型包装在 inline class
中会导致在运行时使用基本数据类型的值
内联类的作用是成为类型的包装,因此 Kotlin
对其做了很多限制:
- 多一个参数(类型不受限制)
- 没有 backing fields
- 没有初始化块
- 不能继承类
inline class
可以
- 实现接口
- 拥有属性和方法
interface Id
inline class DoggoId(val id: Long) : Id {
val stringId
get() = id.toString()
fun isValid()= id > 0L
}
复制代码
typealias
看起来与inline class
很像,但是typealias
只是为现有类型提供了别名,而inline class
会创建新类型
inline class 背后的实现
让我们看一个简单的 inline class
interface Id
inline class DoggoId(val id: Long) : Id
复制代码
构造器
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)
- 公开的构造函数
constructor_impl
当创建新的实例时将使用公开的构造函数
val myDoggoId = DoggoId(1L)
// decompiled
static final long myDoggoId = DoggoId.constructor-impl(1L);
复制代码
当我们尝试在 Java 中创建 doggo 时,会报错
DoggoId u = new DoggoId(1L);
// Error: DoggoId() in DoggoId cannot be applied to (long)
复制代码
无法在 Java 中实例化
inline class
参数化的构造函数是私有的,第二个构造函数在名称中包含 -(在 Java 中为字符)。 这意味着无法在 Java 实例化 inline class
参数使用
这里的 id 通过两种方式使用
- 作为基本数据类型,通过
getId()
- 通过
box_impl
创建DoggoId
的实例化对象
public final long getId() {
return this.id;
}
public static final DoggoId box_impl/* $FF was: box-impl*/(long v) {
return new DoggoId(v);
}
复制代码
如果在可以使用基本数据类型的地方使用 inline class
,Kotlin
编译器将直接使用基本数据类型
fun walkDog(doggoId: DoggoId) {}
// decompiled Java code
public final void walkDog_Mu_n4VY(long doggoId) { }
复制代码
当需要一个对象时,Kotlin
编译器将使用基本数据类型的装箱版本,从而每次都创建一个新的对象
下面我们来看看需要装箱的几种情况
可空对象
fun pet(doggoId: DoggoId?) {}
// decompiled Java code
public static final void pet_5ZN6hPs/* $FF was: pet-5ZN6hPs*/(@Nullable InlineDoggoId doggo) {}
复制代码
只有引用数据类型才能为 null ,因此需要装箱
集合
val doggos = listOf(myDoggoId)
// decompiled Java code
doggos = CollectionsKt.listOf(DoggoId.box-impl(myDoggoId));
复制代码
// CollectionsKt.listOf
fun <T> listOf(element: T): List<T>
复制代码
由于该方法需要引用数据类型,因此需要装箱
基类
fun handleId(id: Id) {}
fun myInterfaceUsage() {
handleId(myDoggoId)
}
// decompiled Java code
public static final void myInterfaceUsage() {
handleId(DoggoId.box-impl(myDoggoId));
}
复制代码
这里也需要装箱
equals 检查
Kotlin
编译器会尽其所能使用没装箱的基本数据类型参数,因此,inline class
具有三种相等检查的方式:1 个重写 equals 和 2 个生成的方法
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;
}
// 重写 equals 方法
public boolean equals(Object var1) {
return equals-impl(this.id, var1);
}
}
复制代码
doggo1.equals(doggo2)
equals
方法调用一个生成的方法:equals_impl(long,Object)
。 由于 equals
期望有一个对象,因此将对doggo2 值进行装箱,但是将 doggo1 用作基本数据类型
DoggoId.equals-impl(doggo1, DoggoId.box-impl(doggo2))
复制代码
doggo1 == doggo2
使用 ==
等价于 DoggoId.equals-impl0(doggo1, doggo2)
因此使用 ==
doggo1 和 doggo2 均使用基本数据类型
doggo1 == 1L
如果 Kotlin
编译器能够确定 doggo1 是 long 类型,那么这种相等性检查有效。 但是,由于 inline class
是类型安全的,因此,编译器要做的件事是检查这两个对象的类型是否相同。 如果不相同,我们会收到编译器错误:Operator ==
can’t be applied to long and DoggoId
doggo1.equals(1L)
由于 Kotlin
编译器使用 equals 实现,因此需要一个 long 和一个 Object 进行相等检查。 但是,由于此方法的件事是检查 Object 的类型,因此该相等性检查将为 false,因为 Object 不是 DoggoId
Zero-cost* abstractions in Kotlin 原文还介绍了在 Java 中使用 inline class
以及如何选择是否使用 inline class
,感兴趣的小伙伴可移步原文查看,这里不做介绍
关于我
我是 Fly_with24