模糊的 Any 和 Optional

 

引出问题

Any 这个东西曾经在喵神的 《Swifter Tips》中被称作 Swift 中的妥协的产物,是一个十分迷惑的概念。这个概念被提出是为了弥补 AnyObject 在 Swift 中的不足,即 structenum 等非 class 的类型。实时证明,Any 的大量使用确实会让你在开发中十分的痛苦,这个痛苦会转化成一篇博客。是的,这篇博客就是这样。🌚

我们可以先在 Playground 中尝试以下代码观察现象:

carbon

在图中我高亮了一行,也是表现最为奇怪的一行。print(b == nil) 输出了 false 。但是根据我们的认知,b == nil 应该返回 true 。因为当我们直接输出 ab 的时候,其结果都是 nil

我们注意到这里的 ba 唯一的区别就是,在赋值时使用了 as Anyb 声明成 Any 类型。这个声明会带来什么影响呢?

追溯 == 源码

由于我们的 a 变量是 Optional<int> 类型,所以我们要追溯到 Optional 的源码位置。在 Swift 的官方 GitHub 代码仓库 中找到了定义 == 符号运算的实现

@_transparent
public static func ==(lhs: Wrapped?, rhs: _OptionalNilComparisonType) -> Bool {
  switch lhs {
  case .some:
    return false
  case .none:
    return true
  }
}

这里我们要学习两个概念:Wrapped_OptionalNilComparisonType

_OptionalNilComparisonTypeExpressibleByNilLiteral

首先再追溯一下 _OptionalNilComparisonType 的代码:

@frozen
public struct _OptionalNilComparisonType: ExpressibleByNilLiteral {
  /// Create an instance initialized with `nil`.
  @_transparent
  public init(nilLiteral: ()) {}
}

发现这个东西是继承于 ExpressibleByNilLiteral 这个协议的,这到底是一个什么东西?我们知道协议往往都是希望让某一类 class 或者 struct 具有某种功能(实现某组方法)。ExpressibleByNilLiteral 协议的效果就是遵循其的类型可以使用 nil 字面量来初始化。在我们的代码,甚至是 Swift 官方源码中,其实也很少用到这个协议,因为有 Optional 的存在,往往就可以表示值可能会不存在。

这里延伸一些 Attribute 的知识,上述代码中的 @frozen 意思是对应的 struct 保证其实例不会更改,从而编译器在编译这个 struct 的时候会做消除冗余负载优化。而 @_transparent 是为了告诉编译器在需要的时候可以将声明的函数内联,即使在 -Onone 下也是如此。

另外我们需要知道的是,nil 不是一个类型,我们必须要用一个继承于 ExpressibleByNilLiteral 的结构体来描述这个参数。

下面我们来做一个实验:

public struct _MyOptionalNil: ExpressibleByNilLiteral {
    /// Create an instance initialized with `nil`.
    @_transparent
    public init(nilLiteral: ()) {}
}


func isRealNil(_ param: _MyOptionalNil) {
    print("This is real nil")
}

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        isRealNil(nil) // "This is real nil"
    }
}

我们发现 nil 是可以走 isRealNil 方法的。这样可以让 isRealNil 显示传入 nil 参数。那么如果我们增加一个 Optional 的同名方法,会怎样呢?

Untitled

此时的推断是具有二义性的,所以在调用侧是不合法的。

由此我们得知,在 Optional 源码中的定义内部 _OptionalNilComparisonType 是为了接收一个显式 nil 参数,从而对其进行特殊处理。

Optional 多层嵌套问题

这也是一个使用 Swife 老生常谈的问题。在多层的 Optional 的关系中往往会让你的代码编写造成各种各样的问题。

在很久以前唐巧老师的博客 Swift 烧脑体操(一) - Optional 的嵌套 中会有这个例子:

let a: Int? = nil
let b: Int?? = a
let c: Int??? = b
let d: Int??? = nil
if let _ = b {
    print("b is not none") // 会输出
}
if let _ = c {
    print("c is not none") // 会输出
}
if let _ = d {
    print("d is not none") // 不会输出
}

为什么会造成这种原因呢,其实上述那篇文章中已经给出了答案。当我们对 Optional 变量进行赋值的时候,会有以下的语句:

var a: Int?? = b

如果当 b 的类型是 Optional<Int> 的时候,此时 a 的类型 Optional<Optional<Int>>b 的类型不相符,那么应该无法编译过才对。其实此处的原因也藏在 Optional 源码当中:

public enum Optional<Wrapped>: ExpressibleByNilLiteral { 
    case none
    case some(Wrapped)

    @_transparent
    public init(_ some: Wrapped) { self = .some(some) } // 重点二
	...
    @_transparent
    public init(nilLiteral: ()) { // 重点一
        self = .none
    }
}

重点一:当传入值右边是一个 Wrapped 类型,即一个泛型的时候,构造函数会将这个传入参数编程 .some 中的 Value ,在最外层再包裹一层 Optional

重点二:我们要知道 Optional 是遵循 ExpressibleByNilLiteral 协议的,所以当赋值符 = 右边是 nil 的时候,则最上层 Optional 直接被赋值为 Optional.none

我们带着这样的理解来看上述的例子:

let a: Int? = nil   // a => Optional<Int>.none
let b: Int?? = a    // b => Optional<Optional<Int>.none>.some
let c: Int??? = b   // c => Optional<Optional<Optional<Int>.none>.some>.some
let d: Int??? = nil // d => Optional<Optional<Optional<Int>.some>.some>.none

