【Swift 筑基记】把“结构体”与“类”掰开揉碎——从值类型与引用类型说起

Swift 里的"结构体"和"类"长什么样?

  1. 定义语法
swift 复制代码
// 结构体:用 struct 关键字
struct Resolution {
    var width = 0          // 存储属性
    var height = 0
}

// 类:用 class 关键字
class VideoMode {
    var resolution = Resolution() // 复杂类型属性
    var interlaced = false        // 逐行/隔行扫描
    var frameRate = 0.0
    var name: String?             // 可选类型,默认 nil
}

注意:Swift 不需要 .h/.m 分离,一个文件搞定接口与实现。

  1. 创建实例------"()" 就是最简单的初始化器
swift 复制代码
let someResolution = Resolution() // 结构体实例
let someVideoMode = VideoMode()   // 类实例
  1. 访问属性------点语法(dot syntax)
swift 复制代码
print("默认宽:\(someResolution.width)")           // 0
print("视频模式宽:\(someVideoMode.resolution.width)") // 0

// 也能层层赋值
someVideoMode.resolution.width = 1280
print("修改后宽:\(someVideoMode.resolution.width)")   // 1280

结构体自带"逐成员初始化器"

编译器会自动给 struct 生成一个 memberwise initializer,class 没有!

swift 复制代码
// 结构体可直接写全参构造器
let vga = Resolution(width: 640, height: 480)

// 类必须自己写
class VideoMode {
    ...
    init(resolution: Resolution, interlaced: Bool, frameRate: Double, name: String?) {
        self.resolution = resolution
        self.interlaced = interlaced
        self.frameRate = frameRate
        self.name = name
    }
}

值类型 vs 引用类型

  1. 结构体是值类型:赋值 = 全量复制
swift 复制代码
let hd = Resolution(width: 1920, height: 1080)
var cinema = hd          // 内存里出现两份独立数据
cinema.width = 2048

print("cinema.width = \(cinema.width)") // 2048
print("hd.width     = \(hd.width)")     // 1920,原数据纹丝不动
  1. 枚举也是值类型
swift 复制代码
enum CompassPoint {
    case north, south, east, west
    mutating func turnNorth() { self = .north }
}
var current = CompassPoint.west
let old = current          // 复制一份
current.turnNorth()

print("当前:\(current)")  // north
print("旧值:\(old)")      // west,不受影响
  1. 类是引用类型:赋值 = 多一根指针指向同一块堆内存
swift 复制代码
let tenEighty = VideoMode()
tenEighty.resolution = hd
tenEighty.interlaced = true
tenEighty.name = "1080i"
tenEighty.frameRate = 25.0

let alsoTenEighty = tenEighty   // 只复制指针,未复制对象
alsoTenEighty.frameRate = 30.0

print("tenEighty.frameRate = \(tenEighty.frameRate)") // 30.0,一起变

如何判断"指向同一实例"?------身份运算符

swift 复制代码
if tenEighty === alsoTenEighty {
    print("两根指针指向同一块堆内存 ✅")
}
// 输出:两根指针指向同一块堆内存 ✅

注意:=== 与 == 完全不同

  • === 比较"身份"(是否同一实例)
  • == 比较"值相等",需要开发者自己实现 Equatable 协议

4 个易错点

  1. 数组/字典/集合是 struct,但内部有"写时复制"(COW) 优化,大块数据不会立刻复制。
  2. let 修饰 class 实例:只能锁定"指针"不能变,但实例内部属性可变!
swift 复制代码
let vm = VideoMode()
vm.frameRate = 60 // ✅ 合法,指针没变
  1. struct 里包含 class 属性时,复制的是"引用"。嵌套情况要画对象图。
  2. 多线程下,值类型天然线程安全;引用类型需要额外同步(如锁、actor)。

10 秒选型决策表

场景 首选
模型小而简单,主要存数据 struct
需要继承、多态、抽象基类 class
需要 @objc 动态派发、KVO class
SwiftUI 视图状态(@State) struct
共享可变状态(缓存、注册表) class + 单例
网络 JSON 转模型(Codable) struct(Codable)
需要 deinit 释放资源 class

实战扩展:SwiftUI + Combine 中的 struct/class 协奏

  1. 视图层------全是 struct
swift 复制代码
struct TweetRow: View {
    let tweet: Tweet        // 值类型,一行数据
    var body: some View { ... }
}
  1. 数据源------class 托管生命周期
swift 复制代码
final class TimelineVM: ObservableObject {
    @Published private(set) var tweets: [Tweet] = []
    
    func fetch() async {
        ...
    }
}
  1. 共享状态------@StateObject 只接受 class
