Swift 中的属性(Properties)是与类型(结构体、类、枚举)关联的值,可以是存储属性、计算属性或类型属性。它们是 Swift 面向对象和协议编程的核心。
一、属性分类概览
| 类型 | 说明 | 适用 |
|---|---|---|
| 存储属性(Stored) | 实际存储值的变量/常量 | struct、class |
| 计算属性(Computed) | 通过计算得出,不存储值 | struct、class、enum |
| 类型属性(Static / Class) | 属于类型本身 | 所有类型 |
| 属性观察器 | 监听属性变化 | 存储属性 |
| 延迟属性(lazy) | 首次访问时初始化 | 存储属性 |
1. 存储属性(Stored Properties)
struct Point {
var x: Double = 0.0 // 可变存储属性
let y: Double // 常量存储属性
}
class Person {
var name: String // 实例存储属性
static var species = "Homo sapiens" // 类型存储属性
}
struct/enum:支持var/letclass:只支持var(即使是常量属性)
2. 计算属性(Computed Properties)
不存储值,通过 get / set 计算。
struct Circle {
var radius: Double
// 只读计算属性(可省略 get)
var diameter: Double {
return radius * 2
}
// 可读写计算属性
var area: Double {
get {
return .pi * radius * radius
}
set {
radius = sqrt(newValue / .pi)
}
}
// 简写 setter
var perimeter: Double {
get { 2 * .pi * radius }
set { radius = newValue / (2 * .pi) }
}
}
var circle = Circle(radius: 5)
print(circle.area) // 78.54...
circle.area = 154 // 自动更新 radius
print(circle.radius) // ≈7.0
只读计算属性(推荐简写)
var description: String {
"圆形,半径: \(radius)"
}
3. 类型属性(Type Properties)
使用 static(结构体/枚举)或 class(类中可重写)
struct Math {
static let pi = 3.14159
static var e = 2.71828
}
class AudioManager {
static let shared = AudioManager()
class var maxVolume: Int { 100 } // 可被子类重写
}
print(Math.pi) // 3.14159
print(AudioManager.shared)
static:不可重写class:仅类中可用,可被子类重写
4. 属性观察器(Property Observers)
监听存储属性的变化。
class StepCounter {
var totalSteps: Int = 0 {
willSet(newTotal) {
print("即将设置为 \(newTotal)")
}
didSet {
if totalSteps > oldValue {
print("增加了 \(totalSteps - oldValue) 步")
}
}
}
}
var counter = StepCounter()
counter.totalSteps = 100
// 即将设置为 100
// 增加了 100 步
counter.totalSteps = 150
// 增加了 50 步
不触发:
- 初始化时
willSet/didSet内部修改自身- 计算属性
5. 延迟存储属性(lazy)
首次访问时才初始化,适用于复杂或耗时对象。
class DataImporter {
init() { print("正在导入数据...") }
var data = ["Apple", "Banana"]
}
class DataManager {
lazy var importer = DataImporter()
var records: [String] = []
}
let manager = DataManager()
manager.records.append("User1")
// 此时 importer 未初始化
manager.importer.data // 打印 "正在导入数据..."
注意事项
- 必须是
var(不能是let) - 多线程访问需注意线程安全
- 不能用于
let或static/class属性(除非结合lazy static技巧)
6. 属性包装器(Property Wrapper)Swift 5.1+
封装属性的存储和逻辑。
@propertyWrapper
struct Clamped<T: Comparable> {
private var value: T
private let range: ClosedRange<T>
init(wrappedValue: T, range: ClosedRange<T>) {
self.range = range
self.value = clamped(wrappedValue, to: range)
}
var wrappedValue: T {
get { value }
set { value = clamped(newValue, to: range) }
}
private func clamped(_ value: T, to range: ClosedRange<T>) -> T {
min(max(value, range.lowerBound), range.upperBound)
}
}
struct VolumeControl {
@Clamped(range: 0...100) var level: Int = 50
}
var control = VolumeControl()
control.level = 150
print(control.level) // 100
control.level = -10
print(control.level) // 0
内置属性包装器
| 包装器 | 用途 |
|---|---|
@State | SwiftUI 状态管理 |
@Published | Combine 发布变化 |
@Environment | 读取环境值 |
@ObservedObject | 观察对象 |
@AppStorage | UserDefaults 持久化 |
import SwiftUI
struct ContentView: View {
@State private var username = ""
@AppStorage("hasOnboarded") var hasOnboarded = false
var body: some View { ... }
}
7. 投影属性(Projected Value)
用 $ 访问包装器的“投影值”。
@propertyWrapper
struct NonEmpty {
private var value: String = ""
var wrappedValue: String {
get { value }
set { value = newValue.isEmpty ? "默认值" : newValue }
}
var projectedValue: Bool { !value.isEmpty }
}
struct User {
@NonEmpty var name: String
}
var user = User()
print(user.$name) // false
user.name = "Tom"
print(user.$name) // true
8. 全局与局部属性
var globalCounter = 0 // 全局(延迟初始化)
func test() {
var local = 1 // 局部
lazy var computed = expensiveOperation() // 错误!lazy 不能用于局部
}
9. 访问控制
class BankAccount {
private(set) var balance: Double = 0 // 可读,不可写(外部)
func deposit(_ amount: Double) {
balance += amount
}
}
最佳实践总结
| 场景 | 推荐 |
|---|---|
| 简单数据 | 存储属性(struct) |
| 派生值 | 计算属性 |
| 单例 / 常量 | static let |
| 延迟加载 | lazy var |
| 值限制 | @propertyWrapper |
| 变化通知 | didSet / @Published |
| 配置项 | static var |
常见面试题
- 计算属性会占用内存吗?
→ 不会,它是方法调用。 lazy和static能一起用吗?
→ 不能直接,但可用static var _cache = [String: Any]()+ 方法封装。- 属性观察器在初始化时触发吗?
→ 不触发。 - 如何实现线程安全的 lazy 属性?
private lazy var _data: [String] = {
// 复杂初始化
return []
}()
// 配合 DispatchQueue 同步访问
小技巧
// 1. 自动合成 Equatable
struct User: Equatable {
var id: Int
var name: String
// 自动 == 比较
}
// 2. 键路径动态访问
let keyPath = \VolumeControl.level
print(control[keyPath: keyPath])
// 3. @autoclosure 延迟执行
func log(_ message: @autoclosure () -> String) {
#if DEBUG
print(message())
#endif
}
高级话题(可继续提问)
- Copy-on-Write 优化(Array、String 等)
- 属性包装器底层原理(_propertyName, $propertyName)
- SwiftUI 中
@Statevs@Bindingvs@ObservedObject - 反射与
Mirror遍历属性 - KVC / KVO 在 Swift 中的替代方案
需要完整项目示例、性能对比、或自定义属性包装器实战?欢迎继续提问!