而解包操作只对最外层的 Optional 进行拆解,是 .some 则拆解成功,是 .none 即无法拆解。如此道理就说得通了。

问题回归

var a: Int? = nil
var b = a as Any
print(a == nil) // true
print(b == nil) // false 要点

这里的要点部分我们要如何解释?其实上面 == 的源码已经给出了答案:

b == nil

// 此处 b 在推断的时候,被当作一个 Optional<Any>.some 的类型,所以自然当作了一个 Wrapped? 来处理
// 当最外层第一次解包后,发现是一个 Wrapped 是 Any 的 .some,自然的认为不是 .none 也就不是 nil

@_transparent
public static func ==(lhs: Wrapped?, rhs: _OptionalNilComparisonType) -> Bool {
  switch lhs {
  case .some:
    return false
  case .none:
    return true
  }
}

源码部分的判断认为,传入的 Wrapped? 类型,如果在解包后是 .some 则不是 nil 。但是我们考虑一下,这真的是对的吗?

我的想法是,至少在我们这些语言使用方(iOS 的开发者)的角度来看,这是错的!因为我们想比较的是一个 Optional 的值,是不是 nil ,而并不关注这个 Optional 最外层的枚举是不是 .some 。从这个角度上来看,这是不友好的一面。

归根结底,这都是由于 Nested Optionals 这种设计而导致的。在 Swift 的官方论坛上,也有很多对于关于此问题的讨论,例如 Challenge: Flattening nested optionals 其中就有一些很有趣的解决方案:

我们可以通过以下这种递归的方式来解决 Nested Optionals 的问题:

private protocol _OptionalProtocol {
    var _deepUnwrapped: Any? { get }
}

extension Optional: _OptionalProtocol {
    fileprivate var _deepUnwrapped: Any? {
        if let wrapped = self { return deepUnwrap(wrapped) }
        return nil
    }
}

func deepUnwrap(_ any: Any) -> Any? {
    if let optional = any as? _OptionalProtocol {
        return optional._deepUnwrapped
    }
    return any
}

deepUnwrap(1)                                                       // 1
deepUnwrap(Optional<Int>.none)                                      // nil
deepUnwrap(Optional<Int>.some(1))                                   // 1
deepUnwrap(Optional<Optional<Int>>.none)                            // nil
deepUnwrap(Optional<Optional<Int>>.some(Optional<Int>.none))        // nil
deepUnwrap(Optional<Optional<Int>>.some(Optional<Int>.some(1)))     // 1

Swift 论坛中关于提出解决方案的开发者还有很多,但是大家的思路几乎都是一致的,是希望将嵌套 Optional 这种形式通过编码的方式处理掉,从而避免一些不必要的问题。

回归业务逻辑

也许你会质疑,上述的问题在开发中根本不常用,怎么可能会出现 var b = a as Any 这种情况?

其实这个场景是我在接手的项目中发现的一个产生 Bug 的原因,我只是抽象了一下对应的场景最简表达。在实际当中遇到的问题大概如下:

let a: Int = 0
let b: String = ""
let c: Int? = nil
let arr: [Any] = [a, b, c]

arr.compactMap { $0 }            // [0, "", nil]
arr.compactMap{ deepUnwrap($0) } // [0, ""]

我有一个数组,由于数组当中的元素在初始化的时候会含有多个类型,其中很自然的可能会出现有 IntStringOptional 的问题。所以数组 x 自然而然变成了 [Any] 。当我在某些情况下,对这个数组进行过滤 nil 的时候,就自然过滤不掉了。也许你会说,代码为何会这么设计?虽然我也认为很多设计的不合理,但是 nil 的这个问题为我接手项目的排查也增加了很大的难度!😭

最后对于 == 的一点小思考

其实笔者也考虑过,如果对 Optional== 逻辑进行改写从而达到判断最深层的 value 是不是 nil 这种效果。受到了上述 Swift 论坛中的影响,其实是可以使用递归的方式来解决这个问题。

以下我写了一个 Demo 来说明:

extension Optional {
    // 这是对 Optional 的一个扩展
    public struct _MyOptionalNil: ExpressibleByNilLiteral {
        @_transparent
        public init(nilLiteral: ()) {}
    }

    typealias OptionAny = Optional<Any>

    /// 重写一个 === 的操作,来递归判断 Nested Optionals 最深层是否是 nil
    public static func ===(lhs: Wrapped?, rhs: _MyOptionalNil) -> Bool {
        switch lhs {
        case let .some(innerOptionalWrappedValue):
            // 将 some 中的 value 取出,来判断 Any 的情况
            if case let OptionAny.some(innerWrappedValue) = innerOptionalWrappedValue as Optional<Any> {
                // 解包后如果是 .none,则就是 nil
                if case OptionAny.none = innerWrappedValue {
                    return true
                }
                // 如果不是 Optional<Any>.noen 则我们需要递归继续处理更深一层
                return innerWrappedValue === rhs
            }
            return false
        case .none:
            return true
        }
    }
}

let a: Int? = nil
let b = a as Any
print(b === nil) // true

当然,上述实现可能会有很多 Corner Case 没有考虑到,只是笔者的一个 Demo,但是也可以解决 Optional<Any>.some 当 Value 为 nil 时的一些值判断不符合预期的问题。

这个问题也让我更加加深了对于 Swift Optional 的认识。如果文中仍旧有纰漏也欢迎斧正!Swift 作为一个官方推广的强大语言,演化的语言复杂度越来越高,所以对基础的认识也要愈发深入和了解,才有能力把握这个技术!

最后的最后,感谢 Swift 大牛 @四娘 为我解答了很多疑问!