Swift 结构体
结构体(struct)是 Swift 中最常用的自定义类型,用来把一组相关的数据打包在一起。
它和类长得很像,但本质区别在于结构体是值类型,类是引用类型。
本文先讲结构体的定义、实例化与成员逐一初始化器,再重点澄清「结构体的值不可修改」这个流传很广的误解,最后用一张表把结构体和类的区别一次讲清。
定义与实例化
用 struct 关键字定义结构体,属性写在花括号里,然后调用它的构造器创建实例。
实例
struct RunoobSite {
var name: String // 网站名称
var url: String // 网站地址
var year: Int // 成立年份
}
// 用自动生成的构造器创建实例
var site = RunoobSite(name: "Runoob", url: "www.runoob.com", year: 2014)
print(site.name)
print(site.url)
// 用点语法访问和修改属性
site.year = 2026
print(site.year)
Runoob www.runoob.com 2026
结构体名用大驼峰,属性名用小驼峰,这是 Swift 社区的通行写法。
成员逐一初始化器
只要结构体没有自己写构造器,Swift 就会自动生成一个成员逐一初始化器(memberwise initializer),参数顺序与属性声明顺序一致。
如果某个属性有默认值,那么对应的参数在调用时就可以省略。
实例
var x: Int
var y: Int
}
// 成员逐一初始化器:参数名就是属性名
let p = RunoobPoint(x: 3, y: 4)
print(p.x, p.y)
struct RunoobCourse {
var title: String
var lessons: Int = 43 // 有默认值,调用时可以省略
}
let course = RunoobCourse(title: "Swift 教程")
print(course.title, course.lessons)
// 所有属性都有默认值时,连参数都不用写
struct RunoobDefaults {
var name = "RUNOOB"
var visits = 0
}
print(RunoobDefaults().name)
3 4 Swift 教程 43 RUNOOB
注意:如果在结构体内部手写了构造器,自动生成的成员逐一初始化器就会消失。把自定义构造器写在扩展 extension 里,则可以同时保留两者。
实例
var width: Int
var height: Int
}
// 构造器写在扩展里,成员逐一初始化器仍然可用
extension RunoobSize {
init(side: Int) {
self.init(width: side, height: side)
}
}
print(RunoobSize(width: 10, height: 20).width) // 用成员逐一初始化器
print(RunoobSize(side: 5).height) // 用扩展里的构造器
10 5
结构体的属性可以修改
很多旧资料写着「结构体是按值传递的,所以它的值不可修改」,这个说法是错的。
结构体实例能否修改只取决于它用什么声明:var 声明的可以改,let 声明的才不能改。
实例
var name: String
var year: Int
}
// var 声明的结构体实例,属性可以随意修改
var site = RunoobSite(name: "Runoob", year: 2014)
site.year = 2026
print(site.year)
// let 声明的结构体实例,所有属性都不能改
let fixed = RunoobSite(name: "RUNOOB", year: 2014)
// fixed.year = 2026 // 编译错误:cannot assign to property
print(fixed.year)
2026 2014
对 let 声明的结构体实例,连调用修改自身的方法(mutating 方法)也不允许。
注意:值类型与可变性是两回事。值类型的含义是「每个变量持有自己的副本」,不是「不能修改」。方法相关的 mutating 用法可以看第 18 篇《Swift 方法》。
值语义
把结构体实例赋值给另一个变量,或者作为参数传给函数,发生的都是复制,两个变量各持有一份独立的数据。
实例
var x: Int
var y: Int
}
var p1 = RunoobPoint(x: 1, y: 2)
var p2 = p1 // 复制出一份独立的副本
p2.x = 100 // 只修改 p2
print(p1.x, p2.x) // p1 完全不受影响
1 100
这种复制是「深」的:如果结构体里嵌套了另一个结构体,修改副本的内层属性同样不会影响原实例。
实例
var x: Int
var y: Int
}
struct RunoobTeam {
var leader: RunoobPoint // 嵌套了一个结构体
}
var t1 = RunoobTeam(leader: RunoobPoint(x: 1, y: 1))
var t2 = t1
t2.leader.x = 99 // 修改副本里嵌套的值
print(t1.leader.x, t2.leader.x)
1 99
值语义带来的好处是行为可预测:函数拿到一个结构体参数,无论怎么改都不会污染调用方的数据。
代价是数据量很大时复制会有开销,Swift 对数组、字典这类标准库类型用写时复制(COW)来优化,细节可以看第 32 篇《Swift 值语义与写时复制》。
结构体与类的对比
结构体和类在语法上高度相似,都能定义属性、方法、下标、构造器,都能遵循协议,但底层模型完全不同。
下表把两者的关键差异集中列出来,第 16 篇《Swift 类》只讲类的特有部分,不再重复这张表。
| 对比项 | 结构体 struct | 类 class |
|---|---|---|
| 类型本质 | 值类型 | 引用类型 |
| 赋值与传参 | 复制出一份独立的副本 | 复制的是引用,多个变量指向同一个实例 |
| 内存管理 | 不涉及引用计数 | 由 ARC 管理,存在引用计数开销 |
| 继承 | 不支持继承 | 支持继承与重写 |
| 构造器 | 自动获得成员逐一初始化器 | 必须自己写 init 或给所有属性默认值 |
| 析构 | 没有 deinit | 有 deinit,实例销毁前执行 |
| 恒等比较 | 用 == 比较内容,需遵循 Equatable | 用 === 判断是不是同一个实例 |
| 可变性 | var 实例可改,let 实例完全不可改 | let 引用仍可修改它指向实例的 var 属性 |
| 适用场景 | 数据模型、坐标、配置等小型数据 | 需要共享状态、继承或多态时 |
官方给出的默认建议是:优先用结构体,只有确实需要引用语义、继承或生命周期管理时才改用类。
常见问题
下面是结构体使用中最常见的几个疑问。
结构体能不能有方法
可以。结构体可以定义实例方法、类型方法,也可以遵循协议、实现下标,功能上和类几乎一样,只是不能继承。
方法内部要修改属性时,需要给方法加上 mutating 关键字。
为什么改了副本,原结构体也变了
如果结构体里存的是引用类型(比如类实例),复制出来的副本里存的是同一个引用,改引用指向的对象时两边都会看到变化。
这是值类型嵌套引用类型的典型陷阱,判断时要看具体属性到底是值类型还是引用类型。
结构体能不能比较相等
让结构体遵循 Equatable 协议后,就能用 == 比较。所有存储属性都遵循 Equatable 时,编译器可以自动合成实现,不需要手写。
类也可以用 ==,但那比较的是内容;要判断是不是同一个实例,必须用 ===。