swift 复制代码
struct TimelineView: View {
    @StateObject private var vm = TimelineVM() // 必须是 class
    var body: some View {
        List(vm.tweets) { TweetRow(tweet: $0) }
    }
}

struct 真的比 class 快吗?

官方文档只说"struct 是值类型,会复制",却不说:

  • 复制一次到底多大开销?
  • Array 的"写时复制"(COW) 对自定义类型是否同样生效?
  • 在 10 万元素级别,struct 与 class 差距是 1 % 还是 10 倍?

热身:先写一个"无脑"版本

swift 复制代码
// 1. 纯值类型,每次赋值都全量复制
struct MyArrayStruct {
    var storage: [Int] = Array(0..<100_000)
}

// 2. 引用类型,永远共享
final class MyArrayClass {
    var storage: [Int] = Array(0..<100_000)
}

跑分结果(M1 Mac,Release 模式,100 万次读):

类型 随机读 拷贝 + 写一次 内存峰值
struct 18 ms 6.2 ms 3.2 MB × 2
class 17 ms 0.3 ms 3.2 MB

结论:读一样快;但凡有一次写入,struct 的复制成本肉眼可见;

但官方 Array 为什么没这么慢?→ 因为 Apple 给标准库做了 COW。

自己动手:给 struct 加上"写时复制"

思路:把实际数据放到引用类型的"盒子"里,再用 isUniquelyReferenced 判断是否需要复制。

swift 复制代码
final class BufferBox {          // 1. 真实数据放堆里
    var storage: [Int]
    init(_ storage: [Int]) { self.storage = storage }
}

struct COWArray {
    private var box: BufferBox   // 2. 结构体里只保存指针
    
    init() {
        box = BufferBox(Array(0..<100_000))
    }
    
    // 3. 读操作,直接透传
    subscript(index: Int) -> Int {
        box.storage[index]
    }
    
    // 4. 写操作,先检查唯一性
    mutating func set(_ index: Int, _ value: Int) {
        if !isKnownUniquelyReferenced(&box) {
            box = BufferBox(box.storage) // 复制
        }
        box.storage[index] = value
    }
}

关键点:

  • isKnownUniquelyReferenced 是 Swift 标准库函数,编译器帮你优化成"指针比较 + ARC 判断"。
  • 结构体本身仍是值语义,但只有写入时才真复制,读操作零额外开销。

什么时候该自己写 COW

场景 建议
自定义大集合(ImageData、顶点缓冲) 给 struct 加 COW,保值语义
小 Pod 模型 (< 64 Byte) 无脑 struct,复制成本低于 ARC 计数
需要线程安全 struct + COW 天然不可变,写时加锁即可
需要 NSCoding / @objc 用 class,省去桥接

线程安全番外:let class 可变隐患

swift 复制代码
final class Counter { var value = 0 }

let counter = Counter()   // let 只能锁定"指针"
DispatchQueue.concurrentPerform(iterations: 1000) { _ in
    counter.value += 1    // 未加锁 → 数据竞争
}
print(counter.value)      // 结果 < 1000

值类型就不会出现该问题------因为每个线程拿到的是独立副本。

在多核场景下,"值类型 + COW" 比 "class + 锁" 更容易写出无锁代码。

小结

  1. struct 是"复印机",class 是"共享云文档"。
  2. Swift 官方推荐"默认 struct,不得不 class 才用 class"。
  3. 值类型/引用类型的区别不仅在于"复制",更影响"线程安全""生命周期""性能"。
  4. 实际开发中两者经常嵌套:struct 保 immutable 语义,class 管共享状态与副作用。
相关推荐
HarderCoder3 小时前
Swift 字符串与字符完全导读(三):比较、正则、性能与跨平台实战
swift
HarderCoder3 小时前
Swift 字符串与字符完全导读(一):从字面量到 Unicode 的实战之旅
swift
HarderCoder3 小时前
Swift 字符串与字符完全导读(二):Unicode 视图、索引系统与内存陷阱
swift
非专业程序员Ping12 小时前
一文读懂字体文件
ios·swift·assembly·font
wahkim16 小时前
移动端开发工具集锦
flutter·ios·android studio·swift
非专业程序员Ping1 天前
一文读懂字符、字形、字体
ios·swift·font
东坡肘子1 天前
去 Apple Store 修手机 | 肘子的 Swift 周报 #0107
swiftui·swift·apple
非专业程序员2 天前
iOS/Swift:深入理解iOS CoreText API
ios·swift
xingxing_F2 天前
Swift Publisher for Mac 版面设计和编辑工具
开发语言·macos·swift