11f9f9949f5ec272d8d25096c29ab4b7
Swift Probe - Optional

Swift Probe - Optional

最近在研究 Swift 中好玩的东西,打算将一些学习笔记,整理成一个系列便于自己温习且与大家交流。这次来玩弄一下 Optional。

Optional 引入由来

Optional 特性是 Swift 中的一大特色,用来解决变量是否存有 nil 值的情况。这样既可减少在数据传递过程中,由于 nil 带来的不确定性,防止未处理 nil 而带来的程序崩溃。
Optional 在高级语言中其实并不是 Swift 的首创,而是效仿其他语言学习来的特性。2015 年的时候,为了迎合 Swift 的 Optional 特性,在 Objective-C 中也引入了 Nullability 特性。Swift 作为一个强类型语言,需要在编译期进行安全检查,所以引入了类型推断的特性。为了保证推断的安全,于是又引入了 Optional 特性。
如果没有 Optional 到底有如何的危险呢?我们用 C++ 的一个例子来看一下:

  • #include <iostream>
  • using namespace std;
  • int main() {
  • auto numbers = { 1, 2, 3 };
  • auto iterator_of_4 = std::find(numbers.begin(), numbers.end(), 4);
  • if (iterator_of_4 == numbers.end()) {
  • cout << "Not found 4" << endl; // 未查找到 4 的操作
  • }
  • else {
  • cout << "Got it" << endl; // 代码执行
  • }
  • return 0;
  • }

在使用迭代器的时候,我们往往要判断迭代器是否已经遍历到末尾,才可以去继续操作。因为有值不存在的情况,所以在以往的操作中都会使用一个特殊值来表示某种特殊的含义,通常情况下对于这种特殊值称作 Sentinal Value,在很多算法书中称其为哨兵值。使用哨兵值会有这么两个弊端:其一是形如 std::find 或者是 std::binary_search 这种方法都从它们各自的签名以及调用上,都无法得知它的错误情况,以及对应的错误情况处理方式。另外,以哨兵值的方式,使我们无法通过编译器来强制错误处理的行为。因为编译器对此是毫无感知的,其哨兵值都是由语言作者或是后期开发人员的约定俗成,例如 C 中文件读取的 open 函数,在读取失败下为 -1,或是上例中 numbers.end() 这个迭代位,只有在程序崩溃之后,才能显出原形。
为了突出 Optional 的必要性,泊学网(笔者也是最近才看过的,这里推荐一下😎)中给出了一个哨兵值方案也无法解决的问题,这是一个 Objective-C 的例子:

  • NSString *tmp = nil;
  • if ([tmp rangeOfString: @"Swift"].location != NSNotFound) {
  • // Will print out for nil string
  • NSLog(@"Something about swift");
  • }

虽然 tmp 的值为 nil,但调用 tmprangeOfString 方法却是合法的,它会返回一个值为 0 的 NSRange ,所以 location 的值也是 0。但是 NSNotFound 的值却是 NSIntegerMax。所以尽管 tmp 的值为 nil, 我们还能够在 Terminal 中看到 Something about swift 的输出。所以,当为 nil 的时候,我们仍旧需要特殊考虑。
于是,这就是 Optional 的由来,为了解决使用 Sentinal Value 约定而无法解决的问题。

使用 Optional 实现方法

这里是 Swift Probe 系列,所以我们不说其用法。在 Swift 的源码中,Optional 以枚举类型来定义的:

  • @_fixed_layout
  • public enum Optional<Wrapped> : ExpressibleByNilLiteral {
  • case none
  • case some(Wrapped)
  • public init(_ some: Wrapped)
  • public func map(_ transform: (Wrapped) throws -> U) rethrows -> U?
  • public func flatMap(_ transform: (Wrapped) throws -> U?) rethrows -> U?
  • public init(nilLiteral: ())
  • public var unsafelyUnwrapped: Wrapped { get }
  • }

当然在枚举中还有很多方法并没有列出,之后我们详细来谈。在枚举定义之前,有一个属性标识(attribute) - @_fixed_layout,由此标识修饰的类型在 SIL (Swift intermediate
Language)生成阶段进行处理。它的主要作用是将这个类型确定为固定布局,也就是在内存中这个类型的空间占用确定且无法改变。
由于 Optional 是多类型的,所以我们通过 <Wrapped> 来声明泛型。ExpressibleByNilLiteral 协议仅仅定义了一个方法:

  • init(nilLiteral: ()) // 使用 nil 初始化一个实例

不看方法,仅仅看这个枚举定义,其实我们就可以模拟一些很简单的方法。例如我们来解决上文中 C++ std::find 那个问题,对 Array 数据结构来写一个 extension

  • import Foundation
  • enum Optional<Wrapped> {
  • case none
  • case some(Wrapped)
  • }
  • extension Array where Element: Equatable {
  • func find(_ element: Element) -> Optional<Index> {
  • var index = startIndex
  • while index != endIndex {
  • if self[index] == element {
  • return .some(index)
  • }
  • formIndex(after: &index)
  • }
  • return .none
  • }
  • }

代码很简单,就是将当前数组做一次遍历来查找这个元素,如果找到则返回一个 some 类别代表这个 Optional 结果是存在的。如果没有则返回 none。我们来测试一下:

发现如果 find 方法在 Array 中无法找到对应元素,则会返回一个 none 的 Optional 对象。
由于在 Swift 的源码中已经定义了 Optional,并且使用特定的重载标记符号进行简化,所以我们也可以简写上述的 find

  • extension Array where Element: Equatable {
  • func find(_ element: Element) -> Index? {
  • var index = startIndex
  • while index != endIndex {
  • if self[index] == element {
  • return index
  • }
  • formIndex(after: &index)
  • }
  • return nil
  • }
  • }

由于 Swift 通过 ? 来对 Optional 类型做了简化,所以我们将返回值修改成 Index? 即可。其他地方也类似,如果有值直接返回,没有则返回 nil。我们使用 if let 使用范式来验证一下 Optioinal 的作用:

Optional 中 map 和 flatMap 实现

top Created with Sketch.