Foundation 新特性
What's new in Foundation
2025年6月12日
一句话判断
如果你还在用 NSPredicate 字符串表达式做过滤,这次更新给了你一个必须迁移的理由——#Predicate 宏把运行时炸弹拆成了编译时错误。
这场 Session 讲了什么
Foundation 这次更新的主线是把 ObjC 时代留下来的设计债一笔笔还掉。Locale 从引用类型变成值类型,听起来像是内部实现细节,但它直接干掉了一类多线程 bug:两个线程在用户切换系统语言的瞬间读到不同 locale 值,导致格式化结果不一致。这种 bug 极难复现,过去你只能靠加锁或者祈祷来规避。现在 Locale.current 返回的是调用时刻的快照,拿到手里就不会变。
JSONEncoder 和 JSONDecoder 的底层重写带来了最高 40% 的性能提升,对处理大量 API 响应的应用来说是白捡的优化。另一个实用变化是 OutputFormatting.withoutEscapingSlashes,终于可以控制 / 的转义行为了——某些后端对 \/ 的处理一直是个坑。
#Predicate 宏是这次的重头戏。NSPredicate 用字符串写 "age >= 18",拼错了只有运行时才知道;#Predicate 让 Swift 编译器在构建阶段就帮你检查类型和属性名。配合 SortDescriptor 的链式 API,Core Data 查询终于可以全链路类型安全了。
值得深挖的点
Locale 值语义化:不是重构,是并发模型的重写
旧 Locale 是引用类型,Locale.current 背后依赖全局状态。这意味着它本质上是一个隐式的共享可变状态——并发编程里最难搞的那类东西。多线程环境下,线程 A 拿到 zh_CN,线程 B 在同一瞬间可能拿到用户刚切换过去的 en_US,而你完全无法预测谁拿到哪个值。
新 Locale 是值类型,创建后不可变。Locale.current 返回的是那一刻的快照,像拍照一样。这和 SwiftUI 的设计哲学一致——状态要么不可变,要么被明确管理。trade-off 是:如果你需要实时响应系统语言变化,不能只靠缓存的 Locale 了,得监听 NSLocale.currentLocaleDidChangeNotification 然后主动刷新。但这个代价是值得的,因为你把隐式的全局状态依赖变成了显式的事件驱动。
对混编项目来说,NSLocale 和 Locale 的 API 表面很相似,但语义完全不同。一个偷偷变,一个不会变。如果你在 ObjC 代码里混用两种 locale,反而可能制造新的不一致。迁移时要连贯,别在同一套数据流里混着用。
#Predicate 宏:编译时安全的代价
NSPredicate(format: "age >= 18") 这种写法的问题不只是拼写错误——它是把 Swift 的类型系统绕过去了。编译器完全不知道你在过滤什么,也不知道 age 到底是什么类型。字符串里的 typo 只有运行时 crash 了你才知道。
#Predicate 宏用 Swift 编译器本身来做类型检查,这意味着属性名错一个字母都编译不过。代价是什么?闭包内只能访问被过滤对象的属性,不能捕获外部变量。这在构建动态查询时会有些别扭——你不能像写普通闭包那样随意引用外部状态。动态条件组合需要用谓词的参数化版本或者多个谓词拼接,写起来比字符串拼接复杂一些。
另一个限制:#Predicate 目前主要用于集合过滤和 Core Data 的 NSFetchRequest,如果你之前用 NSPredicate 做 KVO 或者复杂的动态查询,迁移路径还不那么平滑。但方向很明确——新代码都应该用 #Predicate,老代码逐步替换。
代码片段
值语义 Locale:本地化价格展示
多币种电商场景下,拿到 locale 快照后格式化不会被系统语言切换干扰。
let locale = Locale(identifier: "zh_CN")
let price = 1234.56
let formatted = price.formatted(.currency(code: "CNY").locale(locale))
// "¥1,234.56"
坑:Locale.current 拿到的是快照,如果你需要跟随系统语言实时变化,得监听 NSLocale.currentLocaleDidChangeNotification。
#Predicate 宏:类型安全的集合过滤
用户列表按年龄过滤,拼错属性名直接编译不过。
let adults = users.filter(#Predicate<User> { $0.age >= 18 })
// 如果 User 没有 age 属性,编译器报错,不会等到运行时 crash
坑:闭包内不能捕获外部变量,动态条件需要额外的谓词组合逻辑。
自定义 FormatStyle:相对时间展示
社交应用里的”3小时前”、“昨天”可以用 FormatStyle 封装复用。
struct RelativeDateFormatStyle: FormatStyle {
func format(_ value: Date) -> String {
let formatter = RelativeDateTimeFormatter()
formatter.locale = Locale(identifier: "zh_CN")
formatter.unitsStyle = .short
return formatter.localizedString(for: value, relativeTo: .now)
}
}
let ago = Date().addingTimeInterval(-3600)
ago.formatted(RelativeDateFormatStyle()) // "1小时前"
坑:RelativeDateTimeFormatter 创建成本高,别在高频调用路径上反复 new,缓存实例或者用静态属性。
最佳实践
按以下顺序推进:
先扫描项目里所有 Locale.current 的调用。如果存在多线程场景(GCD、async/await 并发),优先迁移成值语义 Locale。这类 bug 不出事则已,一出事就是用户投诉格式错乱、本地化显示异常,且大概率无法复现。
JSONEncoder/JSONDecoder 的性能提升是系统级的,不需要改代码,升级部署目标即可。但如果项目里有大型 JSON 解析的性能基线测试,建议跑一遍对比数据,写进周报——这种白捡的优化值得让老板知道。
#Predicate 宏不建议急着批量替换已有的 NSPredicate。新代码一律用 #Predicate,老代码在改到相关模块时顺手迁移。混编项目里 ObjC 那边的 NSPredicate 维持现状即可,别为了迁移而迁移。
还有什么值得关注
- FormatStyle 扩展:自定义格式化样式的原生支持让日期、数字、货币的本地化展示更灵活,和
AttributedString配合使用效果很好。 - 农历支持增强:原生查询节气、传统节日信息,做日历类应用的中国开发者直接省掉第三方库。
- SortDescriptor 链式构建:配合
#Predicate使用,排序和过滤终于可以全链路类型安全,告别NSSortDescriptor的字符串键路径。