解密 Swift:为何 reduce 偏爱 + 而非 && ?
作为 Swift 开发者,我们都热爱这门语言提供的优雅和简洁性,尤其是它的函数式编程能力。
Swift 原生提供了一些高阶函数,常见的比如 map
、filter
、reduce
等。这些优雅的高阶函数能够极大的简化我们的代码,但是一个不小心也会报一个让你困扰一整天的错误。
今天,我们就来看看其中一种情况。
思考一下这个场景:对一个整数数组求和。使用 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
,整个表达式就已经确定是true
,b
也不会被执行。
这个特性至关重要,它不仅能提升性能,还能避免潜在的运行时错误,比如我们经常写的代码:
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
来优雅地实现逻辑运算符的短路求值,或者其他延迟求值的相关逻辑。这也导致了 &&
和 ||
这两个逻辑运算符不能像普通的运算符一样那么地 “自由”。
如果有需要,开发者可以自行封装相关函数,从而为逻辑运算符插上自由地翅膀。