Swift: Swift 属性深入:存储属性、计算属性、属性观察器与懒加载
属性是 Swift 类型中关联值的方式。除了基本的存储属性,Swift 还提供了计算属性、属性观察器、懒加载等强大特性,让你精确控制属性的读写行为。
1. 你将学到
- 存储属性与计算属性的区别和用法
willSet和didSet属性观察器监控属性变化lazy延迟加载优化性能和资源利用static类型属性在类型级别共享数据- 属性包装器的基本概念
2. 一个移动开发者的真实故事
(1) 痛点:用户头像每次显示都从网络加载
Bob 实现了一个用户个人资料页面,每次显示头像时都调用网络请求:
SWIFT
class UserProfile {
var avatarUrl: String
func displayAvatar() {
// 每次都从网络下载头像
downloadImage(from: avatarUrl) { image in
// 显示图片
}
}
}
每打开一次个人资料就下载一次头像。如果用户反复进出页面,相同的网络请求重复执行,浪费流量且加载缓慢。
(2) 懒加载 + 计算属性的解法
SWIFT
class UserProfile {
var avatarUrl: String
lazy var cachedAvatar: UIImage? = {
// 只执行一次——首次访问时下载
return downloadImage(from: avatarUrl)
}()
}
配合计算属性实时计算,懒加载只执行一次,后续访问直接返回缓存值。
(3) 收益:按需加载,自动缓存
| 维度 | 直接下载 | 懒加载 |
|---|---|---|
| 加载时机 | 每次访问 | 首次访问 |
| 重复请求 | 每次重新下载 | 只下载一次 |
| 内存占用 | 即使不显示也加载 | 按需分配 |
| 启动速度 | 慢(预加载无关数据) | 快(延迟初始化) |
| 代码复杂度 | 需手动处理缓存 | Swift 自动管理 |
3. 存储属性与计算属性
(1) 存储属性
存储属性是存储在类或结构体实例中的常量或变量:
graph TB
A[属性类型] --> B[存储属性]
A --> C[计算属性]
B --> D["let 常量存储属性"]
B --> E["var 变量存储属性"]
C --> F["getter — 读取时计算"]
C --> G["setter — 设置时处理"]
| 特性 | 存储属性 | 计算属性 |
|---|---|---|
| 内存 | 占用实例内存 | 不占实例内存 |
| 读写方式 | 直接读写 | 通过 getter/setter |
| 类型 | let/var | 仅 var |
| 观察器 | 支持 willSet/didSet | 不支持 |
| 初始化 | 必须初始化或 init 赋值 | 不需要初始化 |
▶ 示例:存储属性
SWIFT
// ============================================
// 存储属性的基本用法
// ============================================
struct User {
let id: Int // 常量存储属性
var name: String // 变量存储属性
var email: String
}
var user = User(id: 1, name: "Alice", email: "alice@example.com")
user.name = "Alice Smith" // 变量属性可以修改
// user.id = 2 // 编译错误!常量属性不可修改
print("\(user.name) (\(user.email))")
输出:
TEXT 📖 仅展示Alice Smith (alice@example.com)
(2) 计算属性
计算属性不存储值,每次访问时计算后返回:
SWIFT
// ============================================
// 计算属性——摄氏/华氏温度互转
// ============================================
struct Temperature {
var celsius: Double
// 计算属性——从 celsius 计算 fahrenheit
var fahrenheit: Double {
get {
return celsius * 9 / 5 + 32
}
set(newFahrenheit) {
celsius = (newFahrenheit - 32) * 5 / 9
}
}
}
var temp = Temperature(celsius: 25)
print("Celsius: \(temp.celsius)°C")
print("Fahrenheit: \(temp.fahrenheit)°F")
// 通过设置 fahrenheit 间接修改 celsius
temp.fahrenheit = 100
print("After setting to 100°F:")
print("Celsius: \(temp.celsius)°C")
输出:
TEXT 📖 仅展示Celsius: 25°C Fahrenheit: 77.0°F After setting to 100°F: Celsius: 37.77777777777778°C💡 提示: 只读计算属性可以省略
get和花括号:var doubled: Int { value * 2 }。
4. 属性观察器
属性观察器监控存储属性的变化,在属性值将要改变和已经改变时触发。
| 观察器 | 时机 | 参数 | 常见用途 |
|---|---|---|---|
willSet |
值被存储之前 | newValue |
更新前校验、日志 |
didSet |
值被存储之后 | oldValue |
更新 UI、同步数据 |
▶ 示例:willSet 和 didSet
SWIFT
// ============================================
// 属性观察器监控用户分数变化
// ============================================
class Player {
var name: String
var score: Int = 0 {
willSet {
print("分数将从 \(score) 变为 \(newValue)")
}
didSet {
print("分数已从 \(oldValue) 变为 \(score)")
if score > 100 {
print("🎉 恭喜 \(name) 获得高分!")
}
}
}
var level: Int {
// 根据分数计算的只读计算属性
switch score {
case 0..<50: return 1
case 50..<100: return 2
default: return 3
}
}
init(name: String) {
self.name = name
}
}
let player = Player(name: "Alice")
player.score = 60
print("Level: \(player.level)")
player.score = 120
print("Level: \(player.level)")
输出:
TEXT 📖 仅展示分数将从 0 变为 60 分数已从 0 变为 60 分数将从 60 变为 120 分数已从 60 变为 120 🎉 恭喜 Alice 获得高分! Level: 3⚠️ 注意: 属性观察器不能用于计算属性,因为计算属性没有值需要"观察"。不要在
willSet或didSet中再次设置该属性,会导致无限循环。
5. 懒加载与类型属性
(1) lazy 懒加载
lazy var 在属性首次被访问时才初始化,适用于创建成本高或不一定用到的属性:
graph LR
A["声明 lazy var property"] --> B["实例创建时"]
B --> C["property 未初始化"]
C --> D["首次访问 property"]
D --> E["执行初始化闭包"]
E --> F["之后访问:返回缓存值"]
| 场景 | 非懒加载 | 懒加载 |
|---|---|---|
| 初始化时机 | 实例创建时 | 首次访问时 |
| 大文件读取 | 无论用不用都加载 | 用到才加载 |
| 网络请求 | 预加载 | 按需加载 |
| 复杂计算 | 即时计算 | 延迟计算 |
▶ 示例:懒加载配置
SWIFT
// ============================================
// lazy 懒加载:数据库配置
// ============================================
class DatabaseManager {
let configFile: String
// 懒加载——只有执行查询时才初始化数据库连接
lazy var connection: String = {
print("正在建立数据库连接(只执行一次)...")
// 模拟建立连接
return "Connected to \(configFile)"
}()
init(configFile: String) {
self.configFile = configFile
}
func query(_ sql: String) {
print("使用连接:\(connection) 执行查询:\(sql)")
}
}
let db = DatabaseManager(configFile: "app.db")
print("DatabaseManager 已创建,连接未初始化")
db.query("SELECT * FROM users")
db.query("SELECT * FROM orders")
// 第二次访问 connection 时不会重新初始化
输出:
TEXT 📖 仅展示DatabaseManager 已创建,连接未初始化 正在建立数据库连接(只执行一次)... 使用连接:Connected to app.db 执行查询:SELECT * FROM users 使用连接:Connected to app.db 执行查询:SELECT * FROM orders⚠️ 注意:
lazy必须和var配合使用(let无法延迟初始化)。懒加载属性不是线程安全的——多线程同时首次访问可能导致多次初始化。
(2) 类型属性(static)
类型属性属于类型本身,而非某个实例。所有实例共享同一份数据:
SWIFT
// ============================================
// static 类型属性
// ============================================
struct AppConfig {
static let appName = "MySwiftApp"
static var version = "1.0"
static var launchCount = 0
static func incrementLaunch() {
launchCount += 1
}
}
// 通过类型名直接访问,无需实例
print(AppConfig.appName)
AppConfig.version = "1.1"
AppConfig.incrementLaunch()
AppConfig.incrementLaunch()
print("Version: \(AppConfig.version), Launches: \(AppConfig.launchCount)")
输出:
TEXT 📖 仅展示MySwiftApp Version: 1.1, Launches: 2
6. 完整示例:个人资料管理系统
SWIFT
// ============================================
// 完整示例:个人资料管理系统
// 功能:存储/计算属性 + 属性观察器 + lazy + static
// ============================================
import Foundation
// 1. 全局配置(类型属性)
struct Config {
static let maxAvatarSizeMB = 5.0
static var apiBaseURL = "https://api.example.com"
static var userCount = 0
}
// 2. 用户资料
class UserProfile {
// 存储属性
let id: Int
var name: String {
didSet {
print("用户名已更新:\(oldValue) → \(name)")
}
}
var avatarUrl: String
// 计算属性——根据姓名生成缩写
var initials: String {
name.split(separator: " ").compactMap { $0.first }.map { String($0) }.joined()
}
// 懒加载——首次访问时计算年龄
lazy var ageDescription: String = {
print("首次计算年龄描述...")
return "\(name)'s profile"
}()
// 属性观察器
var email: String {
willSet {
print("正在更新邮箱...")
}
didSet {
print("邮箱已更新为 \(email)")
}
}
init(id: Int, name: String, email: String, avatarUrl: String) {
self.id = id
self.name = name
self.email = email
self.avatarUrl = avatarUrl
Config.userCount += 1
}
deinit {
Config.userCount -= 1
}
}
// 3. 使用
let profile = UserProfile(
id: 1,
name: "Alice Johnson",
email: "alice@example.com",
avatarUrl: "https://example.com/avatar.jpg"
)
print("缩写:\(profile.initials)")
print("年龄:\(profile.ageDescription)")
print("活跃用户数:\(Config.userCount)")
// 修改触发观察器
profile.email = "alice@newdomain.com"
profile.name = "Alice Smith"
输出:
TEXT 📖 仅展示缩写:AJ 首次计算年龄描述... 年龄:Alice Johnson's profile 活跃用户数:1 正在更新邮箱... 邮箱已更新为 alice@newdomain.com 用户名已更新:Alice Johnson → Alice Smith
❓ 常见问题
Q 懒加载属性和计算属性有什么区别?
A 懒加载属性在首次访问时计算并存储结果(之后直接返回缓存值);计算属性每次访问都重新计算(不缓存)。懒加载适合一次性开销,计算属性适合动态值。
Q 属性观察器在初始化器中会触发吗?
A 不会。在
init 中设置属性时,willSet 和 didSet 不会被调用。只有在初始化的实例上「赋值」时才触发。这是 Swift 的设计决策。Q 计算属性可以没有 setter 吗?
A 可以。只读计算属性可以省略
get 关键字和花括号。例如 var doubled: Int { value * 2 }。只读计算属性不能再使用 -> 符号。Q 类型属性能被子类重写吗?
A
static 属性不能被子类重写。如果在 class 中需要可重写的类型属性,使用 class 关键字代替 static。Q 一个属性可以同时有观察器和 setter 吗?
A 不可以。属性观察器只适用于存储属性。计算属性的变化在 setter 中处理,不需要观察器。两者互斥。
📖 小节
- 存储属性直接存储值,计算属性通过 getter/setter 间接提供值
willSet在值存储前触发,didSet在值存储后触发lazy var属性在首次访问时初始化,适合昂贵的资源创建static属性属于类型本身,所有实例共享- 只读计算属性可以省略
get和 return 花括号 - 属性观察器在
init中不会触发
📝 作业
- 基础题: 定义一个
Circle结构体,包含radius存储属性和一个计算属性area(返回面积,πr²)。创建半径为 5 的 circle 并打印面积。 - 进阶题: 创建一个
BankCard类,包含balance存储属性并添加didSet观察器——余额低于 0 时打印警告,低于 10 时打印低余额提醒。实现withdraw(_:)和deposit(_:)方法。 - 挑战题: 实现一个
Logger类,使用static类型属性存储日志级别和日志历史数组。提供一个static方法log(_:level:)向历史数组追加日志条目,并提供static计算属性formattedLog返回格式化的日志文本。使用didSet在新增日志时自动检查数量限制。