在编程世界中有一种非常通用的模式,那就是某个操作是否要返回一个有效值。
在 Objective-C
中,对 nil
发送消息是安全的。如果这个消息签名返回一个对象,那么
nil
会被返回;如果消息返回的是一个结构体那么它的值都将为零。但在实际开发中我们常常会碰到由于对象为 nil
而导致的崩溃或者不想出现的问题,比如数组、字典、block的判空处理等等。
Tony Hoare在1965年设计了 null
引用,他对此设计表示痛心疾首,并将这个问题称为“价值10亿美元的错误”:
那时候,我正在为一门面向对象的语言(ALGOL W)设计第一个全面的引用类型系统。我的目标是在编译器自动执行的检查的保证下,确保对于引用的所有使用都是安全的。但是我没能抵挡住
null
引用的诱惑,因为它太容易实现了。这导致了不计其数的错误,漏洞以及系统崩溃。这个问题可能在过去四十年里造成了有十亿美元的损失。
前面叙述这么多是为了引出编程语言中更安全的设计 - swift中的可选类型
swift定义后缀 ?
来作为标准库中的定义的命名型类型 Optional<Wrapped>
的语法糖。换句话说,下面两个 声明是等价的:
var optionalInteger: Int?
var optionalInteger: Optional<Int>
在上述两种情况下,变量 optionalInteger
都被声明为可选整型类型。注意在类型和 ?
之间没有空格。
类型 Optional<Wrapped>
是一个枚举,有两个成员,none
和 some(Wrapped)
,用来表示可能有也可能没有的值。任意类型都可以被显式地声明(或隐式地转换)为可选类型。如果你在声明或定义可选变量或属性的时候没有提供初始值,它的值则会自动赋为默认值 nil
。
如果一个可选类型的实例包含一个值,那么你就可以使用后缀运算符 !
来获取该值(即强制解包),正如下面描述的:
optionalInteger = 42
optionalInteger! // 42
使用 !
运算符解包值为 nil
的可选值会导致运行错误。
你也可以使用可选链式调用和可选绑定来选择性地在可选表达式上执行操作。如果值为 nil
,不会执行任何操作,因此也就没有运行错误产生。
- 处理可选值值缺失的方法
一个可选的值是一个具体的值或者是 nil
以表示值缺失。在类型后面加一个问号来标记这个变量的值是可选的。你可以一起使用 if
和 let
来处理值缺失的情况:
var optionalString: String? = "Hello"
print(optionalString == nil) //false
var optionalName: String? = "John Appleseed"
var greeting = "Hello!"
if let name = optionalName {
greeting = "Hello, \(name)"
}
上面的代码中如果变量optionalName
的可选值是 nil
,条件会判断为 false
,大括号中的代码会被跳过。如果不是 nil
,会将值解包并赋给 let
后面的常量 name
,这样代码块中就可以使用这个值了。
还有另外一种处理值缺失的方法,使用 ??
操作符来提供一个默认值。如果可选值缺失的话,可以使用默认值来代替。
let nickName: String? = nil
let fullName: String = "John Appleseed"
let informalGreeting = "Hi \(nickName ?? fullName)"
因为 nickName
为 nil
,所以 informalGreeting
的值就变成了 "Hi John Appleseed"
。
- 隐式解析可选类型
当可以被访问时,Swift 语言定义后缀 !
作为标准库中命名类型 Optional<Wrapped>
的语法糖,来实现自动 解包的功能。换句话说,下面两个声明等价:
var implicitlyUnwrappedString: String!
var explicitlyUnwrappedString: Optional<String>
由于隐式解包修改了包涵其类型的声明语义,嵌套在元组类型或泛型的可选类型(比如字典元素类型或数组元素
类型),不能被标记为隐式解包。例如:
let tupleOfImplicitlyUnwrappedElements: (Int!, Int!) // 错误
let implicitlyUnwrappedTuple: (Int, Int)! // 正确
let arrayOfImplicitlyUnwrappedElements: [Int!] // 错误
let implicitlyUnwrappedArray: [Int]! // 正确
由于隐式解析可选类型和可选类型有同样的表达式析可选类型。比如,你可以将隐式解析可选类型的值赋给变量、常量和可选属性,反之亦然。
正如可选类型一样,你在声明隐式解析可选类型的变量或属性的时候也不用指定初始值,因为它有默认值 nil
。
可以使用可选链式调用来在隐式解析可选表达式上选择性地执行操作。如果值为 nil
,就不会执行任何操作,因此也不会产生运行错误。
- 使用可选链式调用代替强制展开
通过在想调用的属性、方法、或下标的可选值后面放一个问号 ?
,可以定义一个可选链。这一点很像在可选 值后面放一个叹号 !
来强制展开它的值。它们的主要区别在于当可选值为空时可选链式调用只会调用失败,然而强制展开将会触发运行时错误。
为了反映可选链式调用可以在空值 nil
上调用的事实,不论这个调用的属性、方法及下标返回的值是不是可选值,它的返回结果都是一个可选值。你可以利用这个返回值来判断你的可选链式调用是否调用成功,如果调用 有返回值则说明调用成功,返回 nil
则说明调用失败。
特别地,可选链式调用的返回结果与原本的返回结果具有相同的类型,但是被包装成了一个可选值。例如,使用 可选链式调用访问属性,当可选链式调用成功时,如果属性原本的返回结果是 Int
类型,则会变为 Int?
类型。
代码示例,首先定义两个类 Person
和 Residence
:
class Person {
var residence: Residence?
}
class Residence {
var numberOfRooms = 1
}
Residence
有一个 Int
类型的属性 numberOfRooms
,其默认值为 1 。 Person
具有一个可选的 residence
属性,其类型为 Residence?
。
假如你创建了一个新的 Person
实例,它的 residence
属性由于是是可选型而将初始化为 nil
,在下面的代码中, john
有一个值为 nil
的 residence
属性:
let john = Person()
如果使用叹号 !
强制展开获得这个 john
的 residence
属性中的 numberOfRooms
值,会触发运行时错误,因为这时 residence
没有可以展开的值:
let roomCount = john.residence!.numberOfRooms // 这会引发运行时错误
john.residence
为非 nil
值的时候,上面的调用会成功,并且把 roomCount
设置为 Int
类型的房间数量。正如上面提到的,当 residence
为 nil
的时候上面这段代码会触发运行时错误。
可选链式调用提供了另一种访问 numberOfRooms
的方式,使用问号 ?
来替代原来的叹号 !
:
if let roomCount = john.residence?.numberOfRooms {
print("John's residence has \(roomCount) room(s).")
} else {
print("Unable to retrieve the number of rooms.")
}
// 打印 “Unable to retrieve the number of rooms.”
在 residence
后面添加问号之后,Swift
就会在 residence
不为 nil
的情况下访问 numberOfRooms
。
因为访问 numberOfRooms
有可能失败,可选链式调用会返回 Int?
类型,或称为“可选的 Int
”。如上例所 示,当 residence
为 nil
的时候,可选的 Int
将会为 nil
,表明无法访问 numberOfRooms
。访问成功时,可选的
Int
值会通过可选绑定展开,并赋值给非可选类型的 roomCount
常量。
要注意的是,即使 numberOfRooms
是非可选的 Int
时,这一点也成立。只要使用可选链式调用就意味着 numberOfRooms
会返回一个 Int?
而不是 Int
。
可以将一个 Residence 的实例赋给 john.residence ,这样它就不再是 nil 了:
john.residence = Residence()
john.residence
现在包含一个实际的 Residence
实例,而不再是 nil
。如果你试图使用先前的可选链式调用访问 numberOfRooms
,它现在将返回值为 1 的 Int?
类型的值:
if let roomCount = john.residence?.numberOfRooms {
print("John's residence has \(roomCount) room(s).")
} else {
print("Unable to retrieve the number of rooms.")
}
// 打印 “John's residence has 1 room(s).”
- 强制解包的时机
当你能确定你的某个值不可能是
nil
时使用!
进行强制解包,你应当不希望如果它不巧意外地是nil
的话,这句程序会直接挂掉。
总之,使用强制解包时一定要慎重!!!