十、真、假和未定:可空性
在学校里,你学到了对与错的二分法,你可能听说过没有别的了。到目前为止,读完这本书,你已经知道在 Kotlin 中存在一个布尔类型Boolean
,它有精确的可能值:true
和false
。句号。真的吗?
如果你想想现实生活,经验会告诉你一些别的东西。问某人:明天会下雨吗?答案可能是肯定的,也可能是否定的。不过,老实说,没有人有百分之百的把握知道答案。因此我们有真、假、和未定(或未知)。这种三分法被称为三值逻辑(也称为三值逻辑、三值逻辑、或三值逻辑)。为什么我们在这里谈论这个?这不是一本哲学书,是吗?对于类和对象,我们已经指出,计算机程序需要模拟真实世界的场景;因此,我们需要一些在计算机语言中既不是true
也不是false
的东西。
什么是空值
即使计算机语言开发人员不是真正的健全的哲学家,或者可能只是没有意识到这种三分法,未定从计算机语言历史的一开始就已经存在。只是没人这么叫它。比方说,你需要一个代表列表大小的变量。根据具体情况,大小为零的列表可能是有意义的,出于编码的原因,我们可能需要表明列表尚未定义。我们能做什么?嗯,大小的范围是 0,1,2,3,...
,所以我们只取一个通常没有意义的数字,定义这个来代表一个还没有定义的。你能猜出这是什么数字吗?一个可能的答案是1
。
有了数组,情况就更加多样化了。通常数组由某个指针变量定义,该变量指向计算机内存中数组的第一个元素。如果我们需要说数组还没有定义,我们使用一个没有意义的指针值。这可能是1
,但更实际的是值0
。由于技术原因,在内存地址0
实际启动一个数组是不可能的,所以未决定的0
是一个有效的选择。为了阐明真实内存地址和未确定内存地址之间的区别,表示后者的0
只是获得了一个新名称:null
。更有趣的是,在面向对象中我们还有指向类实例的指针,这些指针可以是null
以及表示未决定的或尚未定义的*。
除了紧接着true
和false
的第三个伪布尔undecided
,我们还有另一个用于数组和对象的undecided
。它们之间有什么联系?请看下面的代码片段:
val b:Boolean = ... // some condition
if(b) {
... // do something
} else {
... // do something else
}
这里我们分支讨论是否满足某些条件。由于可以用null
来表达它们的对象还没有被定义,在许多情况下你会有一个扩展版本:
val instance = ... // some object
val b:Boolean = ... // some condition
if(instance == null) {
... // do something
} else if(b) {
... // do something else
} else {
... // do something else
}
在这里,我们基于某事是真还是假,以及某事是否未定义来做出决定。现在,如果我们在一种虚构的计算机语言中有第三个布尔值undecided
,它可以读作
val b:Boolean = ... // some three-valued condition
ifundecided(b) {
... // do something
} if(b) {
... // do something else
} else {
... // do something else
}
这两个构造,一个虚构的三值布尔和一个null
对象引用,表达了相同的代码。这是两个犹豫不决的人相遇的地方。因为在 Kotlin 和我所知道的任何其他语言中,都不存在第三个布尔值,我们必须继续使用null
来实现这个目的。
null
有一个严重的问题:你还记得解引用操作符.
是做什么的吗?它从.
的左边取物体,用右边瞄准一个属性或函数。很自然地,对于未决定或null
的对象,这种取消引用是没有意义的。不幸的是,许多计算机语言在这里都不太好,如果我们试图取消对null
的引用,或者至少中断程序流并指示一个无效的程序流活动,就会崩溃。这种可空性给程序带来了不稳定性,困扰了几代开发人员。但是好处大于问题,所以仅仅避免可空性从来没有被认为是一个真正的选择。
Kotlin 内部如何处理可空性
Kotlin 引入了一些关于可空性的新概念,允许使用可空性,但避免了大多数相关的陷阱。首先,我们注意到默认情况下,Kotlin 不允许null
值在你的应用中偷偷摸摸。类似这样的东西
var p:SomeType = ...
...
p = null
任何类型的属性都不允许使用。这同样适用于构造函数和函数调用:
class A(var p:SomeType) ...
A(null) // does not compile
fun f(p:SomeType) { ... }
f(null) // does not compile
有了这样的不可空属性,通过.
的解引用将总是成功的。另一方面,如果我们希望一个属性、构造函数参数或函数参数可以为空,我们必须加上一个问号(?)到类型:
var p:SomeType? = ...
p = null // OK
class A(var p:SomeType?) ...
A(null) // OK
fun f(p:SomeType?) { ... }
f(null) // OK
注意
因为您必须添加一些东西来允许可空性,所以 Kotlin 稍微倾向于非可空性。事实上,在很多情况下你可以避免使用null
值,如果是这样,你很有可能有一个好的应用设计。
对于这种可空类型,Kotlin 知道通过.property
或.function()
的解引用可能会失败,并禁止使用它们:
var p:SomeType? = ...
...
p.property // does not compile
p.function() // does not compile
如果值碰巧不是null
,那么这也是被禁止的。
那么我们如何使用这样的可空属性呢?答案是我们必须使用 Kotlin 提供的空安全操作符之一。因此,对于解引用.
,有一个空安全变量?.
,可以用于可空属性:
var p:SomeType? = ...
...
p?.property // OK
p?.function() // OK
不同的是,如果p
是null
,p?.property
本身求值为null
,p?.function()
中的函数不会被调用,调用也求值为null
。
var p:SomeType? = null
val res:TypeOfProperty? = p?.property // -> null
val res2:TypeOfFunct? = p?.function() // -> null
// ... and function() not invoked
另一个被设计成零安全的操作员是埃尔维斯操作员?:
。我们已经知道这个了。如果那个不是null
,它就评估到它的左边,否则就评估到它的右边。
var p:String? = "Hello"
var s1 = p?:"default" // -> "Hello"
p = null
var s2 = p?:"default" // -> "default"
Kotlin 不能总是知道一个属性是否可以为空。在这种情况下,使用!!
操作符可能会有所帮助,它也被称为 not null 断言操作符。它取它的左边,不管它是否能评估为null
,都假定它不能是null
。如果你的应用需要别人写的程序,你可能偶尔会用到它。对于用其他语言编写并且没有应用 Kotlin 的空检查机制的库来说尤其如此。当然,如果你试图通过使用.
取消引用它,而这个值意外地是null
,你的应用将会崩溃。尽一切可能避免这种情况,或者知道该怎么做。
警告
使用!!
你基本上绕过了 Kotlin 的空检查机制,所以尽量避免它。
var p:String? = ...
// for whatever reason we know that p cannot be null
val len = p!!.length
// valid, because the !! indicates it cannot be null
// If it accidentally _is_ null, we'll crash here.
顺便说一下,如果你应用!!
,Kotlin 是相当聪明的。在同一个函数的后续语句中,它记得我们应用了这个断言,并继续假设值不能是null
。你可以写作
var p:String? = ...
val len = p!!.length
val intVal = p.toInt()
在这里,最后一个语句只编译,因为在这一行之前的某个地方有!!
。*
版权属于:月萌API www.moonapi.com,转载请注明出处