可选值(Optionals)是 Swift 引入的一项非常棒的特性,本文将基于原书中的案例以及函数式编程思想进行讨论。
概述
首先推荐大家阅读原书中的可选值章节,有很多使用细节以及与 Objective-C 的对比讨论,我认为如果将这一章单独拿出来会是一篇讲解 Swift 可选值很不错的文章。
关于 Swift Optionals,我曾写过一篇,试图借助 Swift 源码来解析可选值,其中开篇提到:
Swift 引入的 Optionals,很好的解决了 Objective-C 时代 “nil or not nil” 的问题,配合 Type-Safe 特性,帮我们减少了很多隐藏的问题。
这里主要有两方面意思:
- “nil or not nil”:在 Objective-C 中,
nil
是一个极其常见的值,指向一个为空的对象,很重要的是向nil
发消息是安全的,回忆一下我们曾经写过的代码,有意无意的都使用了该特性,换句话说,有很多情况我们是不对为空做区别处理的,这样一来,我们的代码通常会简洁一些,可是一旦由于nil
的存在或是传递导致了 Crash,就不得不去追查任何可能为nil
的情况。 - “Type-Safe”:Swift 是一个类型安全的语言,我们需要清楚的了解被操作的值的类型,编译器会对代码时进行类型检查,把不匹配的类型标记为错误,这样能够帮助我们尽早的发现程序中的问题。而对于类似 Objective-C 中的
nil
来说,一定程度与类型安全产生了冲突,因而,引入 Optionals 来与 Type-Safe 配合很好的解决这一问题。
下面将结合原书案例进一步讨论。
注:本文将不涉及对可选值基础应用的讲解,如需学习,请查阅、或。
案例:Dictionary
使用两个 Dictionary
分别存储国家及其首都、城市名及其人口数量:
let capitals = [ "France": "Paris", "Spain": "Madrid", "The Netherlands": "Amsterdam", "Belgium": "Brussels"]let cities = ["Paris": 2241, "Madrid": 3165, "Amsterdam": 827, "Berlin": 3562] // 单位为“千”复制代码
问题:编写函数接收输入的国家名,输出其首都城市的人口数量。
与之前章节一样,问题本身并不复杂,为了对比,我们先编写一个 Objective-C 版本的函数:
@property (nonatomic, copy) NSDictionary *capitals;@property (nonatomic, copy) NSDictionary *cities;_capitals = [[NSDictionary alloc] initWithObjectsAndKeys: @"Paris", @"France", @"Madrid", @"Spain", @"Amsterdam", @"The Netherlands", @"Brussels", @"Belgium", nil];_cities = [[NSDictionary alloc] initWithObjectsAndKeys: @2241, @"Paris", @3165, @"Madrid", @827, @"Amsterdam", @3562, @"Berlin", nil];- (NSInteger)populationOfCapital:(NSString *)country { return [_cities[_capitals[country]] integerValue] * 1000;}复制代码
populationOfCapital
函数就是我们的解决方案,只需要一行代码即可,下面测试一下:
// Case 1NSLog(@"%ld", [self populationOfCapital:@"France"]); // 2241000// Case 2NSLog(@"%ld", [self populationOfCapital:@"China"]); // 0复制代码
对于 Case 1 一切正常,而对于 Case 2,由于 _capitals
中不包含 China
,所以 _capitals[country]
为 nil
,按照 Objective-C 的 nil
处理办法,我们得到了 0
,但是,我们无法区分这个是正常的结果还是异常的输出,如果我们修改一下 _capitals
和 _cities
:
_capitals = [[NSDictionary alloc] initWithObjectsAndKeys: @"Paris", @"France", @"Madrid", @"Spain", @"Amsterdam", @"The Netherlands", @"Brussels", @"Belgium", @"Tokyo", @"Japan", // Add Japan nil];_cities = [[NSDictionary alloc] initWithObjectsAndKeys: @2241, @"Paris", @3165, @"Madrid", @827, @"Amsterdam", @3562, @"Berlin", @0, @"Tokyo", // Add Tokyo nil];// Case 3NSLog(@"%ld", [self populationOfCapital:@"Japan"]); // 0复制代码
我们忽略 Tokyo
是不是真的“零人口”,Case 3 输出 0
是正确的,但我们无法区分 Case 2 和 Case 3 的输出。这也就是 “nil or not nil” 问题。
那么 Swift 如何使用 Optional 解决这个问题呢?我们知道 populationOfCapital
函数应该能够区分 nil
和 not nil
的情况,而这正好匹配了可选值的定义:
// Optional.swift(swift/stdlib/public/core/Optional.swift)public enum Optional: ExpressibleByNilLiteral { case none // nil case some(Wrapped) // not nil}复制代码
因此,populationOfCapital
的返回值类型应为 Int?
,capitals
或 cities
查询失败时对应 nil
的情况,查询成功则对应 not nil
,所以 Swift 中 populationOfCapital
函数定义如下:
func populationOfCapital(country: String) -> Int? { if let capital = capitals[country] { if let population = cities[capital] { return population * 1000 } else { return nil } } else { return nil }}let cities = [ "Paris": 2241, "Madrid": 3165, "Amsterdam": 827, "Berlin": 3562, "Tokyo": 0]let capitals = [ "France": "Paris", "Spain": "Madrid", "The Netherlands": "Amsterdam", "Belgium": "Brussels", "Japan": "Tokyo"]// Case 4populationOfCapital(country: "France") // 2241000populationOfCapital(country: "China") // nilpopulationOfCapital(country: "Japan") // 0复制代码
可以看到,借助可选值,Case 4 的输出满足了我们的需求,China 和 Japan 的情况也得到了区分。问题已经解决了,不过 populationOfCapital
完成的不够“美观”,也并没有利用到函数式编程思想,下面对它进行优化。
首先,我们先来看看 populationOfCapital
到底要完成什么工作,本质上就是从一个值转变为另一个值,即从国家名转变为人口数量,只是在转变过程中可能存在“失败”的情况,需要对这些“失败”的情况作出判断,通过,我们知道值的转变对应的就是 map
方法,所以我们需要开发的工具库就是支持可选值的 map
方法,称之为:flatMap
。
要为 Optional 添加 flatMap
函数,并且存在“失败”的情况,所以 flatMap
函数的输出类型应为 U?
,而输入,则应该是该可选值不为 nil
时所要做的转换(转换本身也是一个函数):
extension Optional { func flatMap(f: Wrapped -> U?) -> U? { guard let x = self else { return nil } return f(x) }}复制代码
工具库就绪后,我们可以改写 populationOfCapital
函数:
func populationOfCapital_map(country: String) -> Int? { return capitals[country].flatMap { capital in return cities[capital] }.flatMap { population in return population * 1000 }}// Case 5populationOfCapital_map(country: "France") // 2241000populationOfCapital_map(country: "China") // nilpopulationOfCapital_map(country: "Japan") // 0复制代码
借助工具库,我们不再需要进行多次判断,而是直接关注“成功”的情况即可。事实上,在 Optional 类库中已经存在一个同样功能的 flatMap
函数,开发中直接调用即可,其源码如下:
// Optional.swift(swift/stdlib/public/core/Optional.swift)public func flatMap(_ transform: (Wrapped) throws -> U?) rethrows -> U? { switch self { case .some(let y): return try transform(y) case .none: return .none }}复制代码
为什么使用可选值?
原书中详细讨论了为什么 Swift 使用可选值,大致总结以下两点:
- 安全特性:不同于 Objective-C 默认近似零的处理方式,Swift 通过显式的可选类型以及类型安全特性,帮助避免由于缺失值而导致的意外崩溃或是逻辑含糊;
- 明确的函数签名:Objective-C 中函数的参数并无是否为空的强制要求,我们在使用时往往也比较模糊,仿佛空和非空均支持(这种情况目前有所改观,借助
nullable
、NS_ASSUME_NONNULL_BEGIN
和NS_ASSUME_NONNULL_END
能够帮助我们设计空和非空的情况),而由于可选值的存在,使得 Swift 中的函数签名是否支持nil
变得非常清晰,更有利于我们写出健壮的代码。
参考资料
本文属于《函数式 Swift》读书笔记系列,同步更新于 ,欢迎关注!