小谈 kotlin 的空解决

作者 : 开心源码 本文共4561个字,预计阅读时间需要12分钟 发布时间: 2022-05-12 共174人阅读

大家好,我是光源。

近来关于 Kotlin 的文章着实不少,Google 官方的支持让越来越多的开发者开始关注 Kotlin。不久前加入的项目用的是 Kotlin 与 Java 混合开发的模式,纸上得来终觉浅,终于可以实践一把新语言。本文就来小谈一下 Kotlin 中的空解决。

一、上手确实容易

先扯一扯 Kotlin 学习本身。

之前各种听人说上手容易,但真要切换到另一门语言,难免还是会踌躇能否有这个必要。现在由于工作关系直接上手 Kotlin,感受是 真香(上手确实容易)

首先在代码阅读层面,对于有 Java 基础的程序员来说阅读 Kotlin 代码基本无障碍,除去少量操作符、少量顺序上的变化,整体上可以直接阅读。

其次在代码编写层面,仅需要改变少量编码习惯。主要是:语句不要写分号、变量需要用 var 或者 val 公告、类型写在变量之后、实例化一个对象时不用 “new” …… 习惯层面的改变只要要多写代码,自然而然就适应了。

最后在学习方式层面,因为 Kotlin 最终都会被编译成字节码跑在 JVM 上,所以初入手时完全可以用 Java 作为比照。比方你可能不知道 Kotlin 里 companion object 是什么意思,但你知道既然 Kotlin 最终会转成 jvm 可以跑的字节码,那 Java 里必然可以找到与之对应的东西。

Android Studio 也提供了很方便的工具。选择菜单 Tools -> Kotlin -> Show Kotlin Bytecode 就可看到 Kotlin 编译成的字节码,点击窗口上方的 “Decompile” 就可看到这份字节码对应的 Java 代码。 —— 这个工具特别重要,如果一段 Kotlin 代码让你看得云里雾里,看一下它对应的 Java 代码你就能知道它的含义。

图1

当然这里仅仅是说上手或者入门(仅入门的话可以忽略诸如协程等高级特性),真正熟练应用乃至完全掌握一定需要肯定时间。

二、针对 NPE 的强规则

有些文章说 Kotlin 帮开发者处理了 NPE(NullPointerException),这个说法是不对的。在我看来,Kotlin 没有帮开发者处理了 NPE (Kotlin: 臣妾真的做不到啊),而是通过在语言层面添加各种强规则,强制开发者去自己解决可能的空指针问题,达到尽量减少(只能减少而无法完全避免)出现 NPE 的目的。

那么 Kotlin 具体是怎样做的呢?别焦急,我们可以先回顾一下在 Java 中我们是怎样解决空指针问题的。

Java 中对于空指针的解决总体来说可以分为“防御式编程”和“契约式编程”两种方案。

“防御式编程”大家应该不陌生,核心思想是不信任任何“外部”输入 —— 不论是真实的客户输入还是其余板块传入的实参,具体点就是各种判空。创立一个方法需要判空,创立一个逻辑块需要判空,甚至自己的代码内部也需要判空(防止对象的回收之类的)。示例如下:

    public void showToast(Activity activity) {        if (activity == null) {            return;        }                ......    }

另一种是“契约式编程”,各个板块之间商定好一种规则,大家按照规则来办事,出了问题找没有遵守规则的人负责,这样可以避免大量的判空逻辑。Android 提供了相关的注解以及最基础的检查来协助开发者,示例如下:

    public void showToast(@NonNull Activity activity) {        ......    }

在示例中我们给 Activity 添加了 @NonNull 的注解,就是向所有调用这个方法的人公告了一个商定,调用方应该保证传入的 activity 非空。当然聪明的你应该知道,这是一个很弱的限制,调用方没注意或者者不理睬这个注解的话,程序就仍然还有 NPE 导致的 crash 的风险。

回过头来,对于 Kotlin,我觉得就是一种把契约式编程和防御式编程相结合且提升到语言层面的解决方式。(听起来似乎比 Java 中各种判空或者注解更麻烦?继续看下去,你会发现确实是更麻烦……)

