小谈Kotlin的空处理的使用

近来关于 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 代码你就能知道它的含义。

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

二、针对 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 中,有以下几方面约束:

在声明阶段,变量需要决定自己是否可为空,比如 var time: Long? 可接受 null,而 var time: Long 则不能接受 null。

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

fun main() {
    ......
    // test(isOpen) 直接这样调用,编译不通过
    // 可以是在空检查之内传递,证明自己非空
    isOpen?.apply {
      test(this)
    }
    // 也可以是强制转成非空类型
    test(isOpen!!)
  }

  private fun test(open: Boolean) {
    ......
  }

在使用阶段,需要严格判空:

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 反倒有点不习惯。今天先写这点,后面有其他需要总结的再分享。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Kotlin函数默认值的完全讲解

    函数默认值 周所周知,Java语言并不支持参数使用默认值.有人说这是因为"默认参数"和"方法重载"同时支持的话有二义性的问题,具体真正的原因我不得而知.但是对我个人来说,Java不支持这个特性的确挺让我蛋疼的,虽然说使用方法重载也可以间接实现与默认参数这个特性相同的功能,但这就意味着你得写更多的代码-- 简要介绍 Kotlin函数定义时,支持对参数指定默认值,这样就有效减少Java之前定义重载函数的数量. 简要对比如下: 1.Java函数定义,如果sayHelloT

  • Kotlin整合Vertx开发Web应用

    今天我们尝试Kotlin整合Vertx,并决定建立一个非常简单的Web应用程序,使用Kotlin和Vertx作为编程语言进行编码构建. 生成项目 打开控制台窗口执行以下代码进行生成一个maven项目 复制代码 代码如下: mvn archetype:generate -DgroupId=com.edurt.kvi -DartifactId=kotlin-vertx-integration -DarchetypeArtifactId=maven-archetype-quickstart -Dver

  • SpringBoot整合Kotlin构建Web服务的方法示例

    今天我们尝试Spring Boot整合Kotlin,并决定建立一个非常简单的Spring Boot微服务,使用Kotlin作为编程语言进行编码构建. 创建一个简单的Spring Boot应用程序.我会在这里使用maven构建项目: <?xml version="1.0" encoding="UTF-8"?> <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

  • Kotlin 接口与 Java8 新特性接口详解

    前言 在看一本关于高性能编程的时候发现 Java8 中关于接口的新特性的介绍,这个特性是真的棒,解决了一个接口中有多个方法,但并不想实现该接口的类都去实现所有的方法,简单的说就是在类需要的情况再去重写接口.所以有了以下的特性出现. 接口增强 在 Java8 的中接口特性中增加以下俩种特性: 在接口中可以使用 default 关键字修饰默认方法或扩展方法,抽象方法因为其特性的原因无法使用 接口可以使用 static 声明为静态方法,可以通过类直接调用Android Studio 中使用 Java8

  • Kotlin中的sam(函数式接口)详解

    用lambda表达式去表示java中的匿名类实例 在使用java去给一个按钮设置监听我们通常会通过创建匿名类实例,如下 Button.setOnClickListener(new OnClickListener()){ @Override public void onClick(View v){ Toast.makeText(this,"Hello World",Toast.LENGTH_LONG).show() } } 在kotlin我们可以通过传递一个lambda表达式去代替这个实

  • 小谈Kotlin的空处理的使用

    近来关于 Kotlin 的文章着实不少,Google 官方的支持让越来越多的开发者开始关注 Kotlin.不久前加入的项目用的是 Kotlin 与 Java 混合开发的模式,纸上得来终觉浅,终于可以实践一把新语言. 本文就来小谈一下 Kotlin 中的空处理. 一.上手的确容易 先扯一扯 Kotlin 学习本身. 之前各种听人说上手容易,但真要切换到另一门语言,难免还是会踌躇是否有这个必要.现在因为工作关系直接上手 Kotlin,感受是 真香(上手的确容易) . 首先在代码阅读层面,对于有 Ja

  • 小谈angular ng deploy的实现

    Angular CLI 在 8.3.0 发布过一个新命令 ng deploy,可以将 Angular 应用部署到远程服务器或云存储上面,例如:Firebase hosting.Azure.GitHub pages等等,这也是算是 Angular CLI 最后一个将 Angular 应用从开发到部署打通全能选手工具了. 快速入门 这里我以ng-deploy-oss为示例,演示如何将 Angular 应用部署到七牛云. 1.创建一个新项目 ng new hello-world --defaults

  • 小谈php正则提取图片地址

    迷上了正则,不断尝试着新花招,首先感谢TNA 的非完全输出RSS,然后再次感谢SH的强迫性学习.没有TNA,我不会去看正则,更不知道世界上有种这么牛的表达式:不是SH的死活说他不懂不知道,我也不会硬着头皮去琢磨,去改进.达到同一个目的,正则的表达方式可以不唯一,没有做不到,只有你没想到.可以这样说吧,正则就是玩设定规律,我大爱这种东西.没有比设定规律筛选东西更让我兴奋.感到awesome的了. 分享一下在php环境下使用正则提取图片地址的一些小心得: 图片网址规范的html代码无非就是 复制代码

  • 浅谈Java8 判空新写法

    目录 引言 API介绍 1.Optional(),empty(),of(),ofNullable() 2.orElse(),orElseGet()和orElseThrow() 3.map()和flatMap() 4.isPresent()和ifPresent(Consumer<? super T> consumer) 5.filter(Predicate<? super T> predicate) 实战 例一 例二 例三 引言 在开发过程中很多时候会遇到判空校验,如果不做判空校验则

  • asp.net小谈网站性能优化

    当然,网站性能优化是多方面的,这里先谈一下这些天来的所获: 1.书写代码的习惯: 再复杂的逻辑,也是从最简单的开始.在书写代码的过程中,很多不好的规范都会影响网站的性能: 以下是整理出来的些许代码习惯: 1)字符串的比较 用 string.Empty 代替 " " 2)在遍历过程中,先定义好计数变量, 再遍历, 这样会减少每次遍历就分配一次内存空间: 复制代码 代码如下: int i; for( i=0; i<100;i++) { // codeing } 3)同样的,用 Str

  • 默默小谈PHP&MYSQL分页原理及实现

    在看本文之前,请确保你已掌握了PHP的一些知识以及MYSQL的查询操作基础哦. 作为一个Web程序,经常要和不计其数的数据打交道,比如会员的数据,文章数据,假如只有几十个会员那很好办,在一页显示就可以了,可是假如你的网站是几千甚至几十万会员的话,如果都在一页打开的话无论对浏览器还是观看者都是一种折磨. 相信每个学习PHP的新手都会对分页这个东西感觉很头疼,不过有了默默的这一水帖,你肯定会拍拍脑袋说,嘿,原来分页竟然如此简单?的确,现在请深呼吸一口新鲜的空气,仔细的听默默给你一点一点的分解. 假设

  • 小谈RADMIN的几个小技巧

    转载请保留版权信息!谢谢合作! by NetPatch welcome www.nspcn.org and www.icehack.com 最近做渗透测试时常碰到RADMIN一类的东西.. 一碰到此类的程序,一般我都会先看下对方把RADMIN的端口配置成什么..以及相应的PASS(加密过的) HKEY_LOCAL_MACHINE\SYSTEM\RAdmin\v2.0\Server\Parameters\Parameter //默认密码注册表位置 HKEY_LOCAL_MACHINE\SYSTEM

  • 小谈RADMIN爆破

    HKEY_LOCAL_MACHINE\SYSTEM\RAdmin\v2.0\Server\Parameters\Parameter //默认密码注册表位置 HKEY_LOCAL_MACHINE\SYSTEM\RAdmin\v2.0\Server\Parameters\Port //默认端口注册表位置 //把海阳读出来的,用逗号格开,然后用下面的代码转换就可以了 复制代码 代码如下: Dim theStr  theStr = InputBox( "请输入要转换的密码:", "输

  • golang中json小谈之字符串转浮点数的操作

    有时会有这种需求,将一个json数据形如: {"x":"golang", "y":"520.1314"} 中的y反序列化为浮点类型,如果这样写: package main import ( "encoding/json" "fmt" ) type JsonTest struct { X string `json:"x"` Y float64 `json:"y

  • Kotlin如何优雅地判断EditText数据是否为空详解

    快速上手 如果不知道如何在Kotlin中写一个相当简单的Java表达式.这里有一个简单的诀窍,就是在AndroidStudio的Java文件中编写一段代码,然后将其粘贴到kt文件中,它会自动转换为Kotlin. Kotlin优势 它更加易表现:这是它最重要的优点之一.你可以编写少得多的代码. 它更加安全:Kotlin是空安全的,也就是说在我们编译时期就处理了各种null的情况,避免了执行时异常.你可以节约很多调试空指针异常的时间,解决掉null引发的bug. 它可以扩展函数:这意味着,就算我们没

随机推荐