Swift 语言基础与现代语法

本文面向有编程基础但初学 Swift 的 iOS 开发者,系统梳理 Swift 5.9/6 的语言基础与现代语法。内容涵盖值类型与引用类型的语义差异、struct/enum/class 的选型决策、Optional 与可选链入门、闭包捕获语义与逃逸规则、属性包装器与 Result builder 的实现原理,以及值语义背后的写时复制机制。同时介绍 if/switch 表达式、宏 macros 等新特性,并给出符合 Swift API Design Guidelines 的命名建议。读完可建立清晰的选型与设计直觉,写出更安全、更易维护的 Swift 代码。

开篇

Swift 诞生于 2014 年的 WWDC,目标是替代 Objective-C 成为 Apple 全平台的首选语言。十多年过去,Swift 已经演进到 6.0(Xcode 16 内置),语言形态与最初相比发生了巨大变化:从早期的 AnyObject 泛滥、@objc 依赖,走向了以值类型、协议、泛型为核心的现代风格。

对从 Java、C#、TypeScript 或 Objective-C 转过来的开发者而言,Swift 的学习曲线并不陡峭,但有几个地方特别容易“写得出代码、却写不对代码”:

  • 把 struct 当 class 用,无脑复制引用语义的思维;
  • 以为 struct 一定没有堆分配,从而在循环里反复拷贝大数组;
  • 闭包里 self 捕获语义理解不清,导致循环引用或数据竞争;
  • 面对 Optional 只会 ! 强制解包,把崩溃留到线上。

本文不追求语法手册式的穷举,而是聚焦“为什么这样设计”与“什么时候该用哪个”。如果你已经能写出能跑的 Swift,这篇文章帮你把直觉校正到正确的方向。后续可以继续阅读 Swift 可选类型与错误处理 与 Swift 协议与泛型编程 ,这三篇构成语言基础的主干。

一、值类型与引用类型

1.1 两种语义的本质区别

Swift 中的类型按赋值行为分为两类:

  • 值类型(value type):struct、enum、tuple,以及标准库中的 Array、Dictionary、Set、String。赋值或传参时产生一份逻辑上独立的副本。
  • 引用类型(reference type):class、actor、闭包。赋值时复制的是指向同一块堆内存的指针,多个变量共享同一实例。
struct Point {
    var x: Int
    var y: Int
}

final class Counter {
    var value: Int = 0
}

var p1 = Point(x: 0, y: 0)
var p2 = p1          // 拷贝
p2.x = 10
print(p1.x)          // 0 —— p1 不受影响

let c1 = Counter()
let c2 = c1          // 共享同一实例
c2.value = 99
print(c1.value)      // 99 —— 两个变量指向同一对象

这个区别看似基础,但它决定了并发安全、缓存行为、API 副作用等一连串设计问题。Swift 6 的严格并发检查(strict concurrency)之所以能把大量数据竞争在编译期拦下来,根基就在于值类型默认是 Sendable 的,而 class 不是。

1.2 写时复制(Copy on Write)

一个常见的误解是“值类型每次赋值都会深拷贝,所以很慢”。实际上标准库的集合类型采用写时复制:赋值时只增加引用计数,只有当其中一份被修改、且引用计数大于 1 时才真正复制底层缓冲区。

var a = Array(0..<1_000_000)   // 底层 buffer 引用计数 = 1
var b = a                       // 只 +1 引用计数,没有复制 8MB 数据
b.append(999_999)               // 此时才真正复制 buffer
print(a.count)                  // 1000000 —— a 不受影响

写时复制是标准库为集合类型手动实现的优化,不是你写 struct 就自动拥有的。自定义 struct 若持有 class 引用或大块堆内存,需要自己用 isKnownUniquelyReferenced(_:) 实现 COW,否则会退化成“共享引用”或“全量深拷贝”。

维度值类型(struct/enum)引用类型(class)
赋值语义逻辑副本共享实例
默认存储位置可能栈、也可能堆(取决于逃逸)堆
继承不支持支持
析构无 deinit有 deinit
恒等比较只能比较内容(==)可比较引用(===)
并发安全天然 Sendable(成员都 Sendable 时)需 @unchecked 或加锁/actor
适合场景数据模型、配置、枚举状态有身份与生命周期的实体、共享可变状态

1.3 什么时候值类型反而更慢

值类型并非万能。当结构体体积很大(比如包含 20 个字段)且频繁在函数间传递时,逐字段拷贝的开销会超过一次指针传递。此时有两种应对:把大结构体装箱成 class,或改用 inout 参数避免拷贝。

// 避免:大结构体按值传参
func process(_ config: HugeConfig) { /* 每次都拷贝 */ }

// 更好:inout 传递,语义上仍是一次修改
func process(_ config: inout HugeConfig) { /* 原地修改 */ }

1.4 let 与 var 的语义边界

Swift 的 let 对值类型与引用类型含义不同:对 struct,let 冻结了所有属性,任何修改都编译报错;对 class,let 只冻结引用本身,实例内部的属性仍可改。

struct Config { var timeout: Int }
final class Session { var token: String = "" }

let config = Config(timeout: 30)
// config.timeout = 60        // 编译错误:config 是 let

let session = Session()
session.token = "abc"          // 合法:引用不变,内容可变

这解释了为什么 struct 的方法要修改自身属性时必须标 mutating——因为方法可能是通过 let 实例调用的,编译器要区分“这个方法会不会改 self”。在协议里声明 mutating func 也会限制它只能被值类型遵循,这是协议设计时常被忽略的约束。

二、struct、enum、class 的选型

2.1 选型决策表

问题选 struct选 enum选 class
需要继承或共享可变状态是
需要表达“有限个互斥状态”是
需要值语义(拷贝即独立)是是
需要 deinit 做资源清理是
需要与 Objective-C 互操作常需
需要被 === 比较身份是

经验法则:默认用 struct,需要有限状态集时用 enum,只有在确实需要引用语义、继承或生命周期管理时才用 class。SwiftUI 的视图、SwiftData 的模型、Codable 的 DTO,绝大多数都应该是 struct。

2.2 enum 被低估的表达力

Swift 的 enum 是带关联值的代数数据类型,能表达“状态机”这类问题而不需要一堆可空字段:

enum LoadState {
    case idle
    case loading(progress: Double)
    case success(data: [String])
    case failure(Error)
}

func render(_ state: LoadState) {
    switch state {
    case .idle:
        print("等待开始")
    case .loading(let progress):
        print("加载中 \(Int(progress * 100))%")
    case .success(let data):
        print("成功,\(data.count) 条")
    case .failure(let error):
        print("失败:\(error)")
    }
}

switch 对 enum 的穷尽性检查是编译期强制的:新增一个 case 时,所有未处理的 switch 都会报错。这比“用几个 Bool 字段表示状态”安全得多——后者很容易出现 isLoading == true && hasError == true 这种非法组合。

2.3 常见误用

  • 用 class 定义纯数据模型,然后为了“比较相等”手写一大堆 ==;
  • 用 struct 表达需要共享的可变状态(比如全局缓存),结果各处的修改互不可见;
  • 在 struct 里塞一个 class 属性,误以为整个类型都是值语义——实际上外层是值语义、内层仍是引用语义,容易踩坑。

三、Optional 与可选链入门

Optional 是 Swift 类型系统最重要的安全机制之一,本质是一个 enum:

enum Optional<Wrapped> {
    case none
    case some(Wrapped)
}

String? 只是 Optional<String> 的语法糖。可选链(optional chaining)让“逐层访问可能为 nil 的属性”变得简洁:

struct Address { var city: String }
struct User { var name: String; var address: Address? }

let user = User(name: "Leeting", address: nil)
let city = user.address?.city          // String?
let upper = user.address?.city.uppercased()  // 链上任一环为 nil,整体返回 nil
let fallback = user.address?.city ?? "未知"  // 用 ?? 提供默认值
print(fallback)  // 未知

要点:可选链的返回值永远是 Optional,即使最内层属性是非可选的 String,user.address?.city 的类型也是 String?。可选链还支持方法调用与下标,且短路特性保证后续表达式不会被执行。

如果你想把 Optional 用得更扎实(可选绑定、guard let、Result、typed throws),详见 Swift 可选类型与错误处理 。

四、闭包与捕获

4.1 捕获语义

闭包会捕获其引用的外部变量。捕获的是变量的引用(box),而不是快照:

var counter = 0
let increment = { counter += 1 }
increment()
increment()
print(counter)  // 2 —— 闭包持有 counter 的引用

如果要捕获当前值的快照,用捕获列表:

var value = 10
let snapshot = { [value] in print(value) }
value = 20
snapshot()  // 10 —— 捕获的是当时的值

捕获列表还能用来打破循环引用,通过 [weak self] 或 [unowned self]:

final class ViewModel {
    var onUpdate: (() -> Void)?

    func bind() {
        onUpdate = { [weak self] in
            guard let self else { return }
            print(self.description)
        }
    }

    var description: String { "ViewModel" }
}

[weak self] 会产生 Optional,必须解包;[unowned self] 不产生 Optional,但如果对象已释放会崩溃。默认选 weak,只有在能严格证明生命周期覆盖时用 unowned。

4.2 逃逸与非逃逸闭包

特性非逃逸 (()->Void)逃逸 @escaping (()->Void)
默认性是需显式标注
生命周期函数返回前结束可超出函数作用域
是否可存为属性否是
捕获 self 是否需 weak通常不需要通常需要
性能编译器可优化,无堆分配通常需堆分配

非逃逸闭包是默认值,编译器可以做更激进的优化(栈上分配、内联)。只有把闭包存起来、或传给异步 API 时才需要 @escaping。

五、属性包装器

属性包装器(property wrapper)把“读写属性时附带的逻辑”抽成一个可复用类型:

@propertyWrapper
struct Clamped<Value: Comparable> {
    var wrappedValue: Value {
        didSet { wrappedValue = min(max(wrappedValue, range.lowerBound), range.upperBound) }
    }
    private let range: ClosedRange<Value>

    init(wrappedValue: Value, _ range: ClosedRange<Value>) {
        self.range = range
        self.wrappedValue = min(max(wrappedValue, range.lowerBound), range.upperBound)
    }
}

struct Player {
    @Clamped(0...100) var volume: Int = 50
}

var player = Player()
player.volume = 150
print(player.volume)  // 100 —— 被自动钳制

标准库与 SwiftUI 里大量使用属性包装器:@State、@Binding、@Published、@Environment、@AppStorage 都是属性包装器。理解其展开形式(编译器把 @Clamped var volume 展开为一个 _volume: Clamped<Int> 存储属性加一个计算属性)之后,调试 SwiftUI 的状态问题会轻松很多。

六、Result builder 简介

Result builder(旧称 function builder)是让“声明式 DSL”成为可能的编译期转换机制,SwiftUI 的 body、SwiftData 的查询、以及社区里的 HTML/SQL DSL 都建立在它之上:

@resultBuilder
struct StringBuilder {
    static func buildBlock(_ parts: String...) -> String {
        parts.joined(separator: "\n")
    }
    static func buildOptional(_ part: String?) -> String {
        part ?? ""
    }
    static func buildEither(first: String) -> String { first }
    static func buildEither(second: String) -> String { second }
}

@StringBuilder
func makeReport(includeHeader: Bool) -> String {
    "标题"
    if includeHeader {
        "副标题"
    }
    "正文"
}

print(makeReport(includeHeader: true))

关键点是 buildBlock、buildOptional、buildEither 这些静态方法把一串语句改写成对它们的嵌套调用。SwiftUI 中“为什么 if 能出现在 body 里、而 for 需要 ForEach”的答案就在这里:buildArray 只支持同构元素,而 if/else 由 buildEither 支持。

七、Swift 5.9 与 Swift 6 的语言特性

7.1 if 与 switch 表达式

从 Swift 5.9 起,if 和 switch 可以作为表达式直接产生值,省掉临时变量:

let score = 82
let grade = if score >= 90 { "A" } else if score >= 80 { "B" } else { "C" }

let icon = switch grade {
case "A": "star.fill"
case "B": "star.leadinghalf.filled"
default: "star"
}

注意每个分支必须是单个表达式,且类型需一致。它适合替代“只为算一个值而写的三元表达式套娃”。

7.2 宏 macros

Swift 5.9 引入的宏系统让编译期代码生成成为语言的一等公民。宏分为两类:freestanding(独立调用,如 #Preview、#Predicate)与 attached(附着在声明上,如 @Observable)。

import Observation

@Observable
final class CartModel {
    var items: [String] = []
    var total: Double = 0
}

#Preview {
    let model = CartModel()
    model.items = ["键盘"]
    return Text("\(model.items.count)")
}

@Observable 是 attached 宏,编译器会为每个存储属性生成观察代码;#Preview 是 freestanding 宏,展开成一段预览注册代码。宏与属性包装器的区别在于:属性包装器是运行时的包装类型,宏是编译期展开的语法树改写,因此没有额外运行时开销,但调试时需要展开宏(Xcode 的 Expand Macro 命令)来看真实代码。

Swift 6 的语言模式(language mode)默认开启严格并发检查,把数据竞争从运行时问题变成编译期错误。这是升级 Xcode 16 时最容易“炸出一片红”的地方,迁移建议逐 target 开启、分批修复。

八、命名与 API 设计规范

Apple 的 Swift API Design Guidelines 是团队协作的底线,几条最常用的:

  • 方法名读起来像英语句子:array.remove(at: 3) 而非 array.removeIndex(3);
  • 布尔属性用 is/has/can 前缀:isEnabled、hasChanges;
  • 副作用的方法用动词命令式(sort()),无副作用的返回新值用 -ed/-ing(sorted());
  • 参数标签优先用介词短语补全语义:move(from:to:)、insert(_:at:);
  • 避免缩写:ViewController 而非 VC,configuration 而非 config(局部变量可放宽)。

命名是“零成本的可维护性投资”,Code Review 里最该被挑剔的就是命名。

常见坑清单

  • 在 struct 的 mutating 方法里调用 self 的闭包属性,导致“逃逸闭包捕获可变 self”的编译错误;
  • 用 class 做模型,然后在 SwiftUI 的 @State 里持有它,发现界面不刷新(因为引用没变);
  • 忘记 Array 的 COW 特性,在热路径里对同一数组做大量写操作却不自知复制发生;
  • 闭包中隐式捕获 self 却不加捕获列表,在长生命周期回调里造成内存泄漏;
  • 属性包装器的 init 里在 wrappedValue 完成初始化之前就读取它,导致编译错误或读到默认值;
  • 以为 enum 不能有方法/计算属性,把逻辑散落到外部 switch;
  • 在 Swift 6 语言模式下把非 Sendable 的 class 跨 actor 传递,编译报错后才回头重构;
  • 用 ! 强制解包处理“理论上不会 nil”的场景,结果线上崩溃率上升。

FAQ

Q:struct 一定在栈上分配吗?

不一定。Swift 的编译器会在能证明生命周期不逃逸时把 struct 放在栈上,但一旦被闭包捕获、被 Any 装箱、或作为 class 的属性,就会分配在堆上。不要用“栈 vs 堆”作为选型依据,用“语义”来选。

Q:Array 是值类型,那传参时会不会很慢?

不会。标准库为集合实现了写时复制,只读传参只是引用计数 +1。真正产生复制的时机是“引用计数大于 1 时发生写操作”。

Q:什么时候必须用 class?

需要继承(如自定义 UIView 子类)、需要 deinit 管理资源(如持有文件句柄、定时器)、需要引用身份语义(如共享的 URLSession 配置)、或需要与 Objective-C 运行时互操作时。

Q:@escaping 是不是加了就更慢?

它本身不改变闭包体,但逃逸闭包通常需要堆分配和引用计数,无法像非逃逸闭包那样栈分配或内联。所以能用非逃逸就别加 @escaping。

相关阅读

小结

Swift 的语言设计围绕三个核心展开:值语义优先、Optional 显式化、编译期安全。把 struct 作为默认选择、把 enum 当作状态建模的利器、把 Optional 当作必须处理的类型而非障碍、把闭包捕获列表当作常规习惯,你就已经跨过了 Swift 新手期最大的几个坑。属性包装器与 Result builder 是理解 SwiftUI 的前置知识,宏与严格并发则是 Swift 6 时代的必备技能。建议在真实项目里刻意练习“先问该用值类型还是引用类型”,两三周后选型直觉就会稳定下来。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「iOS 开发」更多文章

  1. Swift Package Manager 与模块化拆分
  2. Core Animation 与 SwiftUI 动画
  3. iOS 安全:Keychain、生物识别与传输安全