在 Kotlin 中,有以下几方面束缚:

  1. 在公告阶段,变量需要决定自己能否可为空,比方 var time: Long? 可接受 null,而 var time: Long 则不能接受 null。

  2. 在变量传递阶段,必需保持“可空性”一致,比方形参公告是不为空的,那么实参必需本身是非空或者者转为非空才能正常传递。示例如下:

        fun main() {        ......        //  test(isOpen) 直接这样调用,编译不通过        // 可以是在空检查之内传递,证实自己非空        isOpen?.apply {             test(this)        }        // 也可以是强制转成非空类型        test(isOpen!!)    }    private fun test(open: Boolean) {        ......    }
  3. 在使用阶段,需要严格判空:

        var time: Long? = 1000    //虽然你才赋值了非空的值,但在使用过程中,你无法这样:    //time.toInt()    //必需判空    time?.toInt()

总的来说 Kotlin 为理解决 NPE 做了大量语言层级的强限制,确实可以做到减少 NPE 的发生。但这种既“契约式”(判空)又“防御式”(公告空与非空)的方案会让开发者做更多的工作,会更“麻烦”一点。

当然,Kotlin 为了减少麻烦,用 “?” 简化了判空逻辑 —— “?” 的实质还是判空,我们可以通过工具查看 time?.toInt() 的 Java 等价代码是:

      if (time != null) {         int var10000 = (int)time;      }

这种简化在数据层级很深需要写大量判空语句时会特别方便,这也是为什么尽管逻辑上 Kotlin 让开发者做了更多工作,但写代码过程中却并没有感觉到更麻烦。

三、强规则之下的 NPE 问题

在 Kotlin 这么严密的防御之下,NPE 问题能否已经被终结了呢?答案当然能否定的。 在实践过程中我们发现主要有以下几种容易导致 NPE 的场景:

1. data class(含义对应 Java 中的 model)公告了非空

例如从后台拿 json 数据的场景,后台的哪个字段可能会传空是用户端无法控制的,这种情况下我们的预期必需是每个字段都可能为空,这样转成 json object 时才不会有问题:

data class User(        var id: Long?,        var gender: Long?,        var avatar: String?)

如果有一个字段忘了加上”?”,后台没传该值就会抛出空指针异常。

2. 过分依赖 Kotlin 的空值检查

private lateinit var mUser: User...private fun initView() {    mUser = intent.getParcelableExtra<User>("key_user")}

在 Kotlin 的体系中久了会过分依赖于 Android Studio 的空值检查,在代码提醒中 Intent 的 getParcelableExtra 方法返回的是非空,因而这里你直接用方法结果赋值不会有任何警告。但点击进 getParcelableExtra 方法内部你会发现它的实现是这样的:

    public <T extends Parcelable> T getParcelableExtra(String name) {        return mExtras == null ? null : mExtras.<T>getParcelable(name);    }

内部的其余代码不开展了,总之它是可能会返回 null 的,直接赋值显然会有问题。

我了解这是 Kotlin 编译工具对 Java 代码检查的不足之处,它无法精确判断 Java 方法能否会返回空就选择无条件信任,即使方法本身可能还公告了 @Nullable

3. 变量或者形参公告为非空

这点与第一、第二点都很相似,主要是使用过程中肯定要进一步思考传递过来的值能否真的非空。

有人可能会说,那我一律都公告为可空类型不就得了么 —— 这样做会让你在使用该变量的所有地方都需要判空,Kotlin 本身的便利性就荡然无存了。

我的观点是不要因噎废食,使用时多注意点即可以避免大部分问题。

4. !! 强行转为非空

当将可空类型赋值给非空类型时,需要有对空类型的判断,确保非空才能赋值(Kotlin 的束缚)。

我们使用!! 可以很方便得将“可空”转为“非空”,但可空变量值为 null,则会 crash

因而使用上建议在确保非空时才用 !!:

    param!!

否则还是尽量放在判空代码块里:

    param?.let {        doSomething(it)     }

四、实践中碰到的问题

从 Java 的空解决转到 Kotlin 的空解决,我们可能会下意识去寻觅对标 Java 的判空写法:

    if (n != null) {        //非空如何      } else {        //为空又如何    }

在 Kotlin 中相似的写法确实有,那就是结合高阶函数 let、apply、run …… 来解决判空,比方上述 Java 代码即可以写成:

    n?.let {        //非空如何    } ?: let {        //为空又如何    }

但这里有几个小坑。

1. 两个代码块不是互斥关系

如果是 Java 的写法,那么不论 n 的值怎么,两个代码块都是互斥的,也就是“非黑即白”。但 Kotlin 的这种写法不是(不确定这种写法能否是最佳实践,如果有更好的方案可以留言指出)。

?: 这个操作符可以了解为 if (a != null) a else b,也就是它之前的值非空返回之前的值,否则返回之后的值。

而上面代码中这些高阶函数都是有返回值的,详见下表:

函数返回值
let返回指定 return 或者函数里最后一行
apply返回该对象本身
run返回指定 return 或者函数里最后一行
with返回指定 return 或者函数里最后一行
also返回该对象本身
takeIf条件成立返回对象本身,不成立返回 null
takeUnless条件成立返回 null,不成立返回该对象本身

如果用的是 let, 注意看它的返回值是“指定 return 或者函数里最后一行”,那么碰到以下情况:

    val n = 1    var a = 0    n?.let {        a++        ...        null  //最后一行为 null    } ?: let {        a++    }

你会很神奇地发现 a 的值是 2,也就是既执行了前一个代码块,也执行了后一个代码块

上面这种写法你可能不以为然,由于很显著地提示了诸位需要注意最后一行,但如果是之前没注意这个细节或者者是下面这种写法呢?

    n?.let {        ...        anMap.put(key, value) // anMap 是一个 HashMap    } ?: let {        ...    }

应该很少人会注意到 Map 的 put 方法是有返回值的,且可能会返回 null。那么这种情况下很容易踩坑。

2. 两个代码块的对象不同

以 let 为例,在 let 代码块里可以用 it 指代该对象(其余高阶函数可能用 this,相似的),那么我们在写如下代码时可能会顺手这样写:

    activity {        n?.let {            it.hashCode() // it 为 n        } ?: let {            it.hashCode() // it 为 activity        }       }

结果自然会发现值不一样。前一个代码块 it 指代的是 n,然后一个代码块里 it 指代的是整个代码块指向的 this。

起因是 ?: 与 let 之间是没有 . 的,也就是说后一个代码块调用 let 的对象并不是被判空的对象,而是 this。(不过这种场景会出错的概率不大,由于在后一个代码块里很多对象 n 的方法用不了,就会注意到问题了)

后记

总的来说切换到 Kotlin 还是比预期顺利和舒服,写惯了 Kotlin 后再回去写 Java 反倒有点不习惯。今天先写这点,后面有其余需要总结的再分享。

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 小谈 kotlin 的空解决

发表回复