可选类型:可以用来表示可能缺失或者计算失败的值
举例:
let cities = ["Paris": 2241, "Madrid": 3165, "Amsterdam": 827, "Berlin": 3562]
let madridPopulation: Int? = cities["Madrid"]
madridPopulation 的类型是可选类型 Int?,⽽⾮ Int。⼀个 Int? 类型的值是 Int 或者特
殊的 “缺失” 值 nil。
当我们想要使用madridPopulation的时候,你也许会这样写:
if madridPopulation != nil {
print ("The population of Madrid is \(madridPopulation! * 1000)")
} else {
print ("Unknown city: Madrid")
}
又或许会这样写:
if let madridPopulation = cities["Madrid"] {
print ("The population of Madrid is \(madridPopulation * 1000)")
} else {
print ("Unknown city: Madrid")
}
当我们采用第一种方式的写法madridPopulation! ,!代表强制转换,将可选Int类型转为Int类型,又叫做强制解包。
第二种方式又叫做可选绑定机制,可以将madridPopulation定义与检验相结合,这样就不需要显式地使⽤强制解包运算符!
如果可以选择,强烈推荐使用可选绑定机制而非强制解包。每当你一不留神用到!的同时,可能有一只小猫就会死。
如果你有⼀个 nil 值,强制解包可能导致崩溃;可选绑定⿎励你显式地处理异常情况,从⽽避免运⾏时错误。
Swift 还给 ! 运算符提供了⼀个更安全的替代,?? 运算符。使⽤这个运算符时,需要额外提供
⼀个默认值,当运算符被运⽤于 nil 时,这个默认值将被作为返回值。
需要顺便说一句的是,当我们使用 optional ?? defaultValue 时,swift 标准库为我们做了哪些事?
如果 default 的值是通过某个函数或者表达式得到的,那么⽆论可选值是否为 nil,defaultValue 都会被求值。?? 运算符应该只在可选值参数是 nil 时才对 defaultValue 参数进⾏求值。而我们可以通过显示闭包来达到目的 optional ?? {defaultValue}
Swift 标准库中的定义通过使⽤ Swift 的 @autoclosure 类型标签来避开创建显式闭包的需求。它会在所需要的闭包中隐式地将参数封装到 ?? 运算符。这样⼀来,用户⽆需再显式地创建闭包封装 defaultValue 参数。
那再思考一个问题:当多个可选值组合再一起的时候,写起来是不是很麻烦?
swift 有一个特殊的机制,叫可选链。
它用于再被嵌套的类或者结构体中,对方法或属性进行选择。
下面,我想拿到客户的地址
struct Order {
let orderNumber: Int
let person: Person?
}
struct Person {
let name: String
let address: Address?
}
我们可以使用显式解包运算符:order.person!.address! ,上面说过,如果任意中间数据缺失,可能会导致运行时异常。所以使用可选绑定更安全。
if let myPerson = order.person {
if let myAddress = myPerson.address {
// ...
if let myPerson = order.person ,let myAddress = myPerson.address {// ...
是不是很繁琐,或者你允许这么长的话。
使用可选链的话:使用问号运算符来尝试对可选类型进行解包,而不是强制解包。当任意一组成项失败时,整条语句将返回nil
if let myState = order.person?.address{
} else {
}
上面也可以利用guard 语句尽早退出作用于
guard let let myState = order.person?.address else { return }
为什么要用可选值
swift的类型系统相当严格,一旦有可选类型,就必须处理可能为nil的问题。
在许多语⾔中并没有可选,空指针是危险的来源。Objective-C 稍好⼀些,你可以安全地向 nil 发送消息,然后根据不同的返回值的类型,得到像是 nil、数字 0 这样的零值。
为什么在 Swift 中要改变这种特性呢???
选择显式的可选类型更符合 Swift 增强静态安全的特性。强⼤的类型系统能在代码执⾏前捕获到错误,⽽且显式可选类型有助于避免由缺失值导致的意外崩溃。
Objective-C 采⽤的默认近似零的做法有其弊端,可能你会想要区分失败 (键不存在于字典) 和成功返回 nil (键存在于字典,但关联值是 nil) 两种情况。若要在 Objective-C 中做到这⼀点,你只能使⽤ NSNull。
虽然在 Objective-C 中将消息发送给 nil 是安全的,但是使⽤它们往往并不安全。
⽐⽅说我们想创建⼀个属性字符串 (attributed string)。如果我们传递 nil 作为 country 的值,那么 capital 也会是 nil,但是当我们试图将 NSAttributedString 初始化为 nil 时,将会引起崩溃:
- (NSAttributedString *)attributedCapital:(NSString *)country{
NSString *capital = self.capitals[country];
NSDictionary *attr = @{ /* ... */ };return [[ NSAttributedString alloc] initWithString:capital attributes:attr ];
}
虽然像上⾯那样的崩溃并不经常发⽣,不过⼏乎每个开发者都写过像这样导致崩溃的代码。⼤多数时候,在调试阶段这些这些崩溃的导⽕索就会被发现,不过偶尔难免不知不觉就发布了代码,⼀些情况下,变量还可能出乎意料的是 nil。因此,许多程序员使⽤断⾔来显式地标⽰这这种情况。例如,我们可以添加⼀个 NSParameterAssert 以确保当 country 是 nil 时就⽴即崩溃:
- (NSAttributedString *)attributedCapital:(NSString *)country {
NSParameterAssert(country);
NSString *capital = self.capitals[country];
NSDictionary *attr = @{ /* ... */ };
return [[ NSAttributedString alloc] initWithString:capital attributes:attr ];
}
如果我们传递了⼀个 country 值,但是 self.capitals 中并没有键与之匹配该怎么办呢?这种情况发⽣的概率很⼤,特别是当 country 来源于⽤⼾输⼊时。在这种情况下,capital 将会是nil,我们的代码仍然会崩溃。当然,修复的⽅法很简单。不过,我们关注的重点是,在 Swift 中使⽤nil 编写健壮的代码⽐在 Objective-C 中容易。
最后,从本质上看,使⽤断⾔是⾮模块化的。假设我们要实现⼀个 checkCountry ⽅法⽤于确认是否⽀持⾮空 NSString *。可以很容易地在上述代码中加⼊该⽅法:
- (NSAttributedString *)attributedCapital:(NSString*)country {
NSParameterAssert(country);
if (checkCountry(country)) {
// ...
}
}
现在的问题是:checkCountry 函数是否也应该断⾔它的参数不是 nil?⼀⽅⾯,它不应该 因为我们⽅才刚在 attributedCapital ⽅法中执⾏了检验的代码。另⼀⽅⾯,如果checkCountry 函数仅适⽤于⾮ nil 值,我们还是应该复制上述断⾔。我们被迫在暴露不安全接⼝和复制断⾔之间进⾏选择。还有⼀种做法是,可以给签名添加⼀个 nonnull 标注,当该⽅法被⼀个可能为 nil 的值调⽤时,它会发出警告,但这种做法在⼤多数 Objective-C 的代码库中并不常⻅。在 Swift 中,事情要来得容易得多:函数签名可以显式地使⽤可选值来提⽰⼀个值可能为 nil。与他⼈协同编码时,这是⼗分宝贵的信息。下⾯的签名提供了⼤量信息:
func attributedCapital(country: String) -> NSAttributedString?
我们不仅被警告有失败的可能性,还知道了必须传递⼀个 String 作为参数 —— 且不能是 nil值。像上⾯⼀样的崩溃将不会再发⽣。此外,这也是编译器要检验的信息。⽂档很容易过时,但是你可以永远依赖函数签名。
在 Objective-C 中处理标量值时,可选问题显得更加棘⼿。不妨看看下⾯的⽰例,尝试在⼀个字符串中查找⼀个特定关键词的位置:
NSString *someString = ...;
if ([ someString rangeOfString:@"swift"].location != NSNotFound) {
NSLog(@"Someone mentioned swift!");
}
看起来毫⽆问题:如果 rangeOfString: 没有找到字符串,location 就会被设置为NSNotFound。NSNotFound 被定义为 NSIntegerMax。这段代码⼏乎是正确的,第⼀眼很难看到它的问题:若 someString 是 nil,rangeOfString: 将返回⼀个属性全为零的结构体,location 将返回 0。接着所做的判断结果若为真,if 语句中的代码将被执⾏。
如果有可选值的话,这⼀切将免于发⽣。如果我们想将这份代码转为 Swift,会需要做出⼀些结构性的改变。上⾯的代码会被编译器拒绝,类型系统也不会允许你在⼀个 nil 值上运⾏rangeOfString:。因此,⾸先需要将它解包:
if let someString = ... {
if someString.rangeOfString("swift").location != NSNotFound {
print ("Found")
}
}
类型系统将有助于你捕捉难以察觉的细微错误。其中⼀些错误很容易在开发过程中被发现,但是其余的可能会⼀直留存到⽣产代码中去。坚持使⽤可选值能够从根本上杜绝这类错误。
网友评论