var name = ""。这是 Swift 中最常见的一行代码,但可以放在这里的选项比想象中更多:存储属性、计算属性、lazy、willSet 和 didSet,以及类型属性。即使学过所有语法,真正问到“这个值该用 lazy 还是计算属性?”时,也常常没有清晰标准。
这是 Swift 基础系列第 3 篇。本文不罗列语法,而是围绕选择标准整理五种属性。核心问题只有一个:这个值会被存储、被计算,以及何时创建?
在因为需要共享状态而选择类型属性之前,也请确认 Swift 单例的测试成本,这样能更清楚地区分 static let 与 static var 的边界。
存储 vs. 计算——内存中的值与按需创建的值
属性的第一个分岔是存储还是计算。
存储属性(stored property)是实例中实际占用空间的值。写成 var name = "Kim" 后,实例内存会为 name 分配空间。struct 的大小由其存储属性之和决定。
计算属性(computed property)不占用存储空间。每次访问时执行代码生成值,本质上就是一个函数。
struct Rectangle {
var width: Double // 存储
var height: Double // 存储
var area: Double { // 计算——没有存储空间
width * height
}
}
你也可以把 area 做成存储属性。但这样每次 width 改变都要更新 area,两者不一致就会产生 bug。由此得到第一个标准:由其他值推导出的值不要存储,而应计算。保持唯一的事实来源,就能从根本上消除同步 bug。
给计算属性添加 set 后就可以写入。存储 celsius、计算 fahrenheit;给 fahrenheit 赋值时,可以反向计算并更新 celsius。此时存储的事实仍然只有一个,其他都是观察这个事实的窗口。
也说明一下何时用计算属性代替函数。Apple API 设计指南的惯例是:计算成本低(通常接近 O(1))、没有副作用,并且概念上属于“对象的属性”时使用属性;计算昂贵或有副作用时使用方法。这就是 array.count 是属性而 array.sorted() 是方法的原因。
lazy——首次使用时创建的存储属性
lazy 是第三种选择。它是存储属性,但不是在创建实例时初始化,而是在首次访问时初始化。
class ImageProcessor {
lazy var filters: [Filter] = loadExpensiveFilters()
}
常见用途有两种。第一,初始化成本高但可能不会使用的值;每个实例都准备重量级资源会造成浪费。第二,需要引用 self 的属性。普通存储属性的默认值会在 init 结束前计算,因此不能使用 self;lazy 会把初始化推迟到首次访问,所以允许引用 self。这也是立即执行闭包(= { ... }())经常与 lazy 搭配的原因。
有两点需要注意。lazy 必须是 var。由于它的值会从类似 nil 的未初始化状态变成实际值,所以不能是 let。此外它不是线程安全的。多个线程同时首次访问时,初始化可能执行两次。如果多线程环境中的值必须只创建一次,就需要其他机制。
它与计算属性的区别也很明确:lazy 只计算一次并保存结果,之后返回该值;计算属性每次都会重新计算。“昂贵但不变的值”用 lazy,“便宜但必须始终最新的值”用计算属性。
willSet 和 didSet——响应值的变化
存储属性可以添加观察器。它们会在值变化前(willSet)和变化后(didSet)执行代码。
class ProgressBar {
var progress: Double = 0 {
didSet {
// oldValue会自动提供
guard progress != oldValue else { return }
updateUI()
}
}
}
典型用途是在值变化时自动处理后续工作,例如更新 UI、记录日志,以及验证值(超出范围时回退)。无需手动创建 setter,就能保留赋值语法并加入附加行为。
实际工作中有两个容易混淆的规则。第一,在 init 中赋值时不会调用观察器。初始化是“设置”而不是“变化”,所以 didSet 中的 UI 更新不会作用于初始值。第二,修改值类型的属性时,包含它的属性的 didSet 也会被调用。如 person.name = "Lee",即使只修改 struct 内部,也会调用 person 的 didSet。值类型章节中的“修改就是替换成新值”这一语义在这里同样成立。
要警惕滥用 didSet。如果其中堆积了逻辑,就很难追踪一行赋值究竟做了什么。改一个值却发出网络请求,就违反了最小意外原则。didSet 中只保留轻量同步,把繁重逻辑移到显式方法中更安全。
类型属性——附加在类型而非实例上的值
添加 static 后,属性会附加到类型本身,而不是实例。
struct APIConfig {
static let baseURL = URL(string: "https://api.example.com")!
static var requestCount = 0
}
无论创建多少实例,类型属性都只有一个。它适合常量集合(配置值、共享格式化器),标准库也常在 Int.max 和 Double.pi 这样的地方使用它。
有一个值得了解的事实:static let 保证线程安全的延迟初始化。它在首次访问时只初始化一次,并且能安全处理并发访问。lazy var 没有这项保证,而 static 有。这也是单例中的 static let shared 无需额外锁机制即可成立的依据。不过可变的 static var 状态实际上就是全局状态,会破坏测试隔离并成为数据竞争候选。单例为何被称为反模式会在另一篇文章中讨论;这里记住“static var 是最后手段”即可。
选择流程图——用四个问题总结
把五种类型压缩成一句话问题,就是下面这样。
- 是否由其他值推导而来? → 计算属性。只存储一个事实来源。
- 初始化成本高,或需要 self 吗? → lazy var。但要注意多线程首次访问。
- 是否有跟随值变化的行为? → 存储属性 + didSet。但只做轻量工作。
- 是否不依赖实例、只需要一个值? → static。如果是 let,还能获得安全的延迟初始化。
- 如果都不是 → 普通存储属性。大多数情况下,这就是答案。
SwiftUI 的 @State、@Published 等属性包装器(property wrapper)最终也是建立在这套属性系统上的语法。理解存储、计算和观察器的原理后,就能看出包装器其实是“包裹存储属性并自动化 didSet 类行为”。直接创建属性包装器将在中级系列中介绍。
直接运行确认的结果
我在 Apple Swift 6.3.3 中,于同一个对象记录了 lazy 初始化次数、didSet 的旧值与新值,以及计算属性结果。输出显示读取 lazy 两次,并将存储属性从 0 改为 7。
properties=lazy-builds:1,didSet:0->7,computed:14
正因为这个小型运行结果,如果缓存确实必须只创建一次,我不会只依赖 lazy 这个语法。只要存在并发访问的可能,就测试初始化次数并确认是否需要额外同步。相反,在 didSet 中只保留像上述输出那样观察状态变化的轻量行为,把可能失败的 I/O 移到显式方法中。
总结
- 属性的第一个标准是存储还是计算。由其他值推导出的值使用计算,以保持唯一的事实来源。
- lazy 是在首次访问时初始化的存储属性,可解决昂贵初始化和 self 引用问题。代价是必须使用 var,且不具备线程安全性。
- willSet/didSet 用于响应值变化,但在 init 中不会调用;如果加入繁重逻辑,也会难以追踪。
- static let 是保证线程安全延迟初始化的类型属性;static var 是全局状态,应作为最后手段。
下一篇是基础第 4 篇:guard。我们将深入解析在 Optional 文章中短暂介绍的提前退出,并整理 Swift 社区为何要与缩进展开战争。
推荐阅读
- Swift Optional 的真相:其实是 enum(包括 5 种实用解包标准)
- [Swift 基础 #4] 正确使用 Swift guard:用提前退出放倒灾难金字塔
- [Swift 基础 #5] Swift 错误处理全景图:throws、try?、try!、Result 何时使用?
来源与验证
- The Swift Programming Language: PropertiesSwift.org · 官方文档 · 核查 2026年8月26日依据: 存储、计算、延迟存储、属性观察器和类型属性的语言规则

![[Swift 基础 #3] Swift 属性总览:存储、计算、lazy、didSet 的 4 个选择标准 封面图](/assets/images/posts/7621582a-1bb6-4f5a-8fca-12fae48417af/1.jpg)