Kotlin Vocabulary

2020-12-31  本文已影响0人  Drew_MyINTYRE

Collection 和 Sequence

Kotlin 提供了基于不同执行方式的两种集合类型: 立即执行 (eagerly) 的 Collection 类型,延迟执行 (lazily) 的 Sequence 类型。
立即执行和延迟执行的区别在于每次对集合进行转换时,这个操作会在何时真正执行。

Collection 会立即执行对数据的操作,而 Sequence 则是延迟执行。根据要处理的数据量大小,选择最合适的一个: 数据量小,则使用 Collection,数据量大,则使用 Sequence,另外,需注意操作顺序带来的影响。

//Collections
public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {
  return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform)
}

public fun <T, R> Sequence<T>.map(transform: (T) -> R): Sequence<R>{      
   return TransformingSequence(this, transform)
}

不同之处:

Collection 的操作使用了内联函数,所以处理所用到的字节码以及传递给它的 lambda 字节码都会进行内联操作。而 Sequence 不使用内联函数,因此,它会为每个操作创建新的 Function 对象。

640.gif

看图得知:当列表数据很大时,中间集合的创建会很消耗资源,这种情况下就应该使用 Sequence。

密封类 sealed class

sealed class (本文下称 "密封类" ) 则同时包含了前面两者的优势 —— 抽象类表示的灵活性和枚举里集合的受限性。

sealed class Result<out T : Any> {
  data class Success<out T : Any>(val data: T) : Result<T>()
  sealed class Error(val exception: Exception) : Result<Nothing>() {
     class RecoverableError(exception: Exception) : Error(exception)
     class NonRecoverableError(exception: Exception) : Error(exception)
  }
  object InProgress : Result<Nothing>()
}

工作原理:

//反编译 一下:
sealed class Result
data class Success(val data: Any) : Result()
data class Error(val exception: Exception) : Result()
 
@Metadata(
   ...
   d2 = {"Lio/testapp/Result;", "T", "", "()V", "Error", "Success", "Lio/testapp/Result$Success;", "Lio/testapp/Result$Error;" ...}
)
 
public abstract class Result {
   private Result() {
   }
 
   // $FF: synthetic method
  //一个合成构造方法,只有 Kotlin 编译器可以使用
  //这意味着其他的类无法直接调用密封类的构造方法。
   public Result(DefaultConstructorMarker $constructor_marker) {
      this();
   }
}

public final class Success extends Result {
   @NotNull
   private final Object data
 
   public Success(@NotNull Object data) {
      Intrinsics.checkParameterIsNotNull(data, "data");
      //可以看到它调用了 Result 的合成构造方法
      super((DefaultConstructorMarker)null);
      this.data = data;
   }

类型别名 typealias

使用类型别名时,需要思考是否有必要这么做: 在这里使用类型别名真的会让您的代码意义更明确、可读性更好吗?
类型别名需要在类的外部声明,所以使用时您需要考虑约束它们的可见性。

//使用的某个类名称很长,您可以使用类型别名来缩短它:
typealias AVD = AnimatedVectorDrawable

//在此示例中,使用导入别名 (import alias) 会更加合适:
import android.graphics.drawable.AnimatedVectorDrawable as AVD

枚举和 R8 编译器

Kotlin 最终会编译为 Java 字节码,但是它却提供了 Java 所没有的功能。那么 Kotlin 是怎么做到的呢?这些功能有没有额外开销?如果有,我们能做些什么来优化它吗?

事实上,有三个编译器参与了 Android 应用中 Kotlin 代码的编译。

Kotlin 编译器将会首先运行,它会把您写的代码转换为 Java 字节码。虽然听起来很棒,但可惜的是 Android 设备上并不运行 Java 字节码,而是被称为 DEX 的 Dalvik 可执行文件。Dalvik 是 Android 最初所使用的运行时。而 Android 现在的运行时,则是从 Android 5.0 Lollipop 开始使用的 ART (Android Runtime),不过 ART 依然在运行 DEX 代码 (如果替换后的运行时无法运行原有的可执行文件的话,就毫无兼容性可言了)。

D8 是整个链条中的第二个编译器,它把 Java 字节码转换为 DEX 代码。到了这一步,您已经有了能够运行在 Android 中的代码。不过,您也可以选择继续使用第三个编译器 —— R8。

R8 以前是用来优化和缩减应用体积的,它基本上就是 ProGuard 的一个替代方案。R8 不是默认开启的,如果您希望使用它 (例如您想要这里讨论到的那些优化时),就需要启用它。在模块的 build.gradle 里添加 minifyEnabled = true ,就可以强制打开 R8 。它将在所有其他编译工作后执行,来保证您获得的是一个缩减和优化过的应用。

内联类 inline class

interface Id
inline class DoggoId(val id: Long) : Id {
  val stringId
  get() = id.toString()

  fun isValid()= id > 0L
}

内联函数的原理与应用

每个高阶函数都会造成函数对象的创建和内存的分配,从而带来额外的运行时开销。

Reified: 类型擦除后再生计划

泛型提供了类型安全,并帮助开发者在编程时不需要进行显示的类型转换。但是使用泛型也会有一些限制,比如当您在泛型函数中想要获取泛型所表示类型的具体信息时,编译器就会报错,提示说相关的信息不存在。为了达到这一目标,Kotlin 提供了一个特别的关键字 reified,使用它就可以在泛型函数中获取所需的类型信息。

没利用reified关键字,解决这一问题的一个方法,是将泛型实际代表的类型信息作为一个参数传递给函数。

fun <T> printType(classType: Class<T>) {
    print(classType::class.java)
}

历史背景:

在 Java 5.0 版本之前并未支持泛型,那时 Java 中的 collection 是没有类型信息的。也就是说一个 ArrayList 并不会声明它内部所包含的数据类型到底是 String、Integer 还是别的类型。
在没有泛型支持时,任何时候想访问 collection 中的对象,都要做一次显式的类型转换。另外也没有相应的错误保障机制来防止出现非法的类型转换。

List list = new ArrayList();
list.add("First String");
// 正常处理,没有错误
list.add(6); 

String str = (String)list.get(1); 
// 需要显示地进行转换和抛出异常

类型擦除

泛型是通过一种叫类型擦除 (type erasure) 的技巧实现的。由于 Java 5 之前没有关联类型信息,编译器会先将所有类型替换为基本的 Object 类型,然后再进行必要的类型转换。通过将类型信息提供给编译器,类型擦除可以做到既保证编译时类型安全,又可以通过保持字节码同之前的 Java 版本相同来实现向后兼容。

Reified 关键字必须结合内联函数一起使用,它能让本该在编译阶段就被擦除的类型信息,能够在运行时被获取到。

如果一个函数被标记为 inline,那么 Kotlin 编译器会在所有使用该函数的地方将函数调用替换为函数体。这样做的好处是,编译器可以随意地在调用处对函数体进行修改,因为修改的函数体是被复制的,所以修改后不会影响到其余调用同样函数的地方。若是要在参数中使用 reified,那首先需要将函数标记为 inline,然后在泛型参数之前添加 reified 关键字即可。

inline fun <reified T> printType() {
  print(T::class.java)
}

fun printStringType(){
 // 用 String 类型调用被 reified 修饰的泛型函数
  printType<String>()  
}

另外请牢记,Java 代码中不能访问被 reified 修饰的函数。Java 不支持内联,也就意味着在 Java 中的泛型参数不能逃脱被编译器擦除类型的命运。

Reified 允许您在使用泛型来进行编程的同时,还能够在运行时获取到泛型所代表的类型信息,这在之前是无法做到的。当您需要在内联函数中使用到类型信息,或者需要重载泛型返回值时,您可以使用 reified。使用 reified 不会带来任何性能上的损失,但是如果被内联的函数过于复杂则,还是可能会导致性能问题。因为 reified 必须使用内联函数,所以要保证内联函数的简短,并且遵循使用内联函数的最佳实践,以免让性能受到损失。

上一篇 下一篇

猜你喜欢

热点阅读