Swift 计算属性(Computed Property)详解:原理、性能与实战

136 阅读3分钟

原文:What is a Computed Property in Swift? – SwiftLee

什么是计算属性?

Swift 中的属性分为两大族谱:

类型描述存储值
Stored Property(存储属性)保存一个固定的值,最常见
Computed Property(计算属性)每次被访问时实时计算出一个值,不占用额外存储

计算属性的核心特征:“算完即走,不落痕迹”。

它通过 getter(必含)和可选的 setter 来间接读取或修改其他属性。

只读计算属性:最常见形态

典型场景:基于已有属性生成新值

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,代码更冗余,不推荐:
var filename: String {
    get { name + "." + fileExtension }
}

可读可写计算属性:暴露私有模型的接口

有时我们想把复杂的模型隐藏在内部,只暴露一个“代理”属性供外部读写——计算属性就能优雅完成。

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 里,为现有类型(尤其是系统类型)增加无痛扩展。

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 行为。

最简单:直接硬编码

final class HomeViewController: UIViewController {
    override var prefersStatusBarHidden: Bool { true }
}

进阶:由内部存储属性驱动

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,避免每次调用重新计算。

性能陷阱:每次访问都会重新计算

计算属性 不会缓存结果,高频访问 + 重计算 = 性能灾难。

反面教材:每次都排序

struct PeopleViewModel {
    let people: [Person]

    var oldest: Person? {
        people.sorted { $0.age > $1.age }.first   // O(n log n) 每次都要跑
    }
}

优化:移入初始化器,只算一次

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
适用场景简单、无参数、依赖内部状态复杂、需参数、可能异步或耗时

一句话:重逻辑用方法,轻数据用属性。

总结 & 扩展场景

核心结论

  1. 计算属性 = 无存储 + 实时计算 + 可选 setter。
  2. 带来 语义化 API 与 封装性,但 不缓存。
  3. 在 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或自定义缓存字典,实现 “第一次算,之后读” 的懒加载计算属性