Swift 可选值 ——《Swift 进阶》阅读笔记

| Swift , 阅读笔记

 

请注意,本文大部分内容摘自《Swift 进阶》

 

内容概览

  • 1.哨岗值
  • 2.通过枚举解决魔法数的问题
  • 3.可选值概览
  • 4.强制解包的时机
  • 5.隐式解包可选值

 
 

1. 哨岗值

 

常见的“哨岗值”:

哨岗值

这些函数都返回了一个 魔法数 来表示其并没有返回真实的值。这样的值被称为 “哨岗值 (sentinel values)”。

 

2. 通过枚举解决魔法数的问题

 

可选值的实现基于以下这个带有泛型关联值 WrappedOptional 枚举。

enum Optional<Wrapped> {
    case none
    case some(Wrapped)
}

获取可选值中的关联值需要进行解包:

var array = ["one","two","three"]
switch array.firstIndex(of: "four") {
case .some(let idx):
    array.remove(at: idx)
case .none:
    break // 什么都不做
}

或者这样写:

var array = ["one","two","three"]
switch array.firstIndex(of: "four") {
case let idx?:
    array.remove(at: idx)
case nil:
    break // 什么都不做
}

 

3. 可选值概览

 

if let

检查可选值是否为 nil,如果不是 nil,便会解包可选值。

var array = ["one", "two", "three", "four"]
// 可以把布尔限定语句与 if let 搭配在一起使用
if let idx = array.firstIndex(of: "four"), idx != array.startIndex {
    // idx 只在这个 if let 语句的作用域中有效
    array.remove(at: idx)
}

可以在同一个 if 语句中绑定多个值,并且在后面的绑定中可以使用之前成功解包出来的结果。

if let url = URL(string: urlString), 
    url.pathExtension == "png", // 多个 let 的任意部分也能拥有布尔值限定的语句
    let data = try? Data(contentsOf: url), // 通过 try? 来转变为一个可选值
    let image = UIImage(data: data) {
    let view = UIImageView(image: image)
}

 

while let

当一个条件返回 nil 时便终止循环。

// 可以在可选绑定后面添加一个布尔值语句
while let line = readLine(), !line.isEmpty {
    print(line)
}

还可以使用迭代器:

let array = [1, 2, 3]
// 创建迭代器
var iterator = array.makeIterator() 

// 迭代器中的 next 方法将不断返回序列中的值,
// 并在序列中的值被耗尽的时候,返回 nil
while let i = iterator.next() {
    print(i, terminator: " ")
}

for 循环也支持布尔语句,只是要在布尔语句之前,使用 where 关键字:

for i in 0..<10 where i % 2 == 0 {
    print(i, terminator: " ")
}

将上面的 for 循环用 while 重写:

var iterator2 = (0..<10).makeIterator()
while let i = iterator2.next() {
guard i % 2 == 0 else { continue }
    print(i)
}

 

双重可选值

一个可选值的包装类型也是一个可选值的情况。

来看一个例子:

let stringNumbers = ["1", "2", "three"]
let maybeInts = stringNumbers.map { Int($0) }

for maybeInt in maybeInts {
    // maybeInt 是一个 Int? 值
    // 得到两个整数值和一个 `nil`
}

// for...in 是 while 循环加上一个迭代器的简写方式
var iterator = maybeInts.makeIterator()
while let maybeInt = iterator.next() {
    print(maybeInt, terminator: " ") // 得到两个整数值和一个 `nil`
}

由于 next 方法会把序列中的每个元素包装成可选值,所以 iterator.next() 函数返回的其实是一个 Optional<Optional<Int>> 值,或者说是一个 Int??

 

请注意,这本书里面的 高级玩法 来了:

只对非 nil 的值做 for 循环

for case let i? in maybeInts { // 或者 for case let .some(i) in maybeInts
    // i 将是 Int 值,而不是 Int?
    print(i, terminator: " ")
}

x? 这个模式,它只会匹配那些非 nil 的值,是 .some(x) 的简写形式。

只对 nil 的值做 for 循环

for case nil in maybeInts {
    // 将对每个 nil 执行一次 
    print("No value")
}

范围匹配

let j = 5
if case 0..<10 = j {
    print("\(j) 在范围内")
} // 5 在范围内

 

if var and while var

let number = "1"
if var i = Int(number) {
    i += 1
    print(i)
}

请注意,任何对 i 的改变将不会影响到原来的可选值。可选值是值类型,解包一个可选值做的事情是将它里面的值复制出来。

 

解包后可选值的作用域

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[extensionStart...])
    }
}

虽然可以成功解包并获得关联值,但是上面这种写法过于丑陋。

extension String {
    var fileExtension: String? {
        guard let period = lastIndex(of: ".") else {
            return nil
        }
        let extensionStart = index(after: period)
        return String(self[extensionStart...])
    }
}

guard - else

  • guard 能够接受任何在普通的 if 语句中能接受的条件。
  • 条件不成立时,提前退出。

除了书上说的这两点,我个人觉得 guard-else 还有以下优点:

  • 可以有效地减少因大量使用 if - else 而导致的代码块缩进,使代码更优雅、易读;
  • 可以有效地提升防御式编程思维,督促开发者多思考 条件不成立 的情况;

Swift 中的“无”类型

  • “东西不存在”(nil)
  • “存在且为空”(Void), public typealias Void = ()
    • 常用做那些不返回任何东西的函数的返回值。
  • “不可能发生” (Never), public enum Never { }
    • 一个返回 Never 的函数用于通知编译器:它绝对不会返回。一种是像 fatalError 那样表示程序失败的函数,另一种是像 dispatchMain 那样 运行在整个程序生命周期的函数。

 

可选链

delegate?.callback(),加上 ? 来表示你正在链接这个可选值。

这里有一个值得注意的示例,它可以帮助你加深对 ? 的理解。

var a: Int? = 5
a? = 10
a // Optional(10)

var b: Int? = nil
b? = 10
b // nil

前一种写法无条件地将一个新值赋给变量,而后一种写法只在 b 的值在赋值发生前不是 nil 的时候才生效。

我运行过这段代码,确实输出了和注释中的内容一样的值,不信你也可以试试看~

 

nil 合并运算符 ??

在解包可选值的同时,为 nil 的情况设置一个默认值。

let stringteger = "1"
let number = Int(stringteger) ?? 0

需要注意的是,合并操作也能够进行链接。

let i: Int? = nil
let j: Int? = nil
let k: Int? = 42
i ?? j ?? k ?? 0 // 按顺序合并,最终得到 42

除此之外,如果你要处理的是双重嵌套的可选值,并且想使用 ?? 操作符的话,需要特别 小心区分 a ?? b ?? c 和 (a ?? b) ?? c。前者是合并操作的链接,而后者是先解包括号内的内容, 然后再处理外层:

let s1: String?? = nil // nil
(s1 ?? "inner") ?? "outer" // inner
let s2: String?? = .some(nil) // Optional(nil)
(s2 ?? "inner") ?? "outer" // outer

最后,?? 操作符使用短路求值。因为在操作符的函数声明中,对第二个参数使用了 @autoclosure

 

在字符串插值中使用可选值

常见场景如下,通常还会有编译器的警告提示:

let bodyTemperature: Double? = 37.0
let bloodGlucose: Double? = nil
print(bodyTemperature) // Optional(37.0)

修正编译器警告的方式:

  • 显式地用 as Any 进行转换,使用 ! 对值进行强制解包 (如果你能确定该值不为 nil 时)
  • 使用 String(describing: …) 对它进行包装
  • 用 nil 合并运算符提供一个默认值

书中提供了一个比较优雅的解决方案:

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("Body temperature: \(bodyTemperature ??? "n/a")")
// Body temperature: 37.0

 

可选值 map

只在可选值不为 nil 的时候才进行转换。

var firstCharAsString: String? = nil
if let char = characters.first {
    firstCharAsString = String(char)
}

以上代码可以使用 map 进行精简:

let firstChar = characters.first.map { String($0) } // Optional("a")

map 的实现代码:

extension Optional {
    func map<U>(transform: (Wrapped) -> U) -> U? {
        guard let value = self else { return nil }
        return transform(value)
    }
}

 

可选值 flatMap

flatMap 可以把结果展平为单个可选值,避免多重嵌套的可选值(如:Int??)。

这是之前的示例:

let urlString = "https://www.objc.io/logo.png" 
if let url = URL(string: urlString),
    let data = try? Data(contentsOf: url),
    let image = UIImage(data: data) {
    let view = UIImageView(image: image)
}

flatMap 进行等价转换:

let urlString = "https://www.objc.io/logo.png" 
let view = URL(string: urlString)
            .flatMap { try? Data(contentsOf: $0) } 
            .flatMap { UIImage(data: $0) }
            .map { UIImageView(image: $0) }

flatMap 的实现代码:

extension Optional {
    func flatMap<U>(transform: (Wrapped) -> U?) -> U? {
        if let value = self, let transformed = transform(value) {
            return transformed
        }
        return nil
    } 
}

 

使用 compactMap 过滤 nil

来看3个示例,它们都是为了求和:

使用 for

let numbers = ["1", "2", "3", "foo"] 
var sum = 0
for case let i? in numbers.map({ Int($0) }) {
    sum+=i 
}
sum // 6

使用 map

numbers.map { Int($0) }.reduce(0) { $0 + ($1 ?? 0) } // 6

使用 compactMap

numbers.compactMap { Int($0) }.reduce(0, +) // 6

结果一目了然,使用 compactMap 之后,代码简短优雅而且不易出错。

自己实现 compactMap:

extension Sequence {
    func compactMap<B>(_ transform: (Element) -> B?) -> [B] {
        // 使用 lazy 可以避免多个作为中间结果的数组的内存分配
        return lazy.map(transform).filter { $0 != nil }.map { $0! }
    }
}

 

可选值判等

当比较两个可选值时,会有四种组合的可能性:

  • 两者都是 nil
  • 两者都有值;
  • 两者中有一个有值, 另一个是 nil

对应的代码如下:

extension Optional: Equatable where Wrapped: Equatable { 
    static func ==(lhs: Wrapped?, rhs: Wrapped?) -> Bool {
        switch (lhs, rhs) {
        case (nil, nil): return true
        case let (x?, y?): return x == y 
        case (_?, nil), (nil, _?): return false 
        }
    } 
}

当你在使用一个非可选值的时候,如果需要匹配成可选值类型,Swift 总是会将它 “升级” 为一个可选值,编译器会帮助我们将值在需要时转变为可选值。

如果没有隐式转换,你就必须写像是 myDict[“someKey”] = Optional(someValue) 这样的代码。

提到字典,使用下标操作为字典的某个键赋值 nil 值得探讨一下:

首先,直接赋值 nil,这个键会从字典中移除。

可以采用以下方法:

dictWithNils["two"] = Optional(nil) 
dictWithNils["two"] = .some(nil)

以及:

dictWithNils["two"]? = nil

注意,”two” 这个键必须已经存在于字典中,然后才能使用可选链的方式来在获取成功后对值进行设置。

 

可选值比较

书中的建议:

先解包,然后明确地指出 nil 要如何处理,避免意外的结果发生。

 

4. 强制解包的时机

 

当你能确定你的某个值不可能是 nil 时可以使用叹号,你应当会希望如果它意外是 nil 的话,程序应当直接挂掉。

每当你发现需要使用 ! 时,可以回头看看是不是真的别无他法了。

 

改进强制解包的错误信息

当程序因为强制解包而发生错误时,你从输出的 log 中无法通过描述知道原因是什么。

书中给出的比较优雅的解决方案:

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) !! "Expecting integer, got \"\(s)\""

 

在调试版本中进行断言

选择在发布版中让应用崩溃还是很大胆的行为。

在调试版本或者测试版本中进行断言,让程序崩溃。
在最终产品中,你可能会把它替换成像是零或者空数组这样的默认值。

于是,可以这样定义这个运算符:

infix operator !?
func !? <T: ExpressibleByIntegerLiteral>(wrapped: T?, failureText: @autoclosure () -> String) -> T {
    assert(wrapped != nil, failureText())
    return wrapped ?? 0
}

let s = "20"
let i = Int(s) !? "Expecting integer, got \"\(s)\""

然后,对其他字面量转换协议进行重载,可以覆盖不少能够有默认值的类型: ExpressibleByArrayLiteral, ExpressibleByStringLiteral

如果想要显式地提供一个不同的默认值,或者是为非标准的类型提供这个操作符,定义一个接受元组为参数的版本,元组包含默认值和错误信息:

func !?<T>(wrapped: T?, nilDefault: @autoclosure () -> (value: T, text: String)) -> T {
    assert(wrapped != nil, nilDefault().text) 
    return wrapped ?? nilDefault().value
}
// 调试版本中断言,发布版本中返回 5 Int(s) !? (5, "Expected integer")

挂起一个操作的三种方式

  • fatalError,接受一条信息,并且 无条件地停止 操作。
  • 使用 assert 来检查条件,当条件结果为 false 时,停止执行并输出信息。在发布版本中,assert会被移除掉,条件不会被检测,操作也永远不会挂起。
  • 使用 precondition,它和 assert 有一样的接口,但是在发布版本中不会被移除。只要条件被判定为 false,执行就会被停止。

 

5. 隐式解包可选值

 

为什么会要用到隐式解包可选值呢?

原因 1:暂时来说,你可能还需要到 Objective-C 里去调用那些没有检查返回是否存在的代码; 或者你会调用一个没有针对 Swift 做注解的 C 语言的库。

  • Objective-C 中表示引用是否 可以为空的语法是最近才被引入的,以前除了假设返回的引用可能是 nil 引用以外,也没有什么好办法。
  • 所有人都已经习惯了 Objective-C 世界中对象 “可能为空” 的设定,因此把这样的返回值作为隐式解包可选值来使用是可以说得过去的。

原因 2:因为一个值只是很短暂地为 nil,在一段时间后,它就再也不会是 nil。

  • 最常见的情况就是两阶段初始化 (two-phase initialization)。当你的类准备好被使用时,所有的隐式解包可选值都将有一个值。这就是 Xcode 和 Interface Builder 在 view controller 的生命周期中使用它们的方式。

 

隐式可选值行为

你依然可以对它们使用可选链, nil 合并,if let,map 或者将它们与 nil 比较,所有的这些操作都是一样的

var s: String! = "Hello" 
s?.isEmpty // Optional(false) 
if let s = s { print(s) } // Hello 
s = nil
s ?? "Goodbye" // Goodbye

 
 

阅读本书使我获益良多,衷心地感谢优秀的内容生产者们!

如需深入学习相关内容,推荐大家购买原版书籍 《Swift 进阶》
请大家支持原创,用行动去鼓励高质量的内容生产者!

 

觉得不错?点个赞呗~

本文链接:Swift 可选值 ——《Swift 进阶》阅读笔记

转载声明:本站文章如无特别说明,皆为原创。转载请注明:Ficow Shen's Blog

评论区(期待你的留言)