null 也能 toString()
话说我有一段代码,经过运算会得到一个变量,它可能为 null
:
fun resolveCurrentMatchPoint(): MatchPoint?{
return ...
}
代码语言:javascript复制val matchPoint = resolveCurrentMatchPoint()
这时候呢,为了检验我的算法是否符合预期,通常我会在这里把这个 MatchPoint 打印出来,不巧的是 Android 的日志函数签名长这样:
Log.java
代码语言:javascript复制public static int d(String tag, String msg)
所以我每次就得这么写:
代码语言:javascript复制matchPoint?.let {
Log.d(TAG, it.toString())
}
可是这么写的话对于返回 null
的情况就没有日志了,所以只好用朴实的 if...else...
来写:
if(matchPoint == null){
Log.d(TAG, "null")
} else {
Log.d(TAG, matchPoint.toString())
}
这样看上去一点儿都不美好伐。如果我们写的是 Java 的话,也就只好认命了,可现在写的是 Kotlin 哎。
我把上面的代码改成了下面这样,想想也算是大无畏的革命精神嘛,我不 crash 谁 crash:
代码语言:javascript复制Log.d(TAG, matchPoint.toString())
我:大不了就 crash,谁怕谁 Kotlin:谁要你 crash 了,你看看清楚!
嗯?难道这样运行,就算遇到 null 也没问题?我很好奇它们究竟对 toString
做了啥,于是打开源码一看:
/**
* Returns a string representation of the object. Can be called with a null receiver, in which case
* it returns the string "null".
*/
public fun Any?.toString(): String
这,居然是个扩展方法,障眼法啊。如果遇到 receiver
为 null
,那么就直接返回 null
,可以可以,这很 Kotlin。
INVOKESTATIC java/lang/String.valueOf (Ljava/lang/Object;)Ljava/lang/String;
这个 toString
的扩展呢,在编译期被编译成了 String.valueOf
,好吧,一点儿毛病都没有,一个函数的参数为 null
,比起 receiver
为 null
处理起来要容易得多。
所以下面的写法也是可以通过编译的:
代码语言:javascript复制null.toString()
简直就是魔鬼的步伐了。
经常遇到为 null 的数值,判空判到心碎
之前在 Kotlin 论坛上面看到一个帖子,说一哥们经常遇到数值为 null
的情况,期待能有什么特性帮到他。
var first: Int = ...
var second: Int = ...
val result = first / second // ERROR!!
如果能对 null
做默认处理,例如如果运算数为 null
,那么返回 null
,那么前面的代码以目前的情形就只能写成:
val result = if(first == null || second == null){
null
} else {
first / second
}
而对于除数为 0
的情况,还要做特殊处理,不然就。。。
Exception in thread "main" java.lang.ArithmeticException: / by zero
at com.bennyhuo.kotlin.ext4nulls.MainKt.main(main.kt:37)
那么我们让除数为 0 的情况也返回 null
,就写成了下面这样:
val result = if(first == null || second == null || second == 0){
null
} else {
first / second
}
额,每次都这么写,想想就难受。怎么办?
代码语言:javascript复制operator fun Int?.div(other: Int?)
= if(this == null || other == null || other == 0) null
else this / other
我们为可空的 Int
类型定义扩展运算符 div
其实就是 /
,一旦有了这个,后面的事儿就好办了:
val result: Int? = first / second
就算 first
、 second
为 null
或者 为 0 我们也不怕了。
如何正确对待可空类型?
前面给大家介绍了如何用扩展方法来帮助我们处理可空类型的问题。最初接触 Kotlin 的时候,确实有点儿不适应这种类型系统,写点儿代码好麻烦啊,怎么处处都得考虑变量是不是为空的问题 —— 虽然我很喜欢这个东西,当年知道 swift 有这样的特性的时候也曾羡慕不已 —— 这不适应的原因就是 Java 把我们惯坏了。
首先大家必须明确一点,那就是这个空类型安全的特性是非常棒的,从此与空指针说拜拜也不是吹的,原因很简单,这个特性强制要求开发者提高开发意识,每定义一个变量都对它是否可为 null
了如指掌,处处小心,自然也就不会有问题。所以任何情况下,不负责任的定义可空类型都是不好的写法。
var neverDoThis: String? = "If not necessary"
其次,Kotlin 编译器做了很多工作帮我们识别出那些虽然被定义为可空类型但却一定不为空的变量,这种变量通常也得是不可变的,也就是说,定义变量时,能定义只读变量就绝对不定义可变变量 —— 这时候,大家能体会到为什么 Kotlin 的函数参数都是只读变量了吗?
代码语言:javascript复制fun getView(container: View?, position: Int): View{
if(container == null){
container = inflater.inflate(...) // ERROR !!
}
...
}
再者,Kotlin 也提供了很多的扩展来帮助我们与可空类型“周旋”,例如:
代码语言:javascript复制matchPoint?.let {
Log.d(TAG, it.toString())
}
最后,就算真的遇到了 null
,我们也可以很方便的运用 elvis 运算符来提供 null
对应的默认值或者抛出异常:
fun needANonNullMatchPoint(): MatchPoint
= resolveCurrentMatchPoint()?: throw IllegalStateException()
如果你想很好的适应 Kotlin 的可空类型,你必须慢慢养成“多用不可空类型,多用只读变量”的习惯,Kotlin 提供了很好的语法特性让我们去适应这样的要求,再辅以扩展成员,就再也不用担心写不出好的代码了。