作为 Swift 开发者,我们都热爱这门语言提供的优雅和简洁性,尤其是它的函数式编程能力。

Swift 原生提供了一些高阶函数,常见的比如 mapfilterreduce 等。这些优雅的高阶函数能够极大的简化我们的代码,但是一个不小心也会报一个让你困扰一整天的错误。

今天,我们就来看看其中一种情况。

思考一下这个场景:对一个整数数组求和。使用 reduce 函数可以写得非常简洁:

let numbers = [1, 2, 3, 4, 5]

// 冗长但清晰的写法
let sum1 = numbers.reduce(0) { (accumulator, nextElement) in
    accumulator + nextElement
}

// 简单写法
let sum1 = numbers.reduce(0) { $0 + $1 }

// 极简写法
let sum2 = numbers.reduce(0, +)

+ 号可以直接作为 reduce 方法的第二个参数,从而进一步简化代码。这很好,非常简洁。

ok,现在让我们尝试用同样的逻辑来处理一个布尔类型的数组,检查是否所有值都为 true

let conditions = [true, true, false]

let allTrue1 = conditions.reduce(true) { $0 && $1 } // ✅ 工作正常

let allTrue2 = conditions.reduce(true, &&) // ❌ 编译错误!

哦噢,编译错误了。那么为什么 + 可以直接作为参数传递,&& 却不行?其实这背后隐藏着一个关于 Swift 语言设计的深刻且重要的区别。

+ 运算符:“一等公民”

Swift 标准库中包含一个名叫 AdditiveArithmetic 的协议,这个协议中定义了包括 + 在内的多个函数。我们可以从 Github 中查看到它的源码,官方文档在这里

所以 + 可以作为参数传递也就不奇怪了 —— 它本身就是 Swift 中的 “一等公民”:函数,因此可以被直接传入任何接受对应函数类型的参数中。

&& 运算符:重要的“短路”特性

相比较 + 而言,&&|| 这类逻辑运算符有一个核心特性,即 短路求值 (Short-circuit Evaluation):

  • 对于 a && b,如果 a 的结果是 false,那么整个表达式的结果就已经确定是 false 了。因此,b 将永远不会被求值或执行。
  • 对于 a || b,如果 a 的结果是 true,整个表达式就已经确定是 trueb 也不会被执行。

这个特性至关重要,它不仅能提升性能,还能避免潜在的运行时错误,比如我们经常写的代码:

if user != nil && user!.hasPermission {
    // 如果 user 是 nil,第二个条件根本不会被检查,从而避免了强制解包导致的崩溃。
}

而一个普通的 Swift 函数,在它被调用之前,它的所有参数都必须被完整地求值。可以通过一个简单的实验来证明这一点:

func getFalse() -> Bool {
    print("getFalse() 被调用了")
    return false
}

func getTrue() -> Bool {
    print("getTrue() 被调用了")
    return true
}

// 如果 && 是一个普通函数,它会像这样工作:
func logicalAnd(_ a: Bool, _ b: Bool) -> Bool {
    return a && b
}

print("\n--- 测试普通函数 ---")
_ = logicalAnd(getFalse(), getTrue())

// 输出:
// --- 测试普通函数 ---
// getFalse() 被调用了
// getTrue() 被调用了  <-- 两个参数对应的函数都被执行了

从输出结果我们可以得到结论:“短路” 这个行为无法被普通函数模拟 ... 别忘了,我们还可以看它的源码

实际上在 Swift 标准库中,&&|| 同样是两个函数,他们的定义分别如下所示:

extension Bool {
 /// Performs a logical AND operation on two Boolean values.
 @_transparent
 @inline(__always)
 public static func && (lhs: Bool, rhs: @autoclosure () throws -> Bool) rethrows -> Bool {
     return lhs ? try rhs() : false
 }

 /// Performs a logical OR operation on two Boolean values.
 @_transparent
 @inline(__always)
 public static func || (lhs: Bool, rhs: @autoclosure () throws -> Bool) rethrows -> Bool {
     return lhs ? true : try rhs()
 }
}

标准库通过三目运算符以及 @autoclosure,巧妙地实现了短路求值的功能。

但是绕了一圈,既然 && 也是函数,为什么不能像 + 一样作为参数传入 reduce 函数呢?其实我偷偷把报错信息藏起来了。让我把它放出来,再看一下上面例子中的错误:

[true, true, false].reduce(true, &&) // ❌ Cannot convert value of type '(Bool, @autoclosure () throws -> Bool) throws -> Bool' to expected argument type '(Bool, Bool) throws -> Bool'

报错说的很直接:无法将 (Bool, @autoclosure () throws -> Bool) throws -> Bool 类型转换为 (Bool, Bool) throws -> Bool,也就是 @autoclosure () -> Bool 无法被直接当做 Bool 来传递。所以直接原因还是类型转换的问题。

用一段代码可以更好地说明这个问题:

func foo(_ action: (Bool, @autoclosure () /*throws*/ -> Bool) throws -> Bool) { /* ... */ }

foo(&&) // `&&` 可以作为参数正常使用了

@autoclosure:巧妙地语法糖

@autoclosure 是一个纯粹的语法糖,它告诉编译器:把调用时写在这里的表达式自动包成一个闭包,而不是立即求值。

所以 @autoclosure () -> Bool 本质上还是一个闭包类型 () -> Bool,它不是也没有办法转换成一个 Bool 类型。至少在 Swift 6 时代还没有这种特性。

如果没有 @autoclosure,当我们想通过闭包来实现 “延迟调用” 时,可能就要想一想 “用闭包再额外包一层” 这种不优雅的实现,会不会让自己的早餐吐出来:

// `assert` 断言通过闭包实现了判断条件的延迟调用,进而在 RELEASE 模式下获得更好的性能。
// 假设下面是 assert 函数的简化定义
func assert(_ condition: /* @autoclosure */ () -> Bool,  ...) { ... }

// 如果没有 @autoclosure,调用时就必须手动包裹一层闭包
assert({ a > b })

总结

总结一下,Swift 从 1.0 开始就开始使用 @autoclosure优雅地实现逻辑运算符的短路求值,或者其他延迟求值的相关逻辑。这也导致了 &&|| 这两个逻辑运算符不能像普通的运算符一样那么地 “自由”。

如果有需要,开发者可以自行封装相关函数,从而为逻辑运算符插上自由地翅膀。