【再学Kotlin】换个名字可还行?typealias 和 inline class 的使用及背后实现

前言

Kotlin 的语法糖用起来很爽,但咱们不该只知足于会用的状态。本系列文章介绍 Kotlin 关键字的使用以及其背后的实现html

本文摘自 Kotlin Vocabulary 系列文章,原文请移步 Alter type with typealiasZero-cost* abstractions in Kotlinjava

typealias 的使用

使用 Java 开发一段时间可能以为 Java 中的变量名太长了!虽然优秀的命名最好是望文知义,但一堆很长的变量很影响可读性。android

C 和 C++ 中提供了 typedef 关键字来定义别名,而 Kotlin 中也有相似的存在git

typealias 容许在不引入新类型的状况下为类或函数类型提供别名github

可使用 typealias 命名函数类型安全

typealias TeardownLogic = () -> Unit
fun onCancel(teardown : TeardownLogic){ }

private typealias OnDoggoClick = (dog: Pet.GoodDoggo) -> Unit
val onClick: OnDoggoClick
复制代码

这样作的缺点是名称隐藏了传递给函数的参数,从而下降了可读性app

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 更合适ui

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 classKotlin 编译器将直接使用基本数据类型

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

相关文章
相关标签/搜索