请注意,本文大部分内容摘自《Swift 进阶》。
常见的“哨岗值”:
这些函数都返回了一个 魔法数
来表示其并没有返回真实的值。这样的值被称为 “哨岗值 (sentinel values)”。
可选值的实现基于以下这个带有泛型关联值 Wrapped
的 Optional
枚举。
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 // 什么都不做
}
检查可选值是否为 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)
}
当一个条件返回 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 在范围内
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-else
还有以下优点:
if - else
而导致的代码块缩进,使代码更优雅、易读;条件不成立
的情况;Swift 中的“无”类型
nil
)Void
), public typealias Void = ()
。Never
), public enum Never { }
。
delegate?.callback(),加上 ?
来表示你正在链接这个可选值。
这里有一个值得注意的示例,它可以帮助你加深对 ?
的理解。
var a: Int? = 5
a? = 10
a // Optional(10)
var b: Int? = nil
b? = 10
b // nil
前一种写法无条件地将一个新值赋给变量,而后一种写法只在 b 的值在赋值发生前不是 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)
修正编译器警告的方式:
书中提供了一个比较优雅的解决方案:
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
只在可选值不为 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
可以把结果展平为单个可选值,避免多重嵌套的可选值(如: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
}
}
来看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 要如何处理,避免意外的结果发生。
当你能确定你的某个值不可能是 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")
挂起一个操作的三种方式
无条件地停止
操作。在发布版本中,assert会被移除掉
,条件不会被检测,操作也永远不会挂起。在发布版本中不会被移除
。只要条件被判定为 false,执行就会被停止。
为什么会要用到隐式解包可选值呢?
原因 1:暂时来说,你可能还需要到 Objective-C 里去调用那些没有检查返回是否存在的代码; 或者你会调用一个没有针对 Swift 做注解的 C 语言的库。
原因 2:因为一个值只是很短暂地为 nil,在一段时间后,它就再也不会是 nil。
你依然可以对它们使用可选链, 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