可选值
-
Swift 对“东西不存在”「nil」,“存在并且为空”「Void」以及”不可能发生“「Never」做了仔细的区分
-
Optional 本质上是通过枚举Enum 实现的
enum Optional<Wrapped>{ case none case some(Wrapped) }
- Optional | 哨岗值:由于底层是Enum,因此Optional 如其他所有枚举类型一般,或许其中关联值的唯一办法就是通过「模式匹配」。这决定了可选值不同于哨岗值在任何情况都会返回一个“有效值”,除非显式的检查并解包,否则不可能意外的获取到一个Optional 中包装的值。
-
概览
-
if let —— 【当一个值不为nil 时执行】
// 最原始的写法,暴露Optional 作为Enum 的.some 和.none case var array = ["one", "two", "three"] switch array.firstIndex(of: "four"){ case .some(let idx): array.remove(at: idx) case .none: break } // Swift 内置语言简化 switch array.firstIndex(of: "four"){ case let idx?: // 用?代替.some array.remove(at: idx) case nil: // 用nil 字面量匹配.none break } /* 虽然好了不少,但还是略显笨拙,一点都不swift */ // if-let 进行可选值绑定 if let idx = array.firstIndex(of: "three"){ array.remove(at: idx) } // if-let 结合布尔表达式 if let idx = array.firstIndex(of: "three"), idx != array.startIndex{ array.remove(at: idx) } /* if-let 的可选值绑定(optional binding):if-let 语句会检查可选值是否为nil,如果不为nil,便会解包可选值。若解包成功,idx 的类型就为Int,而不再是Optional(Int),并且idx 也只在这个if-let 的作用域中有效 */ // 另外,一个if 语句可以绑定多个值 // 而且,后续的解包过程可以使用之前成功解包出的结果(下url、data) let urlString = "https://www.objc.io/logo.png" if let url = URL(string: urlString), let data = try? Data(contentOf: url), let image = UIImage(data: data) { let view = UIImageView(image: image) PlaygroundPage.current.liveView = view }
-
while let —— 【当一个条件返回nil 时终止循环】
while let line = readLine(){ // readLine() 从标准输入读取内容,返回一个可选字符串。到达末尾时返回nil print(line) } while let line = readLine(), !line.isEmpty{ // 遇到EOF 或者空行都将终止循环 print(line) } // 与Sequence 协议下的迭代器结合 let array = [1,2,3] var iterator = array.makeIterator() while let i = iterator.next(){ print(i, terminator: " ") }
-
双重可选值:可选值允许嵌套
- 嵌套元素类型:Optional<Optional<Int>>
-
If-var & while-var:允许解包元素在作用域中改变值
let number = "1" if var i = Int(number){ i += 1 print(i) }// 2
-
延长解包可选值的作用域 —— guard let
// 利用Swift 的延迟初始化(defered initialization) // 即提前声明,推迟定义 extension String{ var fileExtension: String?{ let period: String.Index if let idx = lastIndex(of: "."){ period = idx }else{ return nil } let extensionStart = index(after: period) return String(self[extensionStatrt...]) } } "hello.txt".fileExtension // Optional("txt") // guard let 等价于 if not let extension String{ var fileExtension: String? { guard let period = lastIndex(of: ".") else{ return nil } let extensionStatt = index(after: period) return String(self[extenisonStart...]) } }
-
可选链:
-
当从源头开始的一系列函数调用都牵扯可选值时,如:
extension Int{ var half: Int?{ guard self < -1 || self > 1 else{return nil} return self/2 } } 20.half?.half?.half // Optional(2)
可见,因为调用half 返回一个可选值,但在对三次half 调用之后,返回的是Int? 而非Int??? 。这是因为编译器自动帮我们展平了结果类型。虽然后者会清晰的标识解包过程是在哪一部分失败的,但会让结果变得非常复杂难以处理,从而让可选脸带来的便利性被抵消。
-
考虑TextField 类通过调用存储在其中的回调函数来通知其所有者,当某个函数的调用需要分情况讨论时,可选链将会非常有帮助
class TextField{ private(set) var text = "" var didChange: ((String) -> ())? func textDidChange(new Text: String){ text = newText didChange?(text) // 如果不是nil,触发回调 } }
-
通过可选链来进行赋值
struct Person{ var name: String var age: Int } var optionalLisa: Person? = Person(name: "Lisa", age: 8) if optionalLisa != nil{ optionalList!.age += 1 // 为了给一个可选类的age 增值,用了两次强制解包 } // 这里无法使用可选绑定,因为Person 是一个结构体,所以是一个值类型 if var lisa = optionalList{ // lisa 是一个复制值,对原本的对象没有影响 lisa.age += 1 }
-
为可选值赋值的小坑:
a? = 10
表示仅在a 不为nil 时为a 赋值10var a: Int? = 5 a? = 10 a // Optional(10) var b: Int? = nil b? = 10 b // nil
-
-
nil 合并运算符
-
预设值 ??
-
当希望通过下标索引数组的第5个值,但不知道数组是否有5个元素时
// 通过3元运算符?(类似Java) array.count > 5 ? array[5] : 0 // 通过对数组索引符进行扩展 extension Array{ subscript(guraded idx: Int) -> Element? { guard(startIndex ..< endIndex).contains(idx) else{ return nil } return self[idx] } } array[guarded: 5] ?? 0 // 如果长度够5,返回对应索引的值;如果不够,索引返回nil,表达式返回0
-
选择几个连续变量中第一个非nil 值
let i: Int? = nil let j: Int? = nil let k: Int? = 42 i??j??k??0 // 42
-
Trick:在字符串插值中使用可选值
let tem: Double? = 37.0 print(tem) // Warning // 37.0 infix operator ???: NilCoalescingPrecedence public func ???<T>(optional: T?, defaultValue: @autoclosure() -> String) -> String{ switch optional{ case let value?: return String(describing: value) case nil: return defaultValue() } } print(\(tem ??? "n/a")) // No warning // 37.0
??? 这个函数接受左侧可选值T? 和右侧字符串。如果可选值不是nil,我们将它解包并返回字符串描述。
否则,我们将传入的默认字符串返回。
@autoclosure 标注确保只有当需要的时候,我们才会对第二个表达式进行求值
-
-
可选值map
-
可选值flatMap
-
使用compactMap 过滤nil
// 在一串字符中,处理所有能够被转化为数字的值 let nums = ["1", "2", "3", "foo"] var sum = 0 // 1: case-let for case let num? in nums.map({Int($0)}){ sum += num }// 6 // 2: reduce + ?? nums.map{Int($0)}.reduce(0){$0 + ($1 ?? 0)} // 6 // 3: compactMap nums.compactMap{Int($0)}.reduce(0, +) // 6
-
可选值判等
- 两个nil 判等;nil 和任何非nil 不等,两个非nil 比较解包后的值
- 即使都为nil,两个类型不相等的值也无法比较,别忘了Optional 本质上是Enum,自然有类型之分
- 其中有一个隐式的转换,当一个可选值和一个非可选值进行比较时,非可选值会升级为一个可选值
-
可选值比较
- 出于安全考虑,Swift3.0 可选值的<、>、<=、>= 都被移除了
- 在函数中通过范型函数来将普通的比较函数“提升为兼容可选值的比较函数”
-
-
强制解包的时机
当你能确定你的某个值不可能是nil 时可以使用叹号,你应当会希望如果它意外是nil 的话,程序应当直接挂掉
let ages = ["Tim": 53,"Angela":54,"Craig":44, "Jony": 47, "Chris": 37, "Michael": 34] // 可以强制解包 // 因为所有的key 都来自ages.keys,不可能会有nil 值 ages.keys.filter{name in ages[name]! < 50}.sorted() // ["Chris", "Craig", "Jony", "Michael"] // trick to avoid '!' ages.filter{(_, age) in age < 50} .map{(name, _) in name} .sorted() // ["Chris", "Craig", "Jony", "Michael"]
-
About Timing: 如引用,当在某处我十分确信某个值不可能为nil 时,这也意味着如果这里出现非nil 值将暗示着我的代码逻辑中存在着十分严重的bug。此时,强制解包同时实现了“解包” 和“报错” 两种功能。
-
改进强制解包的错误信息机制
// custom operator: !! // 为可选值的强制解包包含一个错误信息,当程序因为该强制解包意外退出时,这个信息也将被打印 infix operator !! func !!<T>(wrapped: T?, failureText: @autoclosure() -> String) -> T{ if let x = wrapped{return x} fatalError(failureText()) } let s = "foo" let i = Int(s) !! "Excepting integer, got\"\(s)\""
-
通过断言来规避冒险行径的的风险
- 诚然,在发布版本中主动让应用崩溃还是很大胆的行为,即使只是往这个推了一把
- 因此,可以客制一个!? 符号,以此在:1)调试版本中触发断言,使程序中断;2)发布版本中不触发断言,讲该值替换为默认值
// custom operator: !? infix operator !? func !?<T: ExpressibleByIntegerLiteral>(wrapped: T?, failureText: @autoclosure() -> String) -> T{ assert(wrapped != nil, failureText()) return wrapped ?? 0 // 默认值为0 } let s = "20" let i = Int(s) !? "Expecting integer, got \"\(s)\"" // Array 适用版 func !?<T: ExpressibleByArrayLiteral>(wrapped: T?, failureText: @autoclosure () -> String) -> T{ assert(wrapped != nil, failureText()) return wrapped ?? [] } // String 适用版 func !?<T: ExpressibleByStringLiteral>(wrapped: T?, failureText: @autoclosure () -> String) -> T{ assert(wrapped != nil, failureText()) return wrapped ?? "" }
- 当需要显式的提供默认值,使用以下版本
// 使用Tuple 来完成默认值的传入 func !?<T>(wrapped: T?, nilDefault: @autoclosure() -> (value: T, text: String)) -> T{ assert(wrapped != nil, nilDefault().text return wrapped ?? nilDefault().value } Int(S) !? (5, "Expected integer")
- 需要类似的操作检测一个可选链调用碰到nil,并且无操作的情况
func !?(wrapped:()?, failureText: @autoclosure() -> String){ assert(wrapped != nil, failureText()) } var output: String? = nil output?.write("something") !? "Wasn't expecting chained nil here"
-
-
隐式解包可选值 —— 使用的原因
- 暂时的开发环境中,我们仍有可能需要到Objective-C 里调用那些没有检查返回是否存在的代码;或者可能会调用一个没有针对C 做注解的C 语言库。为了能够更容易地使用OC 和C,隐式解包仍有存在的意义。【早期,所有返回引用的OC 方法都被转换为了一个隐式的可选值。然而,事实上很少有OC API 会真的返回一个空引用,所以将其设定为“可能为空” 的隐式解包可选值状态是说得过去的。】因此,在一个OC 桥接代码中看到隐式解包可以理解,但不应该在一个纯原生Swift API 的返回中使用一个隐式可选值,也不应将其传入回调。
- 有的值可能只是很短暂地为nil,在一段时间后,它将再也不会是nil。常见于「两阶段初始化」。
- 在Xcode 和Interface Builder环境下,view controller 的生命周期中,当类被准备好使用时,所有的隐式解包可选值都将有一个值。
- 在Cocoa 和Cocoa Touch中,view controller会延时创建他们的view,所有在view controller 自身已经被初始化,但他的view 还没有被加载的这段时间窗口内,view 的对象的outlet 引用还没有被创建
-
隐式可选值虽然在行为上相似与非可选值,但其仍具有可选值的操作行为,我们依然可以对其使用可选链,nil 合并,if-let,map 或将其与nil 比较等
Summary
可选值是 Swift 的一大卖点,它是让开发者得以书写更安全的代码的最大特性之一,而且我们 完全同意这个说法。但仔细想想,其实真正带来变化的不是可选值,而是非可选值。几乎所有 的主流语言都有类似 “null” 或者 “nil” 的概念;它们中的大多数所缺乏的是把一个值声明为 “从 不为 nil” 的能力。或者,反过来想,有一些类型 (比如 Objective-C 或 Java 中的非 class 类型) “总是不为 nil”,这又让开发者们必须用某个魔法数来表示缺少一个值的情况。
设计 API 的过程中,根据实际需要让输入和输出包含精心设计过的可选值,不仅会让调用函数 的代码更具表现力,这些函数用起来也会更简单。因为通过签名可以传递更多的信息,开发者 也就不用总是诉诸文档了。
—— 《Advanced Swift》