# Kotlin教程 - 4 非空安全

null 值是一个特殊的值,表示一个变量或常量 没有值,是空的值。当一个变量是 null,我们使用这个变量进行操作的时候,在 Java 中就会报空指针异常(NullPointerException),Kotlin 针对这种情况进行了改良,提前在编译期间进行了相关的限制。

# 4.1 可空类型和非空类型

在 Java 中声明了一个变量赋空值是正常的。但是在 Kotlin中,所有的数据类型默认是非空的

我们在前面将字符串转换为整数的时候,使用的是如下代码:

fun main() {
    val str = "123"
    val value1 = str.toInt()
    val value2 = str.toIntOrNull()

    println(value1)
    println(value2)
}
1
2
3
4
5
6
7
8

我们在转换的时候,是没有添加类型的,如果添加上类型的话,会发现有问题:

fun main() {
    val str = "123"
    val value1:Int = str.toInt()
    val value2:Int = str.toIntOrNull()      // 这句代码会报错

    println(value1)
    println(value2)
}
1
2
3
4
5
6
7
8

上面的代码 val value2:Int = str.toIntOrNull() 会报错,因为如果 str 是不能转换为数字的字符串,结果将是 null,而 val value2:Int 是不能被赋空值的,所以需要将 val value2:Int 修改为 val value2:Int? 表示 value2 可以为空值。

而因为 toInt() 在转换的时候,如果字符串不能被转换为数字,会报错,程序会终止,所以只要程序正常,value1 必定不会为空。

  • 默认情况下,Kotlin 中所有类型都是非空类型,因此变量、常量默认不能为 null

  • 如果想要使用可空类型,需要使用 ? 来标记一个变量、常量为可空类型。

再举个栗子:

fun main() {
    var value1: String? = null      // 可以赋空值
    var value2: String = null       // 报错,非空类型,不可以赋空值

    value1 = "Doubi"
    println(value1)				// 输出:Doubi
}
1
2
3
4
5
6
7

所以添加 ? 表示可空,不添加 ? 默认是非空类型。

# 4.2 类型提升

为了保证空安全特性,Kotlin 的流分析(flow analysis)已经考虑了空特性。如果一个可空对象不可能有空值,那么就会被当作非空对象处理。

举个栗子:

下面的代码编译是会报错的。

fun main() {
    var str: String? = null
    println(str.length)     // 编译出错,str为null
}
1
2
3
4

但是如果我们对str进行非空判断,则确保了str不会为空,编译就不会报错了:

fun main() {
    var str: String? = null
    if (null != str) {
        print(str.length)   // 已经确保 str 不为空,不会编译出错
    }
}
1
2
3
4
5
6

# 4.3 空安全调用

在Kotlin中,使用 ?. (空安全调用操作符)来进行空安全调用。空安全调用表示如果这个变量是空值,则不会调用它的函数或者访问它的属性。

fun main() {
    var str: String? = null
    var length:Int? = str?.length
    println(length)			// 输出: null
}
1
2
3
4
5

在上面的代码中,虽然 str 为 null ,但是使用了空安全调用,所以不会报错。

使用 if-else 也可以实现效果,为什么要使用空安全调用操作符呢?

一个是更简洁,二是空安全调用操作符可以进行多个函数的链式调用。

举个栗子:

data class Person(val name: String?)

fun main() {
    val person: Person? = Person("Doubi")

    // 使用if判断
    if (null != person) {
        if (null != person.name) {
            val length = person.name.length
            println(length)
        }
    }

    // 使用空安全调用操作符,可以链式调用
    val length = person?.name?.length
    println(length)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 4.4 let函数

很多情况下,空安全调用 ?. 会结合 let 函数来使用。

let 是 Kotlin 标准库中的一个函数,它用于执行一个作用域函数(scope function),并将调用者作为 lambda 表达式的参数传递给这个函数。let 的主要目的是在对象上执行某些操作,并且可以方便地处理 null 值。

举个栗子:

fun main() {
    val name:String? = "Doubi"

    val content: String? = name?.let {	// let是一个lambda表达式,参数是调用者本身
        println(it.length)
        "Hello ${it}"
    }
    println(content)
}
1
2
3
4
5
6
7
8
9

let 是一个lambda表达式,参数是调用者本身,也就是 it 就是 name

name?.let {} 表示如果name 不为空就执行 lambda 代码块中的内容,并将 lambda 代码块最后一行内容返回作为结果。

name?.let {} 有点像 if 判断 name 是否为空,如果不为空就执行操作,例如:

fun main() {
    val name:String? = "Doubi"
    var name2:String? = null

    name?.let {				// name 不为空就执行操作,将name赋值给name2
        name2 = name
    }
    println(name2)
}
1
2
3
4
5
6
7
8
9

let 函数和 Lambda 表达式后面在讲解,如果这里不清楚,不必慌张。

# 4.5 非空断言

在Kotlin中,使用 !! 来进行非空断言。非空断言表示这个变量一定不是空值。

举个栗子:

fun main() {
    var str: String? = null
    print(str!!.length)
}
1
2
3
4

虽然 str 是空的,但是使用了 !! 来告诉编译器,str 一定不为空,所以才通过编译。但是上面的代码运行会报错。

所以使用非空断言的时候,一定要确保变量一定不为空,如果它是空值则会抛出异常。

使用非空断言,可以将可空类型转换为非空类型。

fun main() {
    var str1: String? = "abc"
    var str2: String = str1!!        // 需要非空断言才能赋值
}
1
2
3
4

可空类型 是不能直接赋值给 非空类型 的,所以在上面的代码中,需要使用非空断言,将可空类型转换为非空类型才能赋值。

当然使用之前的类型提升也是可以的:

fun main() {
    var str1: String? = "abc"

    if (null != str1) {
        var str2: String = str1        // str1已经做了类型提示,可以赋值
    }
}
1
2
3
4
5
6
7

# 4.6 Elvis 运算符?:

Elvis 运算符 ?: 也叫空合并操作符,用于判断一个表达式是否为null,如果表达式结果不为 null ,则返回左侧表达式的结果,否则返回右侧表达式的结果,和有的语言的三元运算符有些类似。

举个栗子:

fun main() {
    var name:String? = null
    var fullName = name ?: "Doubi"
    print(fullName) 		// 输出:Doubi
}
1
2
3
4
5

上面 var fullName = name ?: "Doubi" 表示先判断 name 是否为 null,如果不为 null ,则将 name 的值赋值给 fullName,否则将"Doubi" 赋值给 fullName

空合并操作符还经常结合let函数来使用,举个栗子:

fun main() {
    var str:String? = "doubi"

    str = str?.let { it.capitalize() } ?: "Niubi"
    println(str)
}
1
2
3
4
5
6

在上面的代码中,首先判断 str 是否为空,如果不为空,就在 let 函数中将字符串首字母变成大写,如果 str 为空,就将 "Niubi" 赋值给 str