现在位置: 首页 > Swift 教程 > 正文

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),参数顺序与属性声明顺序一致。

如果某个属性有默认值,那么对应的参数在调用时就可以省略。

实例

struct RunoobPoint {
    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 里,则可以同时保留两者。

实例

struct RunoobSize {
    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 声明的才不能改。

实例

struct RunoobSite {
    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 方法》。


值语义

把结构体实例赋值给另一个变量,或者作为参数传给函数,发生的都是复制,两个变量各持有一份独立的数据。

实例

struct RunoobPoint {
    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

这种复制是「深」的:如果结构体里嵌套了另一个结构体,修改副本的内层属性同样不会影响原实例。

实例

struct RunoobPoint {
    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 时,编译器可以自动合成实现,不需要手写。

类也可以用 ==,但那比较的是内容;要判断是不是同一个实例,必须用 ===。