Swift: Swift 属性深入:存储属性、计算属性、属性观察器与懒加载

属性是 Swift 类型中关联值的方式。除了基本的存储属性,Swift 还提供了计算属性、属性观察器、懒加载等强大特性,让你精确控制属性的读写行为。

1. 你将学到


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) 存储属性

存储属性是存储在类或结构体实例中的常量或变量:

100%
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

⚠️ 注意: 属性观察器不能用于计算属性,因为计算属性没有值需要"观察"。不要在 willSetdidSet 中再次设置该属性,会导致无限循环。


5. 懒加载与类型属性

(1) lazy 懒加载

lazy var 在属性首次被访问时才初始化,适用于创建成本高或不一定用到的属性:

100%
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 中设置属性时,willSetdidSet 不会被调用。只有在初始化的实例上「赋值」时才触发。这是 Swift 的设计决策。
Q 计算属性可以没有 setter 吗?
A 可以。只读计算属性可以省略 get 关键字和花括号。例如 var doubled: Int { value * 2 }。只读计算属性不能再使用 -> 符号。
Q 类型属性能被子类重写吗?
A static 属性不能被子类重写。如果在 class 中需要可重写的类型属性,使用 class 关键字代替 static
Q 一个属性可以同时有观察器和 setter 吗?
A 不可以。属性观察器只适用于存储属性。计算属性的变化在 setter 中处理,不需要观察器。两者互斥。

📖 小节


📝 作业

  1. 基础题: 定义一个 Circle 结构体,包含 radius 存储属性和一个计算属性 area(返回面积,πr²)。创建半径为 5 的 circle 并打印面积。
  2. 进阶题: 创建一个 BankCard 类,包含 balance 存储属性并添加 didSet 观察器——余额低于 0 时打印警告,低于 10 时打印低余额提醒。实现 withdraw(_:)deposit(_:) 方法。
  3. 挑战题: 实现一个 Logger 类,使用 static 类型属性存储日志级别和日志历史数组。提供一个 static 方法 log(_:level:) 向历史数组追加日志条目,并提供 static 计算属性 formattedLog 返回格式化的日志文本。使用 didSet 在新增日志时自动检查数量限制。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