@autoclosure:把"表达式"包成"闭包",实现"短路求值"
- 场景回顾
swift
/// 自己写的 assert(简化版)
func myAssert(_ condition: Bool, _ message: String) {
if !condition { print("❌ \(message)") }
}
myAssert(2 > 3, "2 不可能大于 3") // 无论断言是否成功,message 都会被求值
问题:
- 字符串先拼接完成,再传进函数------性能浪费。
- 若拼接代价高(
"计算成本:\(expensive())"
),则每次调用都白算。
- 用
@autoclosure
延迟求值
swift
func smartAssert(
_ condition: Bool,
_ message: @autoclosure () -> String = ""
) {
if !condition { print("❌ \(message())") }
}
func expensive() -> String {
"一个非常昂贵的算法"
}
smartAssert(2 > 3, "2 大于 3 的成本:\(expensive())")
- 只有当
condition == false
时,message()
才被真正执行。 - 调用方写法与普通传参一样,无需写大括号------这是
@autoclosure
的语法糖核心。
- 官方用法对照
assert(condition:)
fatalError(message:)
os_log
的message
参数
rethrows:让"函数参数"的异常传播出去
- 背景
swift
func map<T>( _ array: [Int], _ transform: (Int) throws -> T) rethrows -> [T] {
try array.map(transform)
}
- 如果
transform
不抛错,map
也不会抛; - 如果
transform
抛错,map
再把异常原路抛回给调用者。
- 与
throws
的区别
关键字 | 谁可能抛错 | 调用方必须用 try |
---|---|---|
throws |
函数本身 | ✅ |
rethrows |
仅闭包参数 | ✅(但闭包不抛就隐式免 try ) |
- 实现
tryMap
swift
extension Array {
func tryMap<T>(_ transform: (Element) throws -> T) rethrows -> [T] {
var result: [T] = []
for e in self { result.append(try transform(e)) }
return result
}
}
let nums = ["1", "2", "A"]
let parsed: [Int] = try nums.tryMap { str in
guard let v = Int(str) else { throw NSError(domain: "NaN", code: 0) }
return v
}
- 坑位
rethrows
函数内部只能抛出由闭包参数传来的错误,不能自己throw
新错误。- 如果闭包有多个,只要其中一个会抛,即可标注
rethrows
。
@escaping:闭包"逃出"函数生命周期
- 什么是"逃逸"
swift
var handlers: [() -> Void] = []
func addHandler(_ handler: () -> Void) {
handlers.append(handler) // 编译错误:闭包可能稍后调用,必须标记 @escaping
}
- 数组把闭包"留住"→ 函数栈已销毁→ 闭包逃出→ 必须加
@escaping
。
- 正确写法
swift
nonisolated(unsafe) var handlers: [() -> Void] = []
func addHandler(_ handler: @escaping () -> Void) {
handlers.append(handler)
}
- 逃逸闭包的内存管理:循环引用
swift
class Request {
var onSuccess: (() -> Void)?
func start() {
onSuccess?()
}
}
class ViewController {
let request = Request()
var name = "VC"
func setup() {
// 强引用 self → 循环引用
request.onSuccess = {
print(self.name)
}
}
}
解决套路:
-
[weak self]
-
[unowned self]
(生命周期确定时) -
用
guard let self else { return }
消除可选链 -
逃逸闭包在异步 API 的典型形态
swift
func loadData(
from url: String,
completion: @escaping (Data?) -> Void
) {
DispatchQueue.global().async {
let data = try? Data(contentsOf: URL(string: url)!)
DispatchQueue.main.async {
completion(data) // 再次逃逸,但编译器已允许
}
}
}
- 与
@nonescaping
(默认)对比
特性 | 默认(非逃逸) | @escaping |
---|---|---|
持有成本 | 低,可栈分配 | 高,必须堆分配 |
捕获策略 | 无需 self. 限定 |
必须显式处理 self |
使用场景 | 同步回调 | 异步、存储、延迟调用 |
4 个关键字组合实战:写个"线程安全且短路"的缓存加载器
swift
import SwiftUI
final class ImageCache {
private var storage: [String: Image] = [:]
private let queue = DispatchQueue(label: "cache", attributes: .concurrent)
/// 仅当 key 不存在时才调用 `factory` 加载
func load(
_ key: String,
factory: @escaping () throws -> Image
) rethrows -> Image {
// 1. 先读缓存(并发读安全)
if let cached = queue.sync(execute: { storage[key] }) {
return cached
}
// 2. 缓存未命中,加锁写
return try queue.sync(flags: .barrier) {
if let cached = storage[key] { return cached } // 双检锁
let image = try factory()
storage[key] = image
return image
}
}
}
亮点:
@escaping
:工厂闭包可能异步下载,必须逃逸。rethrows
:工厂抛错,缓存器再抛给调用者;若工厂不抛,调用方可免try
。@autoclosure
未出现,是因为工厂需要多次调用(双检锁),而 autoclosure 只能一次性求值。
常见面试追问
-
@autoclosure
与"零成本抽象"冲突吗?不冲突。编译器会把包起来的表达式生成匿名闭包,优化级别高时会内联,运行时仍接近零成本。
-
rethrows
可以标记初始化器吗?可以。
swift
init(_ f: () throws -> Void) rethrows { try f() }
-
逃逸闭包为什么默认捕获强引用?
因为闭包生命周期可能长于当前函数,编译器保守地强引所有外部变量;需要开发者显式
weak/unowned
解除循环。