Swift: Swift 类与结构体:值类型与引用类型的选择指南
类和结构体是 Swift 中构建数据模型的两大核心类型。它们有很多相似之处,但在内存管理和传值方式上有本质区别。本课帮你理清两者的选择策略。
1. 你将学到
- 定义类和结构体的基本语法
- 值类型(struct)与引用类型(class)的核心区别
mutating关键字的作用- 类和结构体的初始化规则
- 何时用类、何时用结构体的选择策略
2. 一个 iOS 开发者的真实故事
(1) 痛点:用户账户改了 A 处,B 处也变了
Alice 在建模用户账户时选择了 class:
SWIFT
class UserAccount {
var name: String
var balance: Double
init(name: String, balance: Double) {
self.name = name
self.balance = balance
}
}
let account1 = UserAccount(name: "Alice", balance: 1000)
let account2 = account1 // 引用同一个实例
account2.name = "Bob"
print(account1.name) // "Bob" —— account1 也被改了!
Alice 只是想创建一个新用户,结果意外修改了原数据。引用类型的副作用在不知情的情况下传播到了整个代码库。
(2) 结构体作为值类型的解法
SWIFT
struct UserAccount {
var name: String
var balance: Double
}
var account1 = UserAccount(name: "Alice", balance: 1000)
var account2 = account1 // 复制一份独立的数据
account2.name = "Bob"
print(account1.name) // "Alice" —— account1 不受影响
结构体赋值时进行值拷贝,每个实例互不影响。
(3) 收益:可预测的数据行为
| 维度 | class(引用类型) | struct(值类型) |
|---|---|---|
| 赋值行为 | 共享同一实例 | 独立拷贝 |
| 意外修改风险 | 高(别名效应) | 无 |
| 内存分配 | 堆分配 | 栈分配(更高效) |
| 线程安全 | 需手动同步 | 天然隔离 |
| 适用场景 | 需共享状态 | 数据模型、值语义 |
3. 定义类与结构体
(1) 基本语法对比
graph TB
subgraph class
A["class Person {"]
B[" var name: String"]
C[" var age: Int"]
D[" func greet() { }"]
E["}"]
end
subgraph struct
F["struct Person {"]
G[" var name: String"]
H[" var age: Int"]
I[" func greet() { }"]
J["}"]
end
| 功能 | class | struct | 说明 |
|---|---|---|---|
| 属性 | ✅ | ✅ | 存储属性和计算属性 |
| 方法 | ✅ | ✅ | 实例方法和类型方法 |
| 初始化器 | ✅ | ✅ | class 不自动生成成员初始化器 |
| 继承 | ✅ | ❌ | struct 不能继承 |
| 析构器 | ✅ | ❌ | struct 没有 deinit |
| 引用计数 | ✅ | ❌ | struct 不需要 ARC |
| 类型转换 | ✅ | ❌ | as?/as! 仅用于 class |
▶ 示例:类与结构体的定义
SWIFT
// ============================================
// 类和结构体的定义对比
// ============================================
// 类定义——需要手动写 init
class PersonClass {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
func introduce() {
print("Hi, I'm \(name), \(age) years old")
}
}
// 结构体定义——自动获得成员初始化器
struct PersonStruct {
var name: String
var age: Int
func introduce() {
print("Hi, I'm \(name), \(age) years old")
}
}
let p1 = PersonClass(name: "Alice", age: 28)
p1.introduce()
let p2 = PersonStruct(name: "Bob", age: 22) // 自动生成 init
p2.introduce()
输出:
TEXT 📖 仅展示Hi, I'm Alice, 28 years old Hi, I'm Bob, 22 years old
4. 值类型 vs 引用类型
(1) 核心区别
graph LR
subgraph "值类型(struct)"
A["var a = Point(x: 1, y: 2)"]
B["var b = a"]
C["b.x = 5"]
D["a.x → 1(独立)"]
A --> B --> C --> D
end
subgraph "引用类型(class)"
E["var a = Point(x: 1, y: 2)"]
F["var b = a"]
G["b.x = 5"]
H["a.x → 5(共享)"]
E --> F --> G --> H
end
| 维度 | 值类型(struct) | 引用类型(class) |
|---|---|---|
| 赋值语义 | 拷贝内容 | 拷贝引用 |
| 相等判断 | == 比较值 |
=== 比较身份 |
| 存储位置 | 栈(更快) | 堆(需 ARC 管理) |
| let 常量 | 属性不可变 | 引用不可变但属性可变 |
| 函数传参 | 默认拷贝 | 默认共享引用 |
▶ 示例:赋值行为对比
SWIFT
// ============================================
// 值类型与引用类型的赋值行为
// ============================================
// 结构体——值类型
struct Position {
var x: Int
var y: Int
}
var posA = Position(x: 10, y: 20)
var posB = posA
posB.x = 99
print("posA: (\(posA.x), \(posA.y))") // 不变
print("posB: (\(posB.x), \(posB.y))") // 只影响 posB
// 类——引用类型
class Location {
var x: Int
var y: Int
init(x: Int, y: Int) { self.x = x; self.y = y }
}
var locA = Location(x: 10, y: 20)
var locB = locA
locB.x = 99
print("locA: (\(locA.x), \(locA.y))") // 也跟着变了!
print("locB: (\(locB.x), \(locB.y))")
输出:
TEXT 📖 仅展示posA: (10, 20) posB: (99, 20) locA: (99, 20) locB: (99, 20)
(2) 恒等运算符 ===
引用类型可以使用 === 判断两个变量是否指向同一个实例:
SWIFT
let loc1 = Location(x: 5, y: 10)
let loc2 = loc1
let loc3 = Location(x: 5, y: 10) // 不同实例
print(loc1 === loc2) // true — 同一实例
print(loc1 === loc3) // false — 不同实例
💡 提示:
=== 是 identity operator(恒等运算符),不同于 ==(相等运算符)。值类型只能使用 ==(需实现 Equatable),不能使用 ===。
5. mutating 方法与选择策略
(1) mutating 关键字
结构体的实例方法默认不能修改属性。如果需要修改,必须标记为 mutating:
| 方法类型 | class | struct(非 mutating) | struct(mutating) |
|---|---|---|---|
| 修改属性 | ✅ 允许 | ❌ 编译错误 | ✅ 允许 |
| 修改 self | ✅ 允许 | ❌ 不允许 | ✅ 允许 |
| 调用前提 | 无限制 | let 常量也可调用 | 变量(var)才能调用 |
▶ 示例:mutating 方法
SWIFT
// ============================================
// struct 的 mutating 方法
// ============================================
struct Counter {
var count = 0
// 非 mutating——只读方法
func display() {
print("Count: \(count)")
}
// mutating——修改属性
mutating func increment() {
count += 1
}
// mutating——替换整个实例
mutating func reset() {
self = Counter() // 替换 self
}
}
var counter = Counter() // 必须是 var 才能调用 mutating
counter.increment()
counter.increment()
counter.display() // Count: 2
counter.reset()
counter.display() // Count: 0
输出:
TEXT 📖 仅展示Count: 2 Count: 0
(2) 选择策略
| 场景 | 推荐类型 | 原因 |
|---|---|---|
| 数据模型(Point、Size、User) | struct | 值语义,安全可预测 |
| 需要继承(UIView 子类、BaseViewController) | class | 继承是 class 独有的 |
| 需共享可变状态 | class | 引用类型自动共享 |
| 需要 ObjC 互操作 | class | @objc 需要 class |
| 轻量封装(包装少量值) | struct | 栈分配,零开销 |
| 需要析构(deinit)清理资源 | class | struct 没有 deinit |
▶ 示例:结构体作为模型的首选
SWIFT
// ============================================
// 为什么现代 Swift 偏向用 struct 做模型
// ============================================
struct User {
let id: Int
var name: String
var email: String
var score: Int
mutating func addScore(_ points: Int) {
score += points
}
}
var user = User(id: 1, name: "Alice", email: "alice@example.com", score: 100)
user.addScore(50)
// 拷贝修改模式——安全地创建变化版本
let admin = User(id: 2, name: "Admin", email: "admin@example.com", score: 0)
// admin.addScore(10) // 编译错误!let 常量不能调用 mutating
var editor = admin
editor.name = "Editor" // 独立修改,不影响 admin
输出:
TEXT 📖 仅展示(无控制台输出,演示值语义——editor.name = "Editor" 不影响 admin.name)
6. 完整示例:银行账户系统
SWIFT
// ============================================
// 完整示例:银行账户系统
// 功能:class 做账户管理,struct 做交易记录
// ============================================
import Foundation
// 1. 交易记录——值类型结构体
struct Transaction {
let id: Int
let amount: Double
let type: String // "deposit" 或 "withdraw"
let date: Date
func description() -> String {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
return "[\(formatter.string(from: date))] \(type): $\(amount)"
}
}
// 2. 账户——引用类型类(共享状态)
class BankAccount {
let accountNumber: String
var balance: Double
private var transactions: [Transaction] = []
private var nextId = 1
init(accountNumber: String, initialBalance: Double = 0) {
self.accountNumber = accountNumber
self.balance = initialBalance
}
func deposit(amount: Double) {
guard amount > 0 else { return }
balance += amount
let tx = Transaction(id: nextId, amount: amount, type: "deposit", date: Date())
transactions.append(tx)
nextId += 1
}
func withdraw(amount: Double) -> Bool {
guard amount > 0 && amount <= balance else { return false }
balance -= amount
let tx = Transaction(id: nextId, amount: amount, type: "withdraw", date: Date())
transactions.append(tx)
nextId += 1
return true
}
func printStatement() {
print("=== Account \(accountNumber) ===")
print("Balance: $\(balance)")
print("Transactions:")
for tx in transactions {
print(" \(tx.description())")
}
}
}
// 3. 使用
let account = BankAccount(accountNumber: "ACC-001", initialBalance: 1000)
account.deposit(amount: 500)
account.withdraw(amount: 200)
account.deposit(amount: 100)
account.printStatement()
输出:
TEXT 📖 仅展示=== Account ACC-001 === Balance: $1400.0 Transactions: [2026-07-30] deposit: $500.0 [2026-07-30] withdraw: $200.0 [2026-07-30] deposit: $100.0
❓ 常见问题
Q 为什么 Apple 推荐优先用 struct?
A 值类型更安全、更可预测。没有引用共享的副作用,也没有循环引用和引用计数的开销。Swift 标准库中的 String、Array、Dictionary 都是 struct。
Q struct 有性能优势吗?
A 是的。struct 分配在栈上,不需要引用计数管理,没有堆分配的开销。但大型 struct 在函数间传递时的拷贝成本可能高于引用类型的传引用成本。Swift 的 Copy-on-Write 机制优化了这个问题。
Q class 的 init 必须手动写吗?
A 对于 class,如果所有属性都有默认值,且没有定义任何初始化器,Swift 会生成一个默认无参 init。但不会像 struct 那样自动生成逐一成员初始化器(memberwise init)。
Q struct 可以有懒加载属性吗?
A 可以。
lazy var 在 struct 中也能工作,但因为 struct 不可变时不能访问 lazy 属性,实际使用限制较多。在 class 中使用 lazy 更自然。Q mutating 方法在 class 中能用吗?
A 不能。
mutating 只能用于值类型(struct 和 enum)。class 的方法默认就可以修改属性,不需要 mutating 关键字。📖 小节
- 类和结构体都能定义属性、方法、下标和扩展
- 结构体是值类型,赋值时拷贝内容;类是引用类型,赋值时拷贝引用
- 结构体自动获得成员初始化器,类需要手动写 init
mutating关键字允许结构体方法修改自身属性- 使用
===判断引用类型是否指向同一实例 - 优先用 struct,需要继承或共享可变状态时用 class
📝 作业
- 基础题: 定义一个
Book结构体,包含title、author、pages属性和一个summary()方法。创建两个实例并赋值测试值类型拷贝特性。 - 进阶题: 定义一个
TemperatureSensor类,包含currentTemp属性和readings: [Double]历史记录数组。实现recordTemp(_:)方法和average()方法。创建实例,记录 5 个温度并打印平均值。 - 挑战题: 设计一个「团队协作系统」:用
Team类管理共享成员列表,用Member结构体表示个人资料。Team提供addMember(_:)和listMembers()方法。Member有mutating方法来更新角色。验证 class 的共享引用特性。