什么是计算属性?
Swift 中的属性分为两大族谱:
类型 | 描述 | 存储值 |
---|---|---|
Stored Property(存储属性) | 保存一个固定的值,最常见 | ✅ |
Computed Property(计算属性) | 每次被访问时实时计算出一个值,不占用额外存储 | ❌ |
计算属性的核心特征:"算完即走,不落痕迹"。
它通过 getter
(必含)和可选的 setter
来间接读取或修改其他属性。
只读计算属性:最常见形态
典型场景:基于已有属性生成新值
swift
struct Content {
var name: String
let fileExtension: String
// 计算属性:拼接文件名
var filename: String {
name + "." + fileExtension // 单行可省略 return
}
}
let content = Content(name: "swiftlee-banner", fileExtension: "png")
print(content.filename) // swiftlee-banner.png
filename
是 只读 的,无法赋值:content.filename = "new.png"
会编译错误。- 若显式写
get
,代码更冗余,不推荐:
swift
var filename: String {
get { name + "." + fileExtension }
}
可读可写计算属性:暴露私有模型的接口
有时我们想把复杂的模型隐藏在内部,只暴露一个"代理"属性供外部读写------计算属性就能优雅完成。
swift
struct ContentViewModel {
private var content: Content // 真正的数据模型对外不可见
init(_ content: Content) {
self.content = content
}
// 计算属性:既读又写,内部转发到 content.name
var name: String {
get { content.name }
set { content.name = newValue } // newValue 是 Swift 的默认形参
}
}
var content = Content(name: "swiftlee-banner", fileExtension: "png")
var viewModel = ContentViewModel(content)
viewModel.name = "SwiftLee Post"
print(viewModel.name) // SwiftLee Post
效果:调用者只知道 name
,却不知道内部还有一个复杂的 Content
对象。
在 Extension 中使用计算属性:无痛加功能
计算属性可以写在 extension 里,为现有类型(尤其是系统类型)增加无痛扩展。
swift
import UIKit
extension UIView {
// 快速访问 frame 尺寸
var width: CGFloat {
frame.size.width
}
var height: CGFloat {
frame.size.height
}
}
let view = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 480))
print(view.width) // 320
优点:无需继承,即刻生效。
在子类中重写计算属性
计算属性还可以 被 override,常用于定制 UIKit 行为。
最简单:直接硬编码
swift
final class HomeViewController: UIViewController {
override var prefersStatusBarHidden: Bool { true }
}
进阶:由内部存储属性驱动
swift
final class HomeViewController: UIViewController {
private var shouldHideStatusBar: Bool = true {
didSet {
// 状态改变后刷新系统样式
setNeedsStatusBarAppearanceUpdate()
}
}
override var prefersStatusBarHidden: Bool { shouldHideStatusBar }
}
何时使用计算属性?官方推荐 3 个条件
条件 | 示例场景 |
---|---|
值依赖其他属性 | 上文的 filename |
在 extension 中定义 | 给 UIView 加 width /height |
作为内部对象的受控访问点 | ContentViewModel.name |
个人补充:若计算逻辑 纯静态、且 无状态依赖,考虑直接声明为
static let
,避免每次调用重新计算。
性能陷阱:每次访问都会重新计算
计算属性 不会缓存结果,高频访问 + 重计算 = 性能灾难。
反面教材:每次都排序
swift
struct PeopleViewModel {
let people: [Person]
var oldest: Person? {
people.sorted { $0.age > $1.age }.first // O(n log n) 每次都要跑
}
}
优化:移入初始化器,只算一次
swift
struct PeopleViewModel {
let people: [Person]
let oldest: Person?
init(people: [Person]) {
self.people = people
oldest = people.max(by: { $0.age < $1.age }) // 或者自己实现一次遍历找最大值
}
}
经验法则:
- 数据量小 or 变化频繁 → 计算属性
- 数据量大 or 代价高昂 → 存储属性 + 预计算
计算属性 VS 方法:如何抉择?
维度 | 计算属性 | 方法 (func) |
---|---|---|
参数 | 无 | 可接受参数 |
可读性暗示 | "轻量级值" | "可能耗时" |
测试/模拟 | 不方便 mock | 容易 stub/mock |
适用场景 | 简单、无参数、依赖内部状态 | 复杂、需参数、可能异步或耗时 |
一句话:重逻辑用方法,轻数据用属性。
总结 & 扩展场景
核心结论
- 计算属性 = 无存储 + 实时计算 + 可选 setter。
- 带来 语义化 API 与 封装性,但 不缓存。
- 在 extension、子类 override、MVVM 视图模型中大放异彩。
扩展实战场景
场景 | 代码示例 & 注释 |
---|---|
格式化输出 | var displayPrice: String { "(price)$" } |
链式依赖 | var isAdult: Bool { age >= 18 } → var canDrink: Bool { isAdult } |
Core Data 轻量级封装 | 在 NSManagedObject 的 extension 中,把 primitiveValue 包装成计算属性,隐藏 KVC 细节 |
SwiftUI 绑定 | 在 ObservableObject 中,用计算属性把 @Published 的私有变量暴露为 public 接口 |
缓存友好型计算属性 | 结合 lazy 或自定义缓存字典,实现 "第一次算,之后读" 的懒加载计算属性 |