作者归档:sickworm

15. Kotlin 究竟该不该用 lateinit?

Published / by sickworm / Leave a Comment

使用 lateinit 的初衷

你是如何看待 lateinit?不少同学对它敬而远之,特别是使用 lateinit 踩坑之后。因为被 lateinit 标记的变量,不再接受空安全检查,它的表现更像是一个普通的 Java 变量。也有同学喜欢尽可能的用上它,把 lateinit 作为介于 nonnull 和 nullable 之间的一个状态:对象构造时为 null,在某一个时刻被初始化后一直都是 nonnull,这样属性的不确定性便减少了。

我也是一个 lateinit 的坚定支持者。原因之一是 lateinit 属性比 nullable 属性在行为上更可靠。所谓可靠,即其行为是确定的。当调用 lateinit 变量时,它此时如果没有被初始化,就会抛出UninitializedPropertyAccessException;如果已经初始化了,则操作一定会执行。反看 nullable 变量,你在任一时刻操作它的时候,它都可能不被执行,因为可空变量在任意时刻都可能被置空。这样的行为在排查问题的时候会造成阻碍。为了减少程序运行的不确定性,我更希望尽可能使用 lateinit 代替 nullable。

另一个原因是既然 Kotlin 语言设计者提供这样的关键字,说明是有可用之处的。

使用 lateinit 的坚持

理性分析完,随后我便开始一顿操作。只要是符合以下条件,我就会使用 lateinit 修饰属性:

  • 该属性在对象构造时无法初始化(缺少必要参数),在某个阶段被初始化之后会一直使用。典型的初始化阶段:Activity.onCreate(),自定义模块的 init()
  • 保证对象的调用都在初始化之后
  • 属性无法用空实现代替。

这个策略看起来是没什么问题的,执行的也比较顺利。自测没有问题,测试那边也顺利通过了。但在灰度的期间还是出现了 UninitializedPropertyAccessException

Crash 量也不多,但总还是得解的。Crash 的原因无非就一个:在初始化 lateinit 属性之前调用了该属性。而解决方案根据不同情况有两种:

  • 是异常路径导致,如 Activity.onCreate() 时数据不正确,需要 finish Activity 不再执行后续初始化代码。此时 Activity 仍然会执行 onDestroy(),而 lateinit 属性没有被初始化。如果 onDestroy() 有对 lateinit 属性的操作,此时就会抛出 UninitializedPropertyAccessException

解决方案:使用 ::lateinitVar.isInitialized 方法,对异常路径的 lateinit 属性进行判断,如果没有初始化则不操作。

对比 nullable 属性:lateinit 属性会 crash,nullable 属性不会,且和 lateinit 属性加了初始化判断的效果一致。这种场景下 nullable 属性表现的更好。

  • 是代码逻辑结构不正确导致,如在某些情况下,上层在调用模块 init() 方法之前,就调用了模块的其他方法。此时抛出 UninitializedPropertyAccessException

解决方案:调整代码调用逻辑,保证调用模块init()方法之前不调用模块的其他方法。

对比 nullable 属性:lateinit 属性会 crash,nullable 属性不会。但 lateinit 属性会把问题暴露出来,而 nullable 属性会把问题隐藏起来,导致问题难以发现和解决。

开发者对 lateinit 的争论也大多源自于此。支持 lateinit 的开发者,是希望代码有更好的逻辑性;反对 lateinit 的开发者,是希望代码有更好的健壮性。而对于我来说,我更希望代码有更好的逻辑性,但我也认可“希望代码有更好的健壮性”的想法,就看开发者的取舍了。

这个想法使我坚持使用 lateinit 半年以上。而这一段使用 lateinit 的痛点,也让我开始重新思考 lateinit 的收益。

使用 lateinit 的痛苦

理论和实践都完善了,但使我苦恼的是,UninitializedPropertyAccessException并没有得到高效的解决,而是三头两日时不时的在灰度时冒出来,使我被迫打断当前工作,花上一点时间解决,并延长版本灰度的时间。这不是我想要的效果。

UninitializedPropertyAccessException主要出现这几种场景:

  • 新代码使用了 lateinit 特性,因没有考虑异常路径在测试期间出现 crash;
  • 旧代码重构后对部分属性使用了 lateinit 特性,在复杂的线上环境中出现 crash;
  • 模块内部代码调整/外部调用逻辑调整,如调用时机的调整,导致之前没有问题的代码,在复杂的线上环境中出现 crash。

Kotlin 的 UninitializedPropertyAccessException本质上和 Java 的空指针错误是一样的,都是错误的估计此处对象不可能为空导致的。在 Java 中我们通过增加一堆空判断来解决这个问题,Kotlin 可以使用 nullable 对象。

而 lateinit 通过舍弃空安全机制,把空安全交回到开发者手上(就像 Java 那样)。但在这几个月的实践中,我发现让开发者自己掌控空指针问题,是困难的。

我发现之前我对 lateinit 的思考,缺少了一个很重要的角度:软件工程的角度。代码是不断迭代的,维护者可能不止一个人,而 lateinit 对空指针问题的保护不足,容易让新的空指针问题出现在代码迭代之后。没有从软件工程的角度去看待问题,导致我对代码的规划过于理想,让代码降低了健壮性。现在我想给 lateinit 增加这样一个观点:lateinit 是一个和软件工程相悖的特性,它不利于软件的健康迭代。

使用 lateinit 的建议

如果你仍想使用 lateinit,那么我建议:

  1. 充分考虑异常分支的执行情况;
  2. 充分考虑异常时序的执行情况;
  3. 充分考虑代码稳定性,是否容易发生需求变更导致结构调整。

目前依然有典型的 lateinit 适用场景,如Activity.onCreate()初始化的属性。但是不要忘了如果有可能初始化失败,需要在异常路径onDestroy()上增加::lateinitVar.isInitialized判断。

对于 Fragment,如果在onCreate执行了 finish(),它的异常路径会是onCreateView()onViewCreate()onDestroy()

版权所有,转载请注明出处:
https://sickworm.com/?p=1837

14. Kotlin 使用高阶函数处理集合数据

Published / by sickworm / Leave a Comment

本文将介绍如何使用 Kotlin 的高阶函数,如sumBy, reduce, fold, mapfilterforEach 等,来应对常见的集合数据处理场景。不了解高阶函数的同学可以先看下之前的文章。

遍历求和 sumBy

场景:输入一个账户列表List,求这些账户的财产总和sum

一般来说 Java 可以这样写:

public int getAccountsSum(List accounts) {
    int sum = 0;
    for (Account a: accounts) {
        sum += a.value;
    }
    return sum;
}

Kotlin 可以使用高阶函数 sumBy

val sum = accounts.sumBy { it.value }

那么sumBy做了什么呢?点击源码可以看到,其实它做的事情和上面 Java 实现的getAccountsSum是一样的,只是增加的值是通过我们传入的 lambda 来计算,而不是写死的Account.value这种通过传入函数来完成函数功能的函数,被称为高阶函数,高阶函数也因此具有很高的通用性和复用效率。

不仅传入函数作为参数的函数被称为高阶函数,返回值为函数的函数也同样被称为高阶函数。

遍历求值 reduce

sumBy有一点不好,他只能求和,而且只接受IntDouble两种类型的值(sumBy:不然我起这个名字干嘛?)。如果我们要得到一个更复杂的逻辑的结果呢?

场景:输入一个列表List,返回它们全部相乘的结果。

Java:

public int getResult(List values) {
    int result = 0;
    for (Account a: accounts) {
        result *= values;
    }
    return result;
}

Kotlin 我们可以使用reduce

val result = values.reduce { acc, v -> acc * v }

reduce 的逻辑是:将初始值acc设置为集合的第一个值,然后从第二个值开始,依次执行acc = lambda(acc, v),遍历完后返回acc。**reduce不仅限做加法运算,它比sumBy具有更广的通用性。

那如果reduce可以代替sumBy,为什么还需要sumBy?——因为它写起来更简单呀!

如果集合为空,reduce会抛出 UnsupportedOperationException(“Empty collection can’t be reduced.”)

更通用的遍历求值 fold

细心的同学已经发现了,sumBy的场景和reduce的场景用的是不同的数据结构。因为acc会被初始化为集合的第一个元素,所以reduce函数的输出也被限制为集合的范型类型。也就是说,sumBy的场景无法用reduce代替。

那 Kotlin 有没有能指定acc类型的高阶函数?有的,它叫fold

我们再回到sumBy的场景:输入一个账户列表List,求这些账户的财产总和sum

val result = accounts.fold(0) { acc, v -> acc + v.value }

foldreduce多了一个参数——初始值,用来赋值给acc。得益于范型,我们可以通过这个办法来指定acc的类型。这样一来,fold可以完美替代sumBy的场景。而相比foldsumBy更专用,表意更清晰,写起来也更简洁。

fold还有另一点好:因为acc由传入参数初始化,所以没有集合不能为空的限制。所以绝大部分情况下,我都建议使用fold来代替reduce

JavaScript 的 reduce 函数就是 Kotlin 的 fold 函数。u1s1,Kotlin 的 reduce 函数挺危险的,还有类型限制,不建议使用。

过滤集合 filter

场景:输入一个账户列表List,返回资产小于 100 的账户:

Java:

public List getPoorAccounts(List accounts) {
    List qbAccounts = new ArrayList<>(); // 想起虾米音乐的 穷逼 VIP 了
    for (Account a: accounts) {
        if (a.value < 100) {
            qbAccounts.add(a);
        }
    }
    return qbAccounts;
}

Kotlin 可以使用filter函数:

val qbAccounts = accounts.filter { it.value < 100 }

filter的逻辑是,新建一个空的 ArrayList(),然后把 lambda 返回值为 true 的元素加入到这个列表里。

列表生成列表 map

场景:输入一个账户列表List,找到所有资产大于 10000 的账户,封装成 VIP 账户返回:

Java:

public List getVipAccounts(List accounts) {
    List vipAccounts = new ArrayList<>();
    for (Account a: accounts) {
        if (a.value >= 10000) {
            vipAccounts.add(new VipAccount(a));
        }
    }
    return vipAccounts;
}

Kotlin 可以通过filter函数加map函数完成:

val vipAccounts = accounts
        .filter { it.value >= 10000 }
        .map { VipAccount(it) }

第一步我们用filter函数筛选出资产大于 10000 的账户,然后用map函数将过滤后的每一个账户转换为VipAccountmap的逻辑也很简单,它回返回一个和调用者大小相同的列表,具体的元素值为 lambda 的执行结果。

实在不适合,就用 forEach 吧

如果遇到了已知高阶函数都不适合的场景,不妨试试用forEach代替传统的 for 循环。为什么?因为写起来稍微简单一点。。

accounts.forEach {
    println("account: $it")
}

什么?你还想要 index下标?

accounts.forEachIndexed { index, account ->
    println("index: $index")
    println("account: $account")
}

不仅forEach有下标版本forEachIndexed,几乎所有高阶函数都有对应的 Indexed 版本。

Kotlin 官方提供了数十个高阶函数,但其实掌握了以上几个高阶函数,基本可以 cover 所有场景了。其他的只是写的简洁还是写的复杂一点的区别。而且你还有另一条路可以走:自己写一个特定的高阶函数。

担心性能?

大家可能会担心,如此频繁的声明 lambda,会不会使得类的数量大量膨胀?其实官方提供的高阶函数,都是用inline关键字修饰的。这意味着不仅高阶函数的调用最终会被函数的实际代码代替,而且声明的 lambda 也会被解析成具体的代码,而不是方法调用。所以Kotlin 高阶函数用 inline 关键字修饰,所以 lambda 不会生成新的 jvm class。而我们在声明自己的高阶函数时,也应该用inline关键字修饰,防止类数量膨胀。

大家可能担心的另一点,像mapfilter这样返回列表的高阶函数,每一次操作都会生成一个列表,这会不会增加垃圾回收的压力?答案是会的。但如果数据量不是万级别的,操作频率不是毫秒级别的,对性能的影响实在小之又小,特别是在移动端的场景更是难以遇到。但我们还是要了解高阶函数对性能开销,在对性能要求高的位置避免对象申请(如UI绘制的回调)。

Java 有高阶函数吗?

Java 也类似高阶函数的能力,如 Collections.sort 这种允许自定义排序的方法,和 Java 8 的 steam API。但因为 Java 没有 inline 无法有效的优化 lambda,且 Java 的 lambda 没有完整的闭包特性,无法修改外部变量。还有一些语法的原因,Java 的高阶函数使用起来相对没有那么舒服。

版权所有,转载请注明出处:
https://sickworm.com/?p=1833

13. Kotlin 作用域函数 run/let/apply/also 的使用

Published / by sickworm / Leave a Comment

上一篇文章我们介绍了作用域函数,并以其中一个作用函数run为例,介绍了作用域函数的使用和原理。除了run之外,Kotlin 官方还内置了letapplyalso这几个作用域函数,下面我们一起来他们的相同点和区别,并举例说明他们的使用场景。

1. 4 个作用域函数 = 2 个特性的两两组合

runletapplyalso,这 4 个作用域函数,其实是 2 个特性的组合结果:

  • 调用作用域函数的对象,是作为this传入,还是作为唯一参数(默认为it)传入;
  • 作用域函数的返回值,是调用的对象,还是 lambda 的返回值。

配合伪代码解释一下:

val result = object.xxx { param ->
    val lambdaResult = param.toString()
    lambdaResult
}

xxx可以是runletapplyalso其中一个。

如果xxx = run/let,那么 result == lambdaResult
而如果xxx = apply/also,那么result == object。且lambdaResult这一行会报错,因为applyalso“` 的 lambda 返回值必须是 Unit。

lambda 最后一行的值会作为 lambda 的返回值。它等价于 return@xxx lambdaResult@xxx表示返回的是这个lambda,而不是退出整个上层方法。如果是不在最后一行返回的代码,比如异常分支,就可以(也只能)这样用。

如果xxx = let/also,那么param == object
而如果xxx = run/apply的话,那么 lambda 是个无参数 lambda,不存在paramparam -> 这里会报错;

如果 lambda 为单参数 lambda,此时param ->可以省略,Kotlin 提供默认的单参数名it。这个很方便,文章后面的代码都会采用这种简写。

总结成表就是:

特性 返回值为this 返回值为lambda结果
调用对象转换为this apply run
调用对象转换为it also let

我们只需要知道 4 个作用域函数分别是 2 个特性的两两组合即可。用的时候直接查源代码,不需要专门记忆。

2. run/let/apply/also 各自的使用场景举例

我们已经知道这 4 个作用域函数的特点了,那么我们怎么用好它们呢?下面一起来看下这几个作用域函数的使用场景。

run

这是工程中的一段代码:

mRecordViewHelper?.run {
    mEffectsView.visibility = View.INVISIBLE
    mLyricViewer.visibility = View.INVISIBLE
    mStatusView = AudioRecordStatusView(CommonContext.getApplicationContext(), null)
    enableSoloCountBackView()
}

run方法很适合用在对某个对象做一系列操作的地方。这里可空对象mRecordViewHelper使用run方法把自己转换成非空对象this“传入”到 lambda 中,并在 lambda 内部进行一系列的赋值和方法调用。

run的返回值是 lambda 的最后一行结果,在这个例子是Unit。使用 lambda 结果的用例较少,主要就是这种转换为this的用法。如果后续遇到特别适合的再补充。

let

使用run还是使用let更像是一个个人喜好的问题,因为有些人就是不习惯把调用对象转换成this,而是更喜欢用入参it

使用run还是使用let的确没有太大区别。不过还是有这几种情况,我更建议你用let

  • 调用对象主要作为参数,而不是用于初始化或方法调用时。
// 赋值和调用的方法都是类的成员和方法,不是 mRecordViewHelper 的成员和方法
mRecordViewHelper?.run {
    myFirstView = firstView
    mySecondView = secondView
    setTargetView(targetView)
}

// 使用 let,不会存在两个 this 混用,代码可读性更高。
mRecordViewHelper?.let {
    myFirstView = it.firstView
    mySecondView = it.secondView
    setTargetView(it.targetView)
}
  • 当 lambda 中需要用到类的this时;
// 因为 this 的变化,导致引用类的 this 需要加@MainActivity。这个和 Java 匿名类的 MainActivity.this 效果一样
mRecordViewHelper?.run {
    setOnClickListener(this@MainActivity)
}

// let 不会改变 this,在需要引用类的 this 的时候比较方便。
mRecordViewHelper?.let {
    it.setOnClickListener(this)
}

总结一下:当 lambda 主要执行的是调用对象的方法和赋值时,建议使用run;而当调用对象主要用作参数时,建议使用let。当 lambda 会用到类的this时,建议使用let

apply

applyrun的区别主要在于,apply返回的是调用对象。这个特性使得apply很适合用来做类似初始化的工作。如:

class VideoConfig {
    val profile = VideoEncodeProfile().apply {
        audioBitRate = 44100
        videoWidth = 480
        videoHeight = 480
        videoFrameRate = 30
        setDefaultQuality()
    }
}

apply 也很适合用来做 property 的初始化,这样 property 的初始化代码就不用写在 init 块里了,做到了代码的高内聚。上面代码的profile 就是 property 初始化的例子。

also

also 的适用场景,和runlet一样,是与apply来对比的。具体建议也和runlet一样:

当 lambda 主要执行的是调用对象的方法和赋值时,建议使用apply;而当调用对象主要用作参数时,建议使用also。当 lambda 会用到类的this时,建议使用also

3. 只有 4 个作用域函数吗?

细心的同学可能已经发现,在 Standard.kt 中,除了runletapplyalso之外,还有好几个作用域函数。其实掌握了这 4 个作用域函数,已经覆盖了大部分使用场景。剩下的几个使用需求没有那么的迫切,但掌握之后,可以帮助你写出更有 Kotlin 味道的代码。

下一篇文章会介绍 Standard.kt 中剩余的作用域函数。

版权所有,转载请注明出处:
https://sickworm.com/?p=1830

12. Kotlin 作用域函数(scope function)run/let/apply/also

Published / by sickworm / Leave a Comment

0. 绕不开的四兄弟

学习 Kotlin 一定绕不开 run/let/apply/also 这四兄弟,它们是 Kotlin 使用频率最高的扩展方法(扩展方法在之前文章有介绍),它们也被称为作用域函数(scope functions)。今天我们就来了解一下它们。本文依然是按代码比较,字节码分析,和扩展思考三个方面进行分析。

搞懂其中一个,其他作用域函数可以视为其变种。这篇文章我们先看 run 方法。

1. run 方法使用

在工程中,我们有一段这样的 Java 代码:

public class PlayManager {
    /** 初始值为空,需要在资源初始化之后再拿到对象 */
    private Player player = null;

    /** 播放 */
    public void play(String path) {
        if (player != null) {
            player.init(path);
            player.prepare();
            player.start();
        }
    }
}

Kotlin等效代码为:

public class PlayManager {
    /** 初始值为空,需要在资源初始化之后再拿到对象 */
    private var player: Player? = null

    /** 播放 */
    fun play(path: String) {
        player?.init(path)
        player?.prepare()
        player?.start()
    }
}

如果用上 Kotlin 的 run 会是这样的:

public class PlayManager {
    /** 初始值为空,需要在资源初始化之后再拿到对象 */
    private var player: Player? = null

    /** 播放 */
    fun play(path: String) {
        player?.run {
            init(path)
            prepare()
            start()
        }
    }
}

这里的 run 调用是一种函数调用的特殊写法,即当 lambda 作为函数的最后一个参数时,可以写在函数括号外部,也就是说object.run { }object.run({ })是等价的。这种写法的好处在我看来,一是不用再去末尾数括号了,写 Java 的时候声明一个匿名类比如View.OnClickListener,总是忘了加括号,在 Kotlin 没有这个烦恼;二是像runmap 这种以函数作为参数的高阶函数,代码写起来看起来都更简洁利落。

看起来简洁了不少。

run的功能很简单,它就做了两件事:

  1. 把 lambda 内部的 this 改成了对应的调用对象。这个看起来很神奇,我们稍后再分析;
  2. run 函数会返回 lambda 的返回值。

run 方法达到了三个效果:

  1. 因为this 的变化,不再需要重复的输入变量,和链式调用异曲同工,但你并不需要额外花费精力来编写链式调用的代码;

  2. 把可空对象转换为了非空对象,因为run方法是问号调用,player 不为空才会执行。因为考虑到并发,Kotlin 要求每次调用可空属性的时候都进行判空,如此一来属性这个小朋友就会有很多问号。。使用 run 方法等效于先把可空属性用临时变量持有再使用,这样就消除了并发竞争的影响(Java 经常也有这种代码,不过要自己手写罢了)。

  3. 在一个函数里声明的这个一个小“代码块“,表示和其他无关的代码隔离,实现了函数内部的高内聚。这个效果可以增加代码的可读性,让人一看就明白:“哦,这是针对这个对象的一系列操作,这个函数里关于这个对象的使用我只需要关注这个代码块就可以了”。

第三点是我尤其喜欢的一个点,我觉得这样的设计不仅是为了提高开发效率,它更是在引导开发者写出好维护的代码。在写 Java 的时候,大家都很容易不自觉的写出某个对象在函数头操作一下,隔几行调用一下,隔几行又操作一下的代码。阅读者很容易误以为这些代码之间有着顺序上的耦合,从而继续按照这个“隐含的规则“来维护代码。却不知当时的开发者只是想到哪写到哪,实际并不存在这样的隐含关系。使用 run 可以在函数内部快速建立起一个个代码块,让函数拥有更清晰的结构,又不用花费很大精力把代码块拆成一个个小函数,毕竟给函数起名字可是非常头秃的事情。

说到不用起名字,lambda 本身就有“匿名函数”的外号,这样的使用方法可以说十分贴切了。而从耦合程度来看,代码块介于函数和过程代码之间。

函数是面向过程的产物,它天生就很容易产生耦合度高的代码。就我看来,作用域函数更像是给函数打上的一个“补丁”。

3. run 方法代码分析

run 源码如下:

@kotlin.internal.InlineOnly
public inline fun < T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

卧槽看起来好吊(看不懂),不是说好了很简单吗?因为这函数涉及的基本都是编译器相关的,平时开发用不到。这里包含了泛型,inline,类扩展 lambda(T.() -> R),contract 这 4 个概念。泛型我就默认你懂了,毕竟这里只讲 Kotlin 的新东西,Kotlin 泛型和 Java 的泛型除了写法没有什么区别。剩下的三个概念我们简单过一下吧。

inline,中文名内联函数,是 C/C++ 的老活儿了。inline 的意思是,虽然你声明了一个函数,但在编译期调用这个函数的地方会被替换为函数包含的代码。inline 的好处是调用该方法不再有调用方法的性能消耗,即不会跳转和产生栈帧;坏处是可能会使二进制文件膨胀,尤其是函数很大的时候。所以 inline 适合被频繁调用但代码量很小的函数,run 就很符合这个条件。我们可以因此得出结论:由于编译器编译时会把 inline 函数内联到实际调用位置,所以使用 run 方法不会有方法调用的性能损耗。

@kotlin.internal.InlineOnly,实际效果为对 Java 不可见(private),因为 Java 不支持 inline。对 Java 不可见后,这个 inline 方法则可以不在字节码里存在,因为调用的地方全部都内联了。

值得注意的是,和 C/C++ 一样,Kotlin 的 inline 也不是必然内联的。具体机制,我们有机会再聊(还没有学到)。

虽然 Java 没有内联函数,但是 JVM 是有内联优化的,只是这个优化无法精确控制。Java 的设计者一直尽可能让 Java 语言保持简单,这可能也是 Java 为什么能持续热门这么久的原因。

类扩展 lambda(关键字 lambda with class extension),即入参的声明 T.() -> R。lambda 我们了解了,扩展方法我们也了解了(强行假设你看过之前的文章),扩展 lambda 也可以理解为给类扩展一个 lambda 函数。它的效果也和扩展方法一样,在 扩展 lambda 作用域内,你可以以对象作为 this 来操作这个对象。

最后一个 contract 契约,指的是代码和 Kotlin 编译器的契约。举一个例子,我们对局部变量增加了如果为空则 return 的逻辑,Kotlin 编译器便可以智能的识别出 return 之后的局部变量一定不为空,局部变量的类型会退化为非空类型。但如果我们把是否为空的代码封装进一个扩展方法如 Any?.isNotNull() 里,那么编译器就无法识别 return 后面的代码局部变量是否为空了,这个局部变量依然是可空类型。

那么这个时候 contract 就派上用场了。我们可以声明一个 contract,告诉编译器如果Any?.isNotNull() 返回了 true,则表示对象非空。这样我们在代码里执行了 isNotNull() 方法之后,return 后面的代码,局部变量也能正确退化为非空类型。具体例子我们可以看官方 Collections.kt 的 Collection.isNullOrEmpty()

了解了 contract 的作用后我们再看 run 包含的契约。它意思是这个 lambda 只会被 run 方法执行一次,且 run 执行完后不会再被执行。对于了解到这个额外信息的 Kotlin 编译器,他就可以更有针对性的优化这里的代码(怎么针对,也还没有学到。。)。

4. 为何 Java 没有作用域函数?

作用域函数需要类扩展内联这两个能力,才能最大化体现其价值。没有类扩展,this 的切换需要通过继承或者匿名类来实现,且做不到通用;而像 let 这种不需要切换 this 的作用域函数,因为没有类扩展能力而为了追求通用性,也只能通过静态工具类来实现,效果是打折扣的。

而没有内联能力的 Java,虽然有 JVM 内联优化支撑,但内联优化只对 final 且调用次数数量级较大的方法有效。如果像 Kotlin 这样规模化的使用作用域函数,对性能是有不可忽视的影响的。

5. 其他作用域函数的使用和适用场景

下一篇!

版权所有,转载请注明出处:
https://sickworm.com/?p=1823

11. Kotlin 类声明与伴生对象(companion)

Published / by sickworm / Leave a Comment

1. companion object 的诞生

Scala 说,要有伴生对象。

于是 Kotlin 便有了 companion object。

companion object 的出现是为了解决 Java static 方法的反面向对象(Anti-OOP)的问题。static 方法无法声明为接口,无法被重写——用更学术的话来说,static 方法没有面向对象的消息传递延迟绑定特性[参考]。而 Scala 为了完成一切皆对象的使命,以及提高与 Java 的兼容性,提出了伴生对象这个概念来代替 static 方法。随后出身的 Kotlin 也借鉴了这个概念。

companion 伴生对象是一个对象,它在类初始化时被实例化。 因为伴生对象不再是类的 static 方法,而是某个类的实例化对象,所以它可以声明接口,里面的方法也可以被重写,具备面向对象的所有特点。

2. companion 的实现

在 Kotlin 中,调用 Java 的 static 方法和调用 Kotlin 的 companion object 方法是一样的:

AJavaClass.staticFun()
AKotlinClass.companionFun()

而在 Java 中,调用 static 方法和 Kotlin 的伴生 companion object 方法,有一点不同:

AJavaClass.staticFun();
AKotlinClass.Companion.companionFun();

从 Java 的调用我们可以发现,companion object 的 JVM 字节码体现,是一个声明了一个叫 Companion 的 static 变量。

而 Kotlin 调用一致,其实是编译器的特殊处理的结果。

如果我们反编译AKotlinClass,可以看到:

// AKotlinClass.class
public final class AKotlinClass {
    public static final Companion Companion = new Companion(null);
}
// AKotlinClass$Companion.class
public final class AKotlinClass$Companion {
    private DownloadExecutor$Companion() {
    }

    public /* synthetic */ DownloadExecutor$Companion(DefaultConstructorMarker $constructor_marker) {
        this();
    }

    public final void companionFun() {
    }
}

可以看到,Companion 是一个叫 AKotlinClass$Companion 的类的实例,带 $ 符号表示这个类是 AKotlinClass 的内部类,名字叫 Companion,所以在AKotlinClass中直接new Companion(null)即可。

你也许还留意到实例化 Companion 使用的是一个带 DefaultConstructorMarker 入参的构造器。它出现的场景是,如果是 Kotlin 编译器生成的特殊构造器,就会带这个参数。在这里,Kotlin 希望能够实例化 Companion 类,但又不想声明一个 public 的构造器,于是就声明了这样一个特殊的构造器。DefaultConstructorMarker 值永远为 null。

DefaultConstructorMarker 出现的另一个场景是:带默认参数的构造器。因为带了默认参数后,构造器需要增加 int 类型的 bitmask 入参,来识别哪个入参需要被替换为默认参数。而为构造器增加一个入参,容易和其他构造器“撞车”,即构造器的入参完全一样,导致编译失败。所以默认参数的构造器末尾还会增加一个 DefaultConstructorMarker 入参,来防止构造器参数一致的问题。[参考]

版权所有,转载请注明出处:
https://sickworm.com/?p=1816

10. Kotlin 类声明和构造器(constructor)

Published / by sickworm / Leave a Comment

1. Java 和 Kotlin 构造器代码对比

Java 的构造器声明和方法声明没有太大区别,也支持重载,唯一的限制是:必须调用父类构造器(如果父类只有一个构造器而且是无参的,编译器会帮你自动加上,这是特例)。我们使用 Java 多年,构造器很少会给我们带来不便,也不曾听人吐槽 Java 的构造器声明的不合理,便是无功无过,规规矩矩。但现代编程语言还是从构造器身上找到了优化空间,Scala–Kotlin 是其中之一。

我们不妨直接上代码对比 Kotlin 和 Java 的构造器声明的区别。现在我们有这样一个 android.view.View 的子类,它的 Java 实现:

public class RecordingBottomView extends ConstraintLayout implements View.OnClickListener {

    private static final String TAG = "RecordingBottomView";
    private static final int DEFAULT_VALUE = 100;
    private EffectBarDialog effectBarDialog = new EffectBarDialog(getContext());

    private TextView channelButton;
    public TextView effectButton;
    private TextView restartButton;
    private TextView finishBtn;

    private RecordPlayButton playButton;
    private TextView switchCameraButton;
    private TextView filterButton;
    private TextView resumeTips;

    private int defaultValue = DEFAULT_VALUE;

    public RecordingBottomView(Context context) {
        super(context);
        init(context, null, 0);
    }

    public RecordingBottomView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs, 0);
    }

    public RecordingBottomView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs, defStyleAttr);
    }

    private void init(Context context, AttributeSet attrs, int defStyleAttr) {
        LayoutInflater.from(context).inflate(R.layout.recording_bootom_panel, this, true);
        findViewById(R.id.recording_channel_switch_btn).setOnClickListener(this);
        channelButton = findViewById(R.id.recording_channel_switch_btn);
        effectButton = findViewById(R.id.recording_effect_btn);
        effectButton.setOnClickListener(this);


        restartButton = findViewById(R.id.recording_restart_btn);
        restartButton.setOnClickListener(this);
        finishBtn = findViewById(R.id.recording_finish_btn);
        finishBtn.setOnClickListener(this);

        switchCameraButton = findViewById(R.id.song_record_camera_switch);
        switchCameraButton.setOnClickListener(this);
        filterButton = findViewById(R.id.song_record_filter);
        filterButton.setOnClickListener(this);
        playButton = findViewById(R.id.recording_play_button);
        playButton.setOnClickListener(this);
        resumeTips = findViewById(R.id.song_record_resume_tip_btn);

        effectBarDialog.setSoundSelectListener(reverbId -> {
            LogUtil.i(TAG, "soundSelect");
            return Unit.INSTANCE;
        });
        effectBarDialog.setToneChangeListener((isUp, toneValue) -> {
            LogUtil.i(TAG, "toneChange");
            return Unit.INSTANCE;
        });
        effectBarDialog.setVolumeChangeListener(volume -> {
            LogUtil.i(TAG, "volumeChange");
            return Unit.INSTANCE;
        });

        if (attrs != null) {
            TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RecordingBottomView);
            defaultValue = ta.getInt(R.styleable.RecordingBottomView_default_value, DEFAULT_VALUE);
        }
    }

    @Override
    public void onClick(View v) {
        // handle click
    }
}

非常经典的代码,在构造器中初始化所有的子 View 成员变量以及 View 参数。

对应的,Kotlin 风格的实现如下:

class RecordingBottomView(context: Context, attrs: AttributeSet?, defStyleAttr: Int):
        ConstraintLayout(context, attrs, defStyleAttr),
        View.OnClickListener {

    constructor(context: Context): this(context, null, 0)

    constructor(context: Context, attrs: AttributeSet?): this(context, attrs, 0)

    private val effectBarDialog = EffectBarDialog(context).also {
        it.soundSelectListener = { reverbId: Int? ->
            LogUtil.i(TAG, "soundSelect")
        }
        it.toneChangeListener = { isUp: Boolean?, toneValue: Int? ->
            LogUtil.i(TAG, "toneChange")
        }
        it.volumeChangeListener = { volume: Float? ->
            LogUtil.i(TAG, "volumeChange")
        }
    }

    private val channelButton = findViewById<TextView>(R.id.recording_channel_switch_btn).also {
        it.setOnClickListener(this)
    }

    var effectButton = findViewById<TextView>(R.id.recording_effect_btn).also {
        it.setOnClickListener(this)
    }

    private val restartButton = findViewById<TextView>(R.id.recording_restart_btn).also {
        it.setOnClickListener(this)
    }

    private val finishBtn = findViewById<TextView>(R.id.recording_finish_btn).also {
        it.setOnClickListener(this)
    }

    private val playButton: RecordPlayButton? = null
    private val switchCameraButton = findViewById<TextView>(R.id.recording_finish_btn).also {
        it.setOnClickListener(this)
    }

    private val filterButton = findViewById<TextView>(R.id.recording_finish_btn).also {
        it.setOnClickListener(this)
    }

    private val resumeTips = findViewById<TextView>(R.id.song_record_resume_tip_btn)

    private val defaultValue: Int = let {
        attrs?: DEFAULT_VALUE

        val ta = context.obtainStyledAttributes(attrs, R.styleable.RecordingBottomView)
        val value = ta.getInt(R.styleable.RecordingBottomView_default_value, DEFAULT_VALUE)
        ta.recycle()
        value
    }

    init {
        LayoutInflater.from(context).inflate(R.layout.recording_bootom_panel, this, true)
        findViewById<View>(R.id.recording_channel_switch_btn).setOnClickListener(this)
    }

    override fun onClick(v: View) {}

    companion object {
        private const val TAG = "RecordingBottomView"
        private const val DEFAULT_VALUE = 100
    }
}

这里暂且不展开谈 默认参数,view binding,also let 闭包的知识点,这些知识点会在其他文章单独介绍。

对我而言,在我接触 Kotlin 这种构造器声明之前,我没有想过 Java 的构造器声明有什么缺点。但当我接触之后,我开始思考 Kotlin 为什么要这样设计构造器声明,以及 Java 构造器声明的不足之处:

1. **Java 构造器成员变量如果依赖构造参数,它们的声明和最终赋值是分离的,同一个成员变量的代码是低内聚的。**而且像```defaultValue```这个参数,虽然他有初始值,但在一定条件下,他可能会被重新赋值。这些问题都会增加阅读者的心智负担;

2. 所有的初始化代码都在一个函数中,很容易出现“超级函数”。**不同成员变量的初始化代码大部分互相没有联系,但是却以先后顺序的形式耦合在同一个函数中,这是高耦合的。**

3. Java 构造器允许重载,虽然设计规范提倡重载函数应最终调用同一个实现,以得到清晰的逻辑表达。但这不是强制性的。这意味着你可能会遇到多个构造器各自拥有自己的实现,这会加重问题 1,2 的严重性。

对应的,Kotlin 采用了如下对策来一一解决这些问题:

1. property 声明初始化时允许使用主构造器参数,变量声明和初始化代码都写在同一个地方,代码是高内聚的;

2. 使用 let 闭包后,成员变量的所有的初始化代码都可以写在闭包内。不同的成员变量初始化代码相互独立,代码是低耦合的;

3. 仅允许一个主构造器,其他构造器为从构造器,并约定从构造器必须调用主构造器,让主构造器去调用父构造器。

如果 Kotlin 类没有声明主构造器,全部都是从构造器,则退化为 Java 构造器风格,没有调用主构造器的约束。这样的设计一是为了 Java 转 Kotlin 代码时能兼容旧代码结构,不用重构也能直接转换为 Kotlin 代码;二也方便了 Java 转 Kotlin 自动化工具的实现。但 property 的初始化无法引用从构造器的入参,因为从构造器是可以有多个的,从调用上无法保证每个从构造器的每个参数都存在。

2. Kotlin 构造器实现分析

上面我们简单的过了一遍 Kotlin 对 Java 构造器的优化,但 Java 采用这样的设计,是因为它忠实的反映了 JVM 的构造器实现。而 Kotlin 的构造器设计,并不符合 JVM 的实现。Kotlin 要最终在 JVM 上运行,必须在编译期处理,最终变回类似 Java 构造器的实现。

我们直接看一下 Kotlin 编译再反编译后的字节码:

public RecordingBottomView(@NotNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        Intrinsics.checkParameterIsNotNull(context, "context");
        super(context, attrs, defStyleAttr);
        EffectBarDialog it = new EffectBarDialog(context);
        it.setSoundSelectListener(RecordingBottomView$effectBarDialog$1$1.INSTANCE);
        it.setToneChangeListener(RecordingBottomView$effectBarDialog$1$2.INSTANCE);
        it.setVolumeChangeListener(RecordingBottomView$effectBarDialog$1$3.INSTANCE);
        this.effectBarDialog = it;
        View findViewById = findViewById(R.id.recording_channel_switch_btn);
        ((TextView) findViewById).setOnClickListener(this);
        this.channelButton = (TextView) findViewById;
        View findViewById2 = findViewById(R.id.recording_effect_btn);
        ((TextView) findViewById2).setOnClickListener(this);
        this.effectButton = (TextView) findViewById2;
        View findViewById3 = findViewById(R.id.recording_restart_btn);
        ((TextView) findViewById3).setOnClickListener(this);
        this.restartButton = (TextView) findViewById3;
        View findViewById4 = findViewById(R.id.recording_finish_btn);
        ((TextView) findViewById4).setOnClickListener(this);
        this.finishBtn = (TextView) findViewById4;
        View findViewById5 = findViewById(R.id.recording_finish_btn);
        ((TextView) findViewById5).setOnClickListener(this);
        this.switchCameraButton = (TextView) findViewById5;
        View findViewById6 = findViewById(R.id.recording_finish_btn);
        ((TextView) findViewById6).setOnClickListener(this);
        this.filterButton = (TextView) findViewById6;
        this.resumeTips = (TextView) findViewById(R.id.song_record_resume_tip_btn);
        RecordingBottomView recordingBottomView = this;
        if (attrs == null) {
            Integer.valueOf(100);
        }
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RecordingBottomView);
        int value = ta.getInt(R.styleable.RecordingBottomView_default_value, 100);
        ta.recycle();
        this.defaultValue = value;
        LayoutInflater.from(context).inflate(R.layout.recording_bootom_panel, (ViewGroup) this, true);
        findViewById(R.id.recording_channel_switch_btn).setOnClickListener(this);
    }

public RecordingBottomView(@NotNull Context context) {
    Intrinsics.checkParameterIsNotNull(context, "context");
    this(context, null, 0);
}

public RecordingBottomView(@NotNull Context context, @Nullable AttributeSet attrs) {
    Intrinsics.checkParameterIsNotNull(context, "context");
    this(context, attrs, 0);
}

可以看到,property 和 init 块的初始化代码会被顺序的放入主构造器中,也就是说代码是从上往下按顺序执行的。因此 Kotlin 的初始化代码不仅可以使用主构造器的参数,还可以使用比自己先初始化的 property 和 init 块。

版权所有,转载请注明出处:
https://sickworm.com/?p=1810

9. Kotlin 函数声明和扩展(extension)

Published / by sickworm / Leave a Comment

1. Java 的老朋友 Utils 工具类

Utils 工具类是无构造参数的 static 方法集合,用于扩展某个对象的功能,如 MathUtils,ToastUtils,FIleUtils,StringUtils, LogUtils。Utils 类在一定程度上减少了重复代码的问题,它是成本最低的 DRY(Don’t repeat yourself)实践。

Utils 工具类实在太常见了,以至于很多开发者都不曾质疑他的合理性。但 Utils 实际上是反 OOP (面向对象模式)的妥协产物。我们从代码设计的角度看,Utils 方法是 static 的,没有 OOP 的继承,重写,抽象的特性(static 本身就是反 OOP 的)。且 Utils 违反了单一职责,一个类应该包含其属性和所有操作方法。而 Utils 实现的方法并不在这个类内。

而从使用者的角度,使用者必须预先知道这个 Utils 工具类的存在,他能使用为这个类添加的扩展方法。 在实际项目实践中,这个条件往往是缺失的,因为在团队开发中,个人无法掌握所有代码,因为不知道这个代码已经有人实现过了,导致大家都实现了自己的 Utils。一个工程里同一个类的 Utils 往往会有好几个。

但存在必然是合理的。我自己就是一个写 Utils 的老司机。从个人角度来看,让我使用 Utils 而不是对象继承的原因,主要是因为:

1. 无法继承/重写这些类及其方法,只能通过 Utils 扩展;
2. 继承一个类比抽取代码块封装为函数的实现成本+替换成本高;
3. Utils 绝大情况下只是用来存储代码块,需求非常稳定,无需面向对象。

依赖的类是 SDK 提供的时候 Utils 往往是不可避免的。且使用 Utils 的场景里很少会用到面向对象的特性,那么没有面向对象的缺点也并没有那么严重了。那么抛开 Utils 的设计缺点,我们是否可以避免使用上的缺点?Kotlin 提供的解决方法就是扩展(extension)。

2. Kotlin 扩展的使用和实现分析

声明一个 Kotlin 扩展如下:

// StringUtils.kt
fun String.appendHaha(): String {
    return this + "haha"
}

它与普通的方法声明很接近,只是方法名前多了一个类名,来表示其归属的类。扩展声明为顶层声明的时候可以被外部调用(是的,因为函数是一等公民,在方法内部也可以声明扩展方法)。

在函数体内用 this 来引用调用的实例,属性和方法的访问权限与普通调用一致。一致的意思是和你正常在其他方法内部调用的权限一致,并不会因为是扩展声明就可以访问 private/propect 权限的属性和方法。这是因为扩展声明在字节码层面上其实是 static 方法。下面是appendHaha 对应 jvm 字节码的反编译结果:

public class StringUtilsKt {
    @NotNull
    public static final String haha(@NotNull String $this$haha) {
        Intrinsics.checkParameterIsNotNull($this$haha, "$this$haha");
        return $this$haha + "haha";
    }
}

所以从 Java 的角度来看,Kotlin 的扩展方法和 Utils 的调用没有差别,都是调用类的 static 方法然后传入操作的参数。实际上 Java 想要调用 Kotlin 的扩展方法也确实是这样调用的。

扩展方法的调用和实例方法调用一致,在调用者角度没有区别。 Android Studio 会自动提示对应类所有的扩展方法,且扩展方法的颜色(黄色)会和普通实例方法(白色)区分开来。

Kotlin 的扩展特性和 objective-C 的 category 特性功能非常相似,都是为一个现有的类添加方法(且只能添加方法),只是代码组织结构上有些许差异。但 objective-C 的 category 特性是 runtime 特性,Kotlin 扩展的实现更接近语法糖。

3. 总结

Kotlin 扩展依然没有解决 Utils 类的设计缺点。就像 Kotlin companion object 对 Java static,Kotlin Int 对 Java int,Kotlin property 对 Java field 一样,Kotlin 扩展是 Kotlin 对 Java 不完全面向对象的“清理”,使 Kotlin 更接近完全面向对象。相比 Utils 工具类,Kotlin 扩展特性能有效减少开发者心智负担和沟通成本,从而提高开发效率。

版权所有,转载请注明出处:
https://sickworm.com/?p=1806

8. Kotlin 函数声明与默认参数(Default argument)

Published / by sickworm / Leave a Comment

1. Java 的函数重载和烦恼

在 Java 中,当我们要实现同一种功能,但函数入参出参不一样的函数的时候,我们可以用到 Java 的函数重载功能。在 Android framework 中同样也存在大量的重载函数,以方便开发者调用。重载函数深入人心,得到大家的认可。

东西确实是好东西,但当重载函数过多的时候,代码就显得臃肿了,比如这里有一个 Toast 显示的工具类,在经过不断的功能扩展后,发展成一个拥有海量重载方法的类:

// ...

public static void show(CharSequence msg) {
    show(Toast.LENGTH_SHORT, msg, Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
}

public static void showLong(CharSequence msg) {
    show(Toast.LENGTH_LONG, msg, Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
}

public static void show(Activity activity, int resId) {
    show(activity, resId, Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
}

public static void show(Activity activity, CharSequence msg) {
    show(Toast.LENGTH_SHORT, activity, msg, Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
}

/**
 * 当msg0不为空时展示msg0,忽略msg1;否则显示msg1
 */
public static void show(Activity activity, CharSequence msg0, CharSequence msg1) {
    if (!TextUtils.isEmpty(msg0)) {
        show(Toast.LENGTH_SHORT, activity, msg0, Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
    } else if (!TextUtils.isEmpty(msg1)) {
        show(Toast.LENGTH_SHORT, activity, msg1, Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
    }
}

public static void show(Activity activity, int resId, int gravity) {
    show(Toast.LENGTH_SHORT, activity, resId == 0 ? null : getString(resId), gravity);
}

public static void show(int duration, Activity activity, int resId) {
    show(duration, activity, resId == 0 ? null : getString(resId), Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
}

public static void show(int duration, Activity activity, CharSequence msg) {
    show(duration, activity, msg, Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
}

public static void show(final int duration, Activity activity, final CharSequence msg, final int gravity) {
    // 具体实现
}

// ...

和 get/set 方法一样,这是典型的信息密度低的代码。那么有什么办法能够更精简的表达同样的功能,不同的入参的特性呢?有的,就是默认参数特性。

2. 重载函数的替代者,默认参数

Kotlin 拥有默认参数的特性,如果用 Kotlin 实现上述 Java 代码,可以简化为:

fun show(msg: CharSequence,
        msg2: CharSequence? = null,
        context: Context = Global.getApplicationContext(),
        gravity: Int = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM,
        duration: Int = Toast.LENGTH_SHORT) {
    // 具体实现
}

我们看到,声明默认参数的方法很简单,只需要把可以使用默认参数的入参用“=“号给他赋值一个默认值即可。一般来说,我们会把必须提供的参数写在前面,有默认参数的入参排在后面。因为你在调用的时候,函数的入参默认是按顺序映射的,按上面的顺序排列的话,你只需要填完必须的参数即可;而如果还想提供可选参数,就继续按顺序填写。

那如果我只想提供部分可选参数,比如上面的show函数我只想提供duration参数,跳过其他可选参数呢?Kotlin 提供了这样的调用办法:

show("this is a toast");
show("this is a toast, duration = Toast.LENGTH_LONG);
show(msg = "this is a toast, duration = Toast.LENGTH_LONG);
show(duration = Toast.LENGTH_LONG, msg = "this is a toast);

我们发现,Kotlin 方法调用时,可以显式的指明对象和入参的映射关系,无需按顺序传递。注意,这个特性不分必须参数和可选参数,所有的参数都可以用这种形式指定映射。

但一般来说,我们只在可选参数时用到。还有一种应用场景是,当你觉得必须参数的值让人迷惑,想显式的告诉阅读者这个值所对应的入参时。

大家可能已经发现,很早以前,Android Studio 对没有提供名字的函数参数,已经默认显示这个参数对应的名字。

Flutter 的 Dart 语言也有默认参数特性,而且 Flutter 组件对默认参数的使用可谓是淋漓尽致。它会把一个控件所有可配置的参数都提供在构造函数中,而且把必须参数和可选参数分开。这样开发者可以很方便的看到它必须配置和可以配置的所有参数,不用再去慢慢找这个控件提供了什么设置方法。

3. 默认参数和函数重载对比

默认参数和重载函数对比,重载函数可以改变入参和出参(返回值),默认参数只可以改变入参。不过改变出参的场景实在很少,一般我们都会用不同的函数名来区分不同的返回值,比如 covertToFloatcovertToInt

其次,每一个重载函数都是一个方法,会记录在方法表,占用 Dex 的最大方法数。默认参数会生成 2 个方法,一个是全参数的方法 A,另一个方法 B 也是全参数,但比全参数方法多出来了 flag 参数,该 flag 参数用来记录哪些参数是可选参数。外部调用的时候调用的是方法 B,没有指定的可选参数将会被赋值 0 或 false 或 null,指定了的可选参数会被赋值,且对应个 flag 位会被标记。到了方法 B 内部,没有被 flag 标记的参数,会被设置为默认值,最后方法 B 调用 方法 A。Kotlin 通过这种方式,减少了重载函数可能带来过多的方法数。

Kotlin 也支持函数重载。

4. 函数声明的特性发展

如果是一开始接触的都是高级语言的同学,可能会觉得函数重载是个比较奇怪的特性:为什么这也算是一种特性?他们除了方法名是一样的,入参不一样,出参不一样,为什么要单独拿出来说呢?这是因为在 C 语言时代,方法的识别只通过函数名,所以无法做到函数重载。后来大家感受到同名函数不同参数存在的必要性,像 C++,就把方法的入参和出参都写到了符号表里。Java 的方法签名,也是包含入参和出参的。这样的语言,就具备识别重载函数的能力,所以函数重载就成为了一种新特性。

但函数重载,是一个个不同的函数,只是名字一致而已。在语义精简和代码规范有一定的缺陷。语义精简就是“更少的代码表达相同的意图”;而代码规范,因为函数重载的功能基本是相同的,更推荐的做法是函数重载只有一份实现代码,其他函数重载都补全参数,然后调用这个完整的实现代码,就像开头的 Toast 工具类代码一样。但实际上由于缺少实际约束,有些开发者会复制多份实现,填入到不同的重载函数中。可以说函数重载容易写出,“smelly”的代码。

而默认参数特性,避免了函数重载的语义精简和代码规范缺陷。

版权所有,转载请注明出处:
https://sickworm.com/?p=1794

7. Kotlin 变量声明和属性(property)

Published / by sickworm / Leave a Comment

1. Java 的成员变量和它们的 get/set 方法

在 Java 中,我们把在类中声明的变量,称为为成员变量(field),函数中声明的变量称为局部变量。在经典的 Java 设计理念中,成员变量是不建议暴露的,而当你想访问修改成员变量时,应声明其对应的 get/set 方法。因为成员变量没有办法继承重写
,无法声明为接口,get/set 权限无法分开控制等。使用 get/set 方法代替直接修改成员变量,更符合面向对象设计。
因此 get/set 方法在 Java 大地上遍地开花,无处不在。所以我们经常能看到这样的代码:

public class StringEntity {

    private String resId;
    private String value;

    public StringEntity() {
    }

    public StringEntity(String resId, String value) {
        this.resId = resId;
        this.value = value;
    }
    public String getResId() {
        return resId;
    }
    public void setResId(String resId) {
        this.resId = resId;
    }
    public String getValue() {
        return value;
    }
    public void setValue(String value) {
        this.value = value;
    }
}

噼里啪啦的写了一大堆代码,但功能却极其简单:StringEntity 包含了 resId 和 value 两个 String 的属性,你可以读取或修改它。虽说现在 IDE 都可以帮你快速的生成这些代码,但无法摆脱代码的信息密度低,可读性差的缺点。那么有没有什么语言能够更精简的表达 get/set 的语义呢?有的。

更详细的 get/set 与 field 的比较,参考:https://stackoverflow.com/questions/1568091/why-use-getters-and-setters-accessors

2. 引入属性 property 的概念

最开始我是在 objective-C 上了解到 property 的概念。属性 property 和成员变量 field 的声明和使用方法都没有什么区别,但property 允许你自定义它的 get/set 方法。如果你不自定义 property 的 get/set 方法,那它就和一个普通的变量没什么区别;而如果你自定义了 get/set 方法,在你读取/修改 property 时,实际上是调用了 property 的 get/set 方法。简单来说,属性 property=成员变量 field + get/set 方法,且 get/set 方法拥有默认实现。

property 的优点也很明显:

  1. 你可以用更简洁的方式实现 get/set 方法;
  2. field 和 get/set 方法统一后,代码的内聚性更高了,不会出现 field 在文件头,get/set 方法在文件尾的情况;
  3. 在 Java 类内部调用中,你既可以调用 field,也可以调用 get/set 方法,这种情况下内部调用是不统一的,当 get/set 方法添加了更多的行为时,原本直接调用 field 的内部代码可能会出错。property 统一了入口,避免了这种问题。

3. Kotlin 的 property 使用

在你不知道 property 的概念时,你就像声明一个局部变量一样声明 property 即可。

当你希望自定义 property 的 get/set 方法时,就为它增加 get/set 方法,按特定的语法结构来声明。举个例子:

class DataList {
    var dataList: List = listOf()
        private set // 1
    var size: Int // 2
        get() = dataList.size // 3
        set(value) { // 4
            dataList = ArrayList(value)
        }
    var name: String = "DataList"
        set(value) {
            field = value // 5
            println("new name: $field")
        }
}

这段代码列举了几种常用的 property 用法,简要说明:

  1. 当你只需要修改 property 的 get/set 访问权限时,不需要定义方法体;
  2. 如果定义了 get 方法且类型可以推断时,类型是可以省略的
  3. get 方法的声明方式,是一个无参的函数(= 号用法适用于代码只有一行的情况,用大括号也可以)
  4. set 方法的声明方式,是一个只有一个参数的函数,入参名字可随意发挥。
  5. 调用 property 的 field 的方法。前面说到 property = field + get/set 就是这个意思。

Kotlin properties 介绍:https://kotlinlang.org/docs/reference/properties.html

4. Kotlin property 的 JVM 字节码体现

Kotlin property 被编译成字节码后,通过反编译我们可以看到,一个 property 会生成一个同名的 field,以及驼峰命名的 get/set 方法。所以 Java 想要调用 Kotlin 的 property 时,直接调用 get/set 方法即可。而 Kotlin 调用 Kotlin 的property,则没有 Java 那么麻烦,就像局部变量一样调用即可。

版权所有,转载请注明出处:
https://sickworm.com/?p=1790&preview=true

6.Kotlin 变量声明与基本类型(Primitive Type)

Published / by sickworm / Leave a Comment

本文将会介绍 Java 的基本类型和 Kotlin 的区别。我们知道,Java 的基本类型是 boolean, char, short, int, long, float, double。这些基本类型不是对象,只可以进行基本的数学逻辑运算。Java 虽然打着“一切皆对象”的口号,但在基本类型还是留了一手。他们是特别的存在。

至于为什么要保留基本类型,真相只有一个:性能。大部分基本类型操作是一条指令就可以完成的,而对象方法调用则需要很多条指令才能完成;另外占用内存相比对象,也小很多。可以说 Java 诞生初期,在概念统一和性能的权衡下,把天平偏向了性能。这也很合理:90 年代时候的硬件速度还非常慢。且 Java 最初是为嵌入式设备而设计的,后面才把目标改为互联网。

现在市面上大部分的银行卡,里面装的是 Java 虚拟机,开发者通过编写受限的 Java 代码来实现一个叫 Applet 的应用单元,并装载到银行卡中。银行卡被插入后,机器会通过针脚或 NFC 和银行卡进行通讯。这种技术叫 Java Card 技术。
所谓受限的 Java 代码,没有 String,没有 JDK,甚至大部分连 int 都不支持。只能用 byte 和 short。因为芯片是 16 位的。
我上一份工作,在银行卡上实现了三种数字货币的交易协议。。

我们不妨把 Java 的面向对象称为不完全面向对象。那么是否有“真·面向对象”语言?有的。如 Smalltalk,Python,Kotlin 就是。在他们的编程环境里,没有基本类型,是真正的“一切皆对象”。这样带来的好处是概念的统一。“基本类型”这样的概念不再被需要,不再需要特别的处理它,所有声明出来的变量都具有同样的行为,不再需要区分引用类型和值类型。说到引用类型和值类型,大家在初学 Java 的时候应该都花了不少功夫去理解吧?

当然了,Java 也有基本类型对应的对象封装。如 int 对应 Integer,float 对应 Float,并且 jdk1.5 之后提供了自动装箱拆箱的编译器特性。但因为写起来比基本类型麻烦,且考虑性能问题,导致如果不是限定场景,大家都不会主动用它们。

而 Kotlin 为了提供完全面向对象的特性,摒弃了基本类型。但 Kotlin 没有直接使用 Java 的 java.lang.Integer,java.lang.Float 装箱类,而是另起山头,创造了 kotlin.Int,kotlin.Float 等类,因为别人写的代码都是 shit,因为 Java 的装箱类是集成在 JDK 的,无法随着 Kotlin 版本更新而更新。且在 Kotlin 中,数值类还有拥有额外的编译特性:

前面说到 Java 因为性能问题,保留了基本类型。那么 Kotlin 选择了完全面向对象,那理应要承受一定的性能损失。但其实 Kotlin 在编译成 jvm 字节码的时候,大部分的 Int 都会编译回 int,小部分会被编译成 Integer。这个小部分,典型的情况就是你声明一个变量为可空类型时,即声明为 Int?,这个时候无法使用 jvm 的基本类型结构。

而我们观察 kotlin.Int 时,可以看到除了数学运算的运算符重载方法,和强转的方法(toFloat,toLong 等)外,就没有其他方法了,而这些方法都可以直接对应基本类型运算的操作。kotlin.Int 声明为这样一个简洁的数值封装类,让转换为 jvm 字节码的基本类型铺平道路。

所以使用 kotlin 的数值类型时,绝大部分场景下,不会有额外的性能开销。

版权所有,转载请注明出处:
https://sickworm.com/?p=1788