write-swift
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWrite Swift
编写Swift代码
How to write Swift the way the language wants to be written, current through Swift 6.4.
Toolchain baseline: Swift 6.3 (current release as of August 2026). Everything here compiles on 6.3 unless marked ⚠, which flags unreleased Swift 6.4 features. Concurrency guidance assumes the Swift 6.2 model — if the project is on 6.1 or earlier, §3's rules about and do not apply.
async@concurrentThe through-line: Swift is a progressive-disclosure language. Start with the simplest, most static, most single-threaded thing that works, and buy dynamism — concurrency, reference semantics, existentials, unsafe pointers — only where you can point at the reason. Every rule below is an application of that.
Model this hierarchy of defaults. Move down a level only with a reason you can state:
| Need | Reach for | Move down only when |
|---|---|---|
| Data | | you need identity, sharing, or inheritance |
| Abstraction | concrete type | you have repeated code across types |
| Polymorphism | | you need heterogeneous storage → |
| Execution | main actor, synchronous | profiling shows a hang → |
| Memory | | profiling shows the cost → |
| Safety | safe API | C interop or a measured hot path → |
如何按照Swift语言的设计初衷编写代码,内容涵盖至Swift 6.4版本。
工具链基准版本:Swift 6.3(截至2026年8月的当前发布版本)。除非标记⚠(代表未发布的Swift 6.4特性),否则所有内容均可在6.3版本编译。并发指南基于Swift 6.2模型——如果项目使用6.1或更早版本,第3章中关于和的规则不适用。
async@concurrent核心原则:**Swift是一门渐进式披露语言。从最简单、最静态、最单线程的可行方案入手,仅在有明确理由时才引入动态特性——并发、引用语义、存在类型、不安全指针等。**以下所有规则均是这一原则的具体应用。
遵循以下默认层级,仅在能明确说明理由时才向下一层级迁移:
| 需求 | 首选方案 | 仅在以下情况才降级使用 |
|---|---|---|
| 数据建模 | | 需要标识、共享或继承特性时 |
| 抽象设计 | 具体类型 | 不同类型间存在重复代码时 |
| 多态实现 | | 需要异构存储时 → |
| 执行方式 | 主Actor、同步执行 | 性能分析显示卡顿 → |
| 内存管理 | | 性能分析显示内存开销过大 → |
| 安全性 | 安全API | 需要与C交互或针对热点路径优化时 → |
1. Model data with value types
1. 使用值类型建模数据
Value types are the default in Swift, not a special case.
- Default to and
struct. Useenumonly for identity, shared mutable state, inheritance, or resource lifetime. A window, a database connection, an entity stored in a rendering engine — those have identity. Aclass, aPoint, aDrinkdoes not.Material - by default;
letonly when you mutate. This is the same discipline asvarbeforesomeand value before reference: start narrow, widen with cause.any - A struct with a mutable reference-type property is neither a value nor a reference. Copies share the object; mutations leak across copies. Either keep the referenced type immutable, expose only computed properties that forward to it, or make it a stored property behind copy-on-write.
private - Copy-on-write is how you get out-of-line storage and value semantics. Wrap a final class in a struct and check before mutating; copy first if it isn't. This is exactly how
isKnownUniquelyReferenced(&storage),Array, andStringwork.Dictionary - Enums are the tool for "a fixed set of things" and for mutually exclusive state. Replacing a pile of optional stored properties (,
isSharing,selectedRows) with oneshareTargetmakes invalid combinations unrepresentable and makes state change atomic instead of a sequence of property writes you can forget to finish.enum State - Composing values yields a value. A struct whose stored properties are all value types has value semantics for free — which is what makes undo, diffing, and state restoration a single code path instead of one per property.
swift
struct Material { // value semantics preserved
var roughness: Double
private var _texture: Texture // a class
var color: Color {
get { _texture.color }
set {
if !isKnownUniquelyReferenced(&_texture) { _texture = Texture(copying: _texture) }
_texture.color = newValue
}
}
}Noncopyable types () express unique ownership: a file descriptor, a bank transfer, an open resource. Suppressing the copy turns "you must not run this twice" from an assertion into a compile error, and makes on a struct meaningful. Mark the finishing method so the compiler proves it's the last use. Parameter ownership becomes explicit: (read-only, the default), (takes it away), / (temporary write access).
~Copyabledeinitconsumingborrowingconsuminginoutmutating值类型是Swift的默认选择,而非特例。
- **默认使用和
struct。仅在需要标识、共享可变状态、继承或资源生命周期管理时使用enum。**窗口、数据库连接、渲染引擎中存储的实体等具有标识性;而class、Point、Drink等则没有。Material - **默认使用;仅在需要修改时使用
let。**这与优先使用var而非some、优先使用值类型而非引用类型的原则一致:从最严格的约束开始,仅在有必要时放宽。any - **包含可变引用类型属性的结构体既不是值类型也不是引用类型。**副本会共享对象,修改操作会在副本间泄露。要么保持引用类型不可变,要么仅暴露转发至该类型的计算属性,要么将其封装在结构体内部并实现写时复制(copy-on-write)。
- **写时复制是实现离线存储与值语义的方式。**将最终类(final class)封装在结构体中,修改前检查;如果不是唯一引用,则先复制。
isKnownUniquelyReferenced(&storage)、Array和String正是采用这种机制。Dictionary - **枚举是处理「固定集合」和互斥状态的工具。**用一个替代一堆可选存储属性(如
enum State、isSharing、selectedRows),可避免无效状态组合,同时让状态变更成为原子操作,而非容易遗漏的属性修改序列。shareTarget - **组合值类型仍会得到值类型。**所有存储属性均为值类型的结构体,天然具备值语义——这使得撤销、差异对比和状态恢复只需单一代码路径,而非为每个属性单独实现。
swift
struct Material { // 保留值语义
var roughness: Double
private var _texture: Texture // 一个类类型
var color: Color {
get { _texture.color }
set {
if !isKnownUniquelyReferenced(&_texture) { _texture = Texture(copying: _texture) }
_texture.color = newValue
}
}
}不可复制类型()表示唯一所有权:文件描述符、银行转账、打开的资源等。禁用复制可将「不得重复执行」从断言转为编译错误,同时让结构体的变得有意义。将完成方法标记为,以便编译器验证其为最后一次使用。参数所有权变得明确:(只读,默认)、(转移所有权)、/(临时写权限)。
~Copyabledeinitconsumingborrowingconsuminginoutmutating2. Errors and optionals — make the failure paths visible
2. 错误与可选值——让失败路径可见
Swift error handling rests on three points: sources of error are marked so they can't surprise you; errors carry enough context to act on; and recoverable errors are different from programmer mistakes.
- Recoverable → . Programmer mistake →
throw/precondition. A failed network call keeps the program running. An out-of-bounds index means the code is wrong and must halt before the bug becomes a security issue.fatalError - Enums with associated values make the best error types. beats
case duplicateFriend(String)— the context is the whole point.case duplicateFriend - for error conditions, because it forces the exit path.
guardfor the ordinary unwrap.if let - Typed throws () are for internal functions, error-forwarding generic code, and constrained environments where boxing
throws(MyError)is too costly. For public API, untypedany Errorpreserves your freedom to change the error type later. Note the unification:throwsisthrows, and non-throwing isthrows(any Error)— which is what letsthrows(Never)abstract over both.map - Force-unwrap only where you can state the invariant, and prefer a failing /
#requirewith a message over a bareprecondition.!
Swift错误处理基于三点:错误源会被标记,避免意外;错误携带足够上下文以便处理;可恢复错误与程序员错误是不同的。
- **可恢复错误 → 使用。程序员错误 → 使用
throw/precondition。**网络请求失败不会终止程序;数组越界意味着代码存在错误,必须在漏洞演变为安全问题前终止程序。fatalError - 带关联值的枚举是最佳错误类型。优于
case duplicateFriend(String)——上下文信息至关重要。case duplicateFriend - 使用处理错误条件,因为它强制退出当前路径。使用
guard处理普通的可选值解包。if let - 类型化抛出()适用于内部函数、转发错误的泛型代码和受限环境,此时装箱
throws(MyError)的开销过高。对于公共API,使用无类型any Error可保留后续修改错误类型的灵活性。注意统一规则:throws等价于throws,非抛出函数等价于throws(any Error)——这正是throws(Never)能同时处理两种情况的原因。map - 仅在能明确说明不变性时才强制解包,优先使用带消息的/
#require而非裸precondition。!
3. Concurrency: stay single-threaded until profiling says otherwise
3. 并发:除非性能分析要求,否则保持单线程
This is the section agents get wrong most often, because the model changed in Swift 6.2.
Start every app entirely on the main thread. Single-threaded code goes a long way, and most apps never need to leave it.
The progression, in order. Do not skip steps.
- Single-threaded on the main actor. No concurrency at all. Fine for most apps.
- /
asyncto hide latency (network, disk). Still no concurrency of your own — SDK APIs likeawaitoffload on your behalf.URLSession.data(from:) - to move your expensive work off the main thread — only after Instruments shows a hang.
@concurrent - to move state off the main actor — only when too much main-actor state is forcing tasks to hop back constantly.
actor
Turn on the right build settings first. Enable Approachable Concurrency in every project. For app modules and UI-facing modules, also set Default Actor Isolation to MainActor — it's the default for new app projects in Xcode 26, and it deletes most of your annotations. In a package: . Do not set main-actor-by-default for a general-purpose library — libraries should ship APIs and let clients decide where work runs.
@MainActorswiftSettings: [.defaultIsolation(MainActor.self)]nonisolated这是AI工具最容易出错的部分,因为Swift 6.2对并发模型进行了修改。
所有应用都从主线程开始。单线程代码的适用范围很广,大多数应用永远不需要离开主线程。
按以下顺序演进,不要跳过步骤。
- **主线程单线程执行。**完全不使用并发。适用于大多数应用。
- **/
async**用于隐藏延迟(网络、磁盘操作)。仍不主动使用并发——SDK API如await会替你完成任务卸载。URLSession.data(from:) - ****用于将你的耗时任务移出主线程——仅在Instruments显示卡顿后使用。
@concurrent - ****用于将状态移出主Actor——仅当过多主Actor状态导致任务频繁切换时使用。
actor
先开启正确的构建设置。在所有项目中启用易用并发(Approachable Concurrency)。对于应用模块和面向UI的模块,还需将默认Actor隔离(Default Actor Isolation)设置为MainActor——这是Xcode 26中新建应用项目的默认设置,可删除大部分注解。在Package中设置:。不要为通用库设置默认主Actor——库应提供 API,由客户端决定任务运行位置。
@MainActorswiftSettings: [.defaultIsolation(MainActor.self)]nonisolatedThe rule that changed
已修改的规则
In Swift 6.2, marking a function does not move it off the current actor. It runs where it was called from. This is what makes "the most natural code to write" data-race free by default.
async- — always switches to the concurrent thread pool. Use it on your CPU-heavy work.
@concurrent - — runs wherever it's called from. This is the right default for library APIs, because the caller decides.
nonisolatedon a type makes all its members nonisolated (Swift 6.1+).nonisolated - Neither one — stays on the caller's actor.
swift
nonisolated struct PhotoProcessor { // decoupled from the main actor
@concurrent // guaranteed to run in the background
func process(_ data: Data) async -> ProcessedPhoto {
async let sticker = extractSticker(data) // two independent jobs, in parallel
async let colors = extractColors(data)
return await ProcessedPhoto(sticker: sticker, colors: colors)
}
}- Profile before you offload. Use Instruments (Time Profiler, hangs). If the code can be made faster without concurrency, always do that first. Concurrency has real cost — task allocation, scheduling, and reasoning.
- Don't spawn a task for trivial work. A child task to read a value costs more than it saves.
UserDefaults - One task per end-to-end operation. Work that must happen in order goes in one task; independent operations get separate tasks so the runtime can interleave them.
- is a suspension point, and it breaks atomicity. State can change while you're suspended, and you may resume on a different thread. Re-check assumptions after every
await. Never hold a lock across one. Never rely on thread-local storage across one.await
**在Swift 6.2中,标记函数为并不会将其移出当前Actor。**它会在调用者所在的Actor上运行。这使得「最自然的代码写法」默认具备数据竞争安全性。
async- ——始终切换至并发线程池。用于你的CPU密集型任务。
@concurrent - ——在调用者所在的线程运行。这是库API的正确默认设置,因为调用者可自行决定运行位置。在类型上标记
nonisolated会使其所有成员变为非隔离(Swift 6.1+)。nonisolated - 不标记任何属性——保留在调用者的Actor上运行。
swift
nonisolated struct PhotoProcessor { // 与主Actor解耦
@concurrent // 保证在后台运行
func process(_ data: Data) async -> ProcessedPhoto {
async let sticker = extractSticker(data) // 两个独立任务,并行执行
async let colors = extractColors(data)
return await ProcessedPhoto(sticker: sticker, colors: colors)
}
}- **在卸载任务前先做性能分析。**使用Instruments(Time Profiler、hangs工具)。如果无需并发即可提升性能,优先选择这种方式。并发存在实际开销——任务分配、调度和逻辑复杂度。
- **不要为琐碎任务创建任务。**创建子任务读取值的开销大于收益。
UserDefaults - **每个端到端操作对应一个任务。**必须按顺序执行的工作放在一个任务中;独立操作使用单独任务,以便运行时交错执行。
- **是挂起点,会打破原子性。**挂起期间状态可能发生变化,恢复时可能在不同线程。每次
await后重新检查假设。永远不要在await期间持有锁。永远不要依赖跨await的线程本地存储。await
Actor reentrancy
Actor可重入性
Actors guarantee mutual exclusion, not transactions. Between two s on the same actor, other work runs.
await- Mutate actor state in synchronous methods. Synchronous code on an actor runs to completion uninterrupted — that's your transaction boundary.
- Keep async actor methods thin, composed of synchronous transactional operations, and leave the actor in a consistent state at every .
await - The classic bug: check cache → download → write cache. Two tasks both miss, both download, the second clobbers the first. Re-check after the
await, or dedupe the in-flight work.await - Actors are not FIFO. They run highest-priority work first, precisely to avoid priority inversion. If you need ordering, use a task (which runs start to finish) or an , not an actor.
AsyncStream
Actor保证互斥,但不保证事务性。在同一Actor的两次之间,会执行其他工作。
await- **在同步方法中修改Actor状态。**Actor上的同步代码会不间断地运行至完成——这就是事务边界。
- 保持异步Actor方法简洁,由同步事务操作组成,并在每次时让Actor处于一致状态。
await - 典型错误:检查缓存 → 下载 → 写入缓存。两个任务均未命中缓存,都执行下载,第二个任务覆盖第一个任务的结果。
await后重新检查,或去重在运行中的任务。await - **Actor不是FIFO队列。**它会优先执行高优先级任务,以避免优先级反转。如果需要顺序执行,使用任务(从头到尾运行)或,而非Actor。
AsyncStream
4. Sendable and sharing data
4. Sendable与数据共享
Sendable- Value types are when their storage is — inferred automatically for non-public types. Public types never get inferred sendability: marking a public type
Sendableis a promise to your clients, so Swift makes you write it.Sendable - Actors and classes are implicitly
@MainActor, because their state is isolated.Sendable - Most model classes should be neither nor
@MainActor. Keep them non-Sendableon purpose — it prevents half the model being mutated on the main thread while the other half is mutated in the background. If they need to leave the main actor, make themSendable, notnonisolated.Sendable - You can still send a non-object between domains as long as the sender stops using it. Make all your mutations before handing it off; touching it afterward is the error.
Sendable - Closures capture state too. Only mark a function type if it genuinely crosses domains.
@Sendable - is a promise the compiler can't check. Reserve it for types with real internal synchronization (a
@unchecked Sendable, a lock). Same forMutexon a global — last resort, not a warning silencer.nonisolated(unsafe)
When you hit a data-race error, work down this list:
- Don't share it. Move the shared object into a local so each concurrent job gets its own instance. (This is the fix for the overwhelming majority of real errors.)
- Make it a value type, so "sharing" is really copying.
Sendable - Isolate it to an actor — the main actor, or your own.
- Only then reach for /
Mutexfrom theAtomicmodule (store them inSynchronizationproperties), orlet.@unchecked Sendable
Global and static variables are the most common source of errors. In order of preference: make it a ; put it on ; wrap it in a ; . Note globals in Swift are initialized lazily and atomically — unlike C.
let@MainActorMutexnonisolated(unsafe)Bridging old callback APIs: annotate delegate protocols with if you own them. If you don't, mark the method and use — it asserts rather than hopping, so it traps loudly instead of racing silently. on the conformance is the shorthand for the same thing. Use to temporarily silence sendability warnings from a module that hasn't migrated; the warnings come back — correctly — once it does.
@MainActornonisolatedMainActor.assumeIsolated { }@preconcurrency@preconcurrency importSendable- 值类型在其存储为时自动推断为
Sendable——非公开类型会自动推断。公开类型永远不会自动推断可发送性:标记公开类型为Sendable是向客户端做出的承诺,因此Swift要求显式声明。Sendable - Actor和类隐式为
@MainActor,因为它们的状态是隔离的。Sendable - **大多数模型类既不应是也不应是
@MainActor。**故意保持非Sendable——可避免模型的一部分在主线程修改,另一部分在后台修改。如果需要离开主Actor,将其设为Sendable,而非nonisolated。Sendable - 仍可在域之间发送非对象,只要发送方停止使用它。在移交前完成所有修改;之后再触碰它就是错误。
Sendable - 闭包也会捕获状态。仅当函数类型确实需要跨域时才标记为。
@Sendable - **是编译器无法验证的承诺。**仅保留给具备真正内部同步机制的类型(如
@unchecked Sendable、锁)。全局变量的Mutex同理——是最后手段,而非警告消除器。nonisolated(unsafe)
遇到数据竞争错误时,按以下顺序解决:
- **不要共享它。**将共享对象移至本地,让每个并发任务拥有自己的实例。(这是解决绝大多数实际错误的方案。)
- 将其改为值类型,这样「共享」实际上是复制。
Sendable - 将其隔离至Actor——主Actor或自定义Actor。
- 只有在万不得已时才使用模块的
Synchronization/Mutex(存储在Atomic属性中),或let。@unchecked Sendable
**全局变量和静态变量是最常见的错误来源。**优先顺序:设为;放在上;用包装;。注意Swift中的全局变量是懒加载且原子性的——与C不同。
let@MainActorMutexnonisolated(unsafe)**桥接旧的回调API:**如果是你拥有的代理协议,用注解。如果不是,将方法标记为并使用——它会断言而非切换线程,因此会直接崩溃而非静默竞争。协议一致性上的是简写形式。使用临时消除未迁移模块的可发送性警告;一旦模块完成迁移,警告会正确恢复。
@MainActornonisolatedMainActor.assumeIsolated { }@preconcurrency@preconcurrency import5. Structured concurrency
5. 结构化并发
Always prefer structured tasks.
Structured tasks (, task groups) are scoped like local variables: they can't outlive the block, they're awaited automatically, and they inherit cancellation, priority, and task-local values through the task tree. Unstructured tasks (, ) give you none of that automatically.
async letTask { }Task.detached- for a fixed, statically known number of concurrent children.
async let - when the number is dynamic. Task groups conform to
withTaskGroup— iterate results as they land. UseAsyncSequencewhen children return nothing: it frees each child's resources immediately and cancels siblings on the first error.withDiscardingTaskGroup - only when the work's lifetime doesn't fit a scope — reacting to a delegate callback, a button tap, a view appearing. It inherits actor isolation and priority; you must manage cancellation yourself.
Task { } - almost never. It inherits nothing — not isolation, not priority, not task-locals. If you need a detached root, put a task group inside it rather than detaching repeatedly.
Task.detached
Cancellation is cooperative. Cancelling sets a flag; it stops nothing. Check or before starting expensive work, and in synchronous helpers too. For work that's suspended rather than running (an 's ), use — and remember the handler runs immediately and concurrently with the body, so the state it touches needs real synchronization (an atomic or a lock, not an actor — you can't guarantee ordering on an actor).
Task.isCancelledtry Task.checkCancellation()AsyncSequencenext()withTaskCancellationHandlerBound your concurrency. Don't fan out one child per item over an unbounded list. Start N children, then add a new one each time one finishes.
Task-local values () propagate context — a request ID, a trace span — down the task tree without threading a parameter through every signature. Make them optional so unbound reads have a sensible default.
@TaskLocalBridging callbacks: / . The contract is resume exactly once on every path — never resuming hangs the caller forever; resuming twice is a fatal error. For delegate APIs that fire later, store the continuation and nil it out when you resume. (Swift 6.4 — unreleased — adds a type that checks single-resumption at compile time.)
withCheckedContinuationwithCheckedThrowingContinuationContinuationAsyncSequencefor awaitfor try awaitAsyncStreamAsyncThrowingStreamyieldonTermination始终优先使用结构化任务。
结构化任务(、任务组)的作用域类似于局部变量:无法超出代码块的生命周期,会自动被等待,并通过任务树继承取消、优先级和任务本地值。非结构化任务(、)无法自动获得这些特性。
async letTask { }Task.detached- ****用于固定、静态已知数量的并发子任务。
async let - 用于动态数量的子任务。任务组符合
withTaskGroup——可在结果到达时迭代。当子任务无返回值时,使用AsyncSequence:它会立即释放每个子任务的资源,并在第一个错误发生时取消其他子任务。withDiscardingTaskGroup - ****仅在任务生命周期不适合作用域时使用——响应代理回调、按钮点击、视图出现等。它继承Actor隔离和优先级;必须自行管理取消。
Task { } - ****几乎从不使用。它不继承任何特性——隔离、优先级、任务本地值均不继承。如果需要分离根任务,在其中放置任务组而非重复分离。
Task.detached
**取消是协作式的。**取消会设置一个标志,但不会停止任何操作。在开始昂贵工作前检查或,同步辅助函数中也需检查。对于挂起而非运行的工作(如的),使用——记住处理器会立即与主体并发运行,因此它触碰的状态需要真正的同步机制(原子或锁,而非Actor——无法保证Actor上的顺序)。
Task.isCancelledtry Task.checkCancellation()AsyncSequencenext()withTaskCancellationHandler**限制并发数量。**不要为无界列表中的每个项创建一个子任务。启动N个子任务,每个子任务完成后再添加新的子任务。
任务本地值()向下传递上下文——请求ID、跟踪Span——无需通过每个签名传递参数。将其设为可选,以便未绑定读取时有合理默认值。
@TaskLocal桥接回调:使用 / 。契约是在所有路径上精确恢复一次——从不恢复会永远挂起调用者;恢复两次是致命错误。对于稍后触发的代理API,存储continuation并在恢复时将其置为nil。(未发布的Swift 6.4添加了类型,可在编译时检查单次恢复。)
withCheckedContinuationwithCheckedThrowingContinuationContinuation**:**使用 / 迭代。使用 / 适配现有的基于处理器或代理的API——在闭包中构造源,从处理器中,并在中清理。
AsyncSequencefor awaitfor try awaitAsyncStreamAsyncThrowingStreamyieldonTermination6. Concurrency in SwiftUI
6. SwiftUI中的并发
- is
View-isolated, and so is everything it contains, including your@MainActor. You almost never need to write@Stateon a view or a view model — and with main-actor-by-default you can delete the ones you have.@MainActor - SwiftUI deliberately runs some of your code off the main thread to keep frames cheap. The signal is in the API's signature:
@Sendable,visualEffect,Shape.path(in:)requirements,Layout. When you hit an isolation error inside one of those closures, don't sendonGeometryChange— copy the one value you need into the closure's capture list.self
swift
.visualEffect { [pulse] effect, proxy in // copy the Bool, don't capture self
effect.blur(radius: pulse ? 2 : 0)
}- SwiftUI's action callbacks are synchronous on purpose. Time-sensitive UI updates — starting an animation in response to a gesture or a scroll event — must happen on the same frame as the event. Put the state change in the synchronous callback; open a
withAnimationonly for the long-running work that follows.Task - Put a piece of state on the seam between UI and async work. The view kicks off a task; the async layer does a synchronous mutation when it finishes; the UI reacts. That keeps view logic synchronous and makes the async logic testable without importing SwiftUI.
- 默认是
View隔离的,包含的所有内容也是如此,包括@MainActor。几乎不需要在视图或视图模型上写@State——启用默认主Actor后,可删除已有的注解。@MainActor - SwiftUI会故意在主线程外运行部分代码以降低帧成本。信号是API签名中的:
@Sendable、visualEffect、Shape.path(in:)要求、Layout。如果在这些闭包中遇到隔离错误,不要传递onGeometryChange——将所需的单个值复制到闭包的捕获列表中。self
swift
.visualEffect { [pulse] effect, proxy in // 复制Bool值,不要捕获self
effect.blur(radius: pulse ? 2 : 0)
}- **SwiftUI的动作回调默认是同步的。**时间敏感的UI更新——响应手势或滚动事件启动动画——必须与事件在同一帧发生。将状态变更放在同步回调中;仅在后续的长时间工作中开启
withAnimation。Task - **在UI与异步工作之间放置状态层。**视图启动任务;异步层完成后进行同步状态变更;UI做出响应。这可保持视图逻辑同步,并让异步逻辑无需导入SwiftUI即可测试。
7. Protocols and generics
7. 协议与泛型
Don't start with a class. Don't start with a protocol either.
The workflow: write concrete types → notice repeated code across them → factor the shared capability into a protocol → write generic code against it. Overloads with near-identical bodies are the signal that it's time to generalize.
- A protocol with no per-type customization is a wasted protocol. If every conformance would use the same default implementation, write a constrained extension on an existing protocol instead. Elaborate protocol hierarchies ("type zoology") cost compile time and binary size and buy nothing.
- Prefer has-a to is-a. If only some of a protocol's operations make sense for your type, don't refine it — wrap it in a generic struct and expose exactly the API you mean. (rather than
GeometricVector<Storage: SIMD>.)GeometricVector: SIMD - A protocol requirement is a customization point — it's dynamically dispatched, and a conforming type's implementation wins everywhere. A method only in an extension is statically dispatched, so a conformer's version shadows rather than overrides it, and code that only knows calls the extension's. If a type should be able to customize something, make it a requirement.
any P - Composition over inheritance. Class inheritance is monolithic (one superclass), intrusive (you inherit stored properties and initializer complexity), and leaves unwritten contracts about what may be overridden and when to call super. Compose small values instead.
- A forced downcast is a code smell — it usually means a type relationship was lost to a class hierarchy or an existential.
不要从类开始。也不要从协议开始。
工作流程:**编写具体类型 → 发现类型间存在重复代码 → 将共享能力提取为协议 → 针对协议编写泛型代码。**具有近乎相同主体的重载函数是需要泛化的信号。
- **无类型定制的协议是无用的。**如果每个一致性都使用相同的默认实现,改为在现有协议上编写约束扩展。复杂的协议层次结构(「类型分类学」)会增加编译时间和二进制大小,却毫无收益。
- **优先使用组合而非继承。**如果仅协议的部分操作对类型有意义,不要继承——将其包装在泛型结构体中,仅暴露所需的API。(如而非
GeometricVector<Storage: SIMD>。)GeometricVector: SIMD - 协议要求是定制点——它是动态派发的,一致性类型的实现会在所有地方生效。仅在扩展中的方法是静态派发的,因此一致性类型的版本会覆盖而非重写它,仅知道的代码会调用扩展中的方法。如果类型需要定制某个功能,将其设为协议要求。
any P - **组合优于继承。**类继承是单一的(一个父类)、侵入式的(继承存储属性和初始化器复杂度),且留下关于可重写内容和何时调用super的未书面契约。改用小值组合。
- 强制向下转型是代码异味——通常意味着类型关系因类层次结构或存在类型而丢失。
some
vs any
someanysome
vs any
someany- Write by default. Change to
some Pwhen you need to store arbitrary types. Same discipline asany Pbeforelet.var - — one fixed underlying type per scope. You keep every type relationship, including associated types, and the compiler can specialize.
some P - — type-erased box, dynamic type varies at runtime. Needed for heterogeneous collections, for optionality of the underlying type, and to hide the abstraction entirely. You pay for it: associated-type relationships are erased to their upper bounds, and calls are opaque to the optimizer.
any P - You cannot call a method that takes an associated type on an . Erasure works in producing position (the result is erased to its upper bound) but not consuming position. The fix is to pass the existential into a function taking
any P— the compiler unboxes it, and inside that scope the type is fixed again.some P - Constrained existentials and opaque types — ,
some Collection<Element>— let you hideany Collection<any Animal>while still exposing the element type. Declare primary associated types on your own protocols (LazyFilterSequence<[Animal]>) for the type callers actually supply, not for implementation details likeprotocol Container<Item>.Iterator - Same-type requirements in clauses are how you pin down relationships across protocols (
where). Without them, "grow then harvest" doesn't typecheck, and wrong conformances compile.where Self.CropType.FeedType == Self
- **默认使用。仅在需要存储任意类型时改为
some P。**与优先使用any P而非let的原则一致。var - ——每个作用域对应一个固定的底层类型。保留所有类型关系,包括关联类型,编译器可进行特化。
some P - ——类型擦除的装箱,运行时动态类型可变。适用于异构集合、底层类型可选性,以及完全隐藏抽象的场景。需要付出代价:关联类型关系会被擦除为其上限,调用对优化器不透明。
any P - **无法在上调用带关联类型的方法。**擦除在产出位置有效(结果被擦除为其上限),但在消费位置无效。解决方法是将存在类型传入接受
any P的函数——编译器会拆箱,在该作用域内类型再次固定。some P - 约束存在类型和不透明类型——、
some Collection<Element>——可隐藏any Collection<any Animal>同时仍暴露元素类型。在自定义协议上声明主关联类型(LazyFilterSequence<[Animal]>),用于调用者实际提供的类型,而非protocol Container<Item>等实现细节。Iterator - 子句中的同类型要求用于固定协议间的关系(
where)。没有它们,「种植然后收获」无法通过类型检查,错误的一致性会被编译通过。where Self.CropType.FeedType == Self
8. API design — clarity at the point of use
8. API设计——使用点清晰优先
Clarity at the point of use is the goal that outranks every other one here.
- No type prefixes in Swift-only APIs. Modules disambiguate. Keep prefixes only where the API mirrors an Objective-C one. But avoid very general names from specific frameworks — they read badly out of context and force manual disambiguation.
- Drop leading from async alternatives and from anything that returns its result directly.
get, notpersistentPosts.getPersistentPosts - Access control is documentation. (file),
private(module, and the default),internal,package. Being explicit at the boundary is what forces the sendability and API-evolution decisions above.public - Design the model so illegal states can't be spelled. Private setters plus a validating mutating method; enums for closed sets; a strongly typed instead of a
UUID.String - Property wrappers factor out an access policy (,
@Argument, defensive copying, lazy, thread-local) so the declaration site states the policy in one word. Combine with@Publishedon a key path to project through a wrapper (that's how@dynamicMemberLookupworks).$binding.title - Result builders for declarative DSLs. Macros when the boilerplate is code the compiler could have written (§12).
使用点清晰是高于其他所有目标的核心原则。
- **纯Swift API中不要使用类型前缀。**模块会进行区分。仅在API镜像Objective-C API时保留前缀。但避免使用特定框架中的通用名称——脱离上下文时可读性差,且需要手动消除歧义。
- **从异步替代方案和直接返回结果的方法中删除前缀。**使用
get而非persistentPosts。getPersistentPosts - 访问控制即文档。(文件内)、
private(模块内,默认)、internal、package。在边界处显式声明,可强制做出上述可发送性和API演进决策。public - **设计模型以避免非法状态。**私有setter加验证修改方法;枚举用于封闭集合;强类型而非
UUID。String - 属性包装器提取访问策略(、
@Argument、防御性复制、懒加载、线程本地),以便声明位置用一个词说明策略。结合@Published和键路径,可通过包装器投影(如@dynamicMemberLookup的工作方式)。$binding.title - 结果构建器用于声明式DSL。宏用于编译器可推导的样板代码(第12章)。
9. Performance — measure, then choose
9. 性能——先测量,再选择
Low-level Swift performance is dominated by four costs. Know which one you're paying.
- Function calls — argument copies, static vs dynamic dispatch, call-frame allocation, and blocked optimization.
- Memory layout — inline vs out-of-line storage; dynamically sized types.
- Allocation — global (free), stack (cheap: one subtraction), heap (expensive: search plus locking).
- Copies — retains/releases and recursive struct copies.
But do the algorithmic work first. Every time you write a loop, try replacing it with a call to an algorithm. The largest wins are almost never micro-optimizations:
- Know the complexity of what you call. is O(n); calling it in a loop is O(n²).
Array.remove(at:)is O(n) total. Building aremoveAll(where:)by re-slicing per byte is O(n²);Datais O(1). Both of these were 100×+ regressions hiding behind clean-looking code.popFirst() - Chained /
map/flatMapallocate an array per stage. Elegant ≠ fast. If a pipeline runs per-pixel or per-element in a hot loop, size the output once and write into it.filter - Then profile. Instruments' Time Profiler and Allocations, run against a test (secondary-click the test's run button → Profile) so you're measuring exactly the code you care about. dominating a flame graph means accidental copying; a million transient allocations means intermediate arrays;
platform_memmovemeans runtime exclusivity checks;swift_beginAccess/swift_retainmeans reference-counting traffic.swift_release
Concrete levers, roughly in order of what they buy:
- on classes you don't intend to subclass turns dynamic dispatch static and unlocks inlining. Whole-module optimization lets the compiler prove this for you in many cases — and enables generic specialization, which is where generics stop costing anything.
final - Struct storage is inline; class storage is out-of-line. Small structs are free; a large struct with three reference-typed fields costs three retains per copy, versus one for a class. If you copy it a lot, use copy-on-write.
- An existential has a 3-word inline buffer. Values that fit live inline; larger ones get heap-allocated per copy. Same technique applies: give the large type indirect storage with copy-on-write and it fits in the buffer again.
any P - Homogeneous beats
[MyModel]— densely packed, type info passed once, specializable.[any Model]is the flexible-but-opaque option; take it when you need it.[any Model] - Constraining a generic parameter to a class () gives the compiler a known representation even without specialization.
T: AnyObject - (Swift 6.2) for fixed-size storage: elements stored inline, size in the type via value generics, no heap allocation, no reference counting, no uniqueness or exclusivity checks. Wrong choice if it gets copied or shared.
InlineArray<N, T> - /
Span/RawSpan(Swift 6.2) replaceOutputSpanfor direct access to contiguous storage. They're non-escapable, so the compiler ties their lifetime to the container — you get pointer performance with no lifetime bugs, and the retains/releases disappear.withUnsafeBufferPointer - Moving stored properties out of a nested class into the parent struct removes runtime exclusivity checks.
- Shipped in Swift 6.3, when you've measured the need: (pair with
@inline(always)on methods) andfinal(SE-0460) to pre-specialize a generic for hot concrete types.@specialized(where T == ...) - Landing in Swift 6.4 (unreleased — see the note below §15): /
borrowaccessors instead ofmutate/getfor large stored values,set/UniqueArray, andUniqueBox/Refto hoist a repeated lookup out of a loop.MutableRef
Async functions keep their state on a per-task slab allocator rather than the C stack, and split into partial functions at each suspension point. The cost profile is similar to sync functions with slightly higher call overhead — which is another reason not to make something that has nothing to await.
asyncHops to and from the main actor cost a real context switch. Batch: push the loop into / so they take arrays, rather than hopping twice per iteration.
loadArticlesupdateUISwift底层性能主要由四种开销决定。要知道你正在付出哪种开销。
- 函数调用——参数复制、静态与动态派发、调用帧分配、优化受阻。
- 内存布局——内联与离线存储;动态大小类型。
- 内存分配——全局(免费)、栈(廉价:仅一次减法)、堆(昂贵:搜索加锁)。
- 复制——保留/释放和递归结构体复制。
**但首先优化算法。**每次编写循环时,尝试用算法调用替代。最大的性能提升几乎永远不是微优化:
- 了解所调用方法的复杂度。是O(n);在循环中调用是O(n²)。
Array.remove(at:)总复杂度是O(n)。通过逐字节重新切片构建removeAll(where:)是O(n²);Data是O(1)。这些都是隐藏在简洁代码背后的100倍以上性能退化。popFirst() - **链式/
map/flatMap会为每个阶段分配数组。**优雅≠快速。如果流水线在热点路径中逐像素或逐元素运行,先确定输出大小再写入。filter - **然后进行性能分析。**使用Instruments的Time Profiler和Allocations,针对测试运行(右键点击测试的运行按钮→Profile),以便精确测量你关心的代码。火焰图中占主导意味着意外复制;百万级临时分配意味着中间数组;
platform_memmove意味着运行时排他性检查;swift_beginAccess/swift_retain意味着引用计数开销。swift_release
具体优化手段,按收益大致排序:
- **在不打算子类化的类上使用**将动态派发转为静态派发,并解锁内联。全模块优化可让编译器在许多情况下自动推断这一点——并启用泛型特化,此时泛型不再有开销。
final - **结构体存储是内联的;类存储是离线的。**小结构体是免费的;包含三个引用类型字段的大结构体每次复制会产生三次保留操作,而类仅一次。如果频繁复制,使用写时复制。
- **存在类型有3字内联缓冲区。**适合的值内联存储;较大的值每次复制会堆分配。同样的解决方案:为大类型提供间接存储并实现写时复制,使其能放入缓冲区。
any P - 同质优于
[MyModel]——密集打包,类型信息仅传递一次,可特化。[any Model]是灵活但不透明的选项;仅在需要时使用。[any Model] - 将泛型参数约束为类()可让编译器在无需特化的情况下获得已知表示。
T: AnyObject - (Swift 6.2)用于固定大小存储:元素内联存储,大小通过值泛型在类型中声明,无堆分配,无引用计数,无唯一性或排他性检查。如果需要复制或共享,则不适合。
InlineArray<N, T> - /
Span/RawSpan(Swift 6.2)替代OutputSpan以直接访问连续存储。它们是非逃逸的,因此编译器将其生命周期与容器绑定——获得指针性能且无生命周期错误,保留/释放操作消失。withUnsafeBufferPointer - 将嵌套类的存储属性移至父结构体可消除运行时排他性检查。
- 在Swift 6.3中,仅在测量后确有需要时使用:(与方法上的
@inline(always)配合)和final(SE-0460)为热点具体类型预特化泛型。@specialized(where T == ...) - 即将在Swift 6.4中发布(未发布——见第15章下方说明):针对大存储值使用/
borrow访问器替代mutate/get,set/UniqueArray,以及UniqueBox/Ref将重复查找移出循环。MutableRef
异步函数将状态存储在每个任务的 slab 分配器中,而非C栈,并在每个挂起点拆分为部分函数。开销与同步函数类似,但调用开销略高——这是另一个不要将无需等待的函数设为的原因。
async**往返主Actor会产生真实的上下文切换开销。**批量处理:将循环推入/,使其接受数组,而非每次迭代切换两次。
loadArticlesupdateUI10. ARC and object lifetime
10. ARC与对象生命周期
- An object's guaranteed lifetime ends at its last use, not at the closing brace. Observed lifetimes are an emergent property of the optimizer and will change. Code that depends on when a runs is a latent bug.
deinit - /
weakare for breaking reference cycles — nothing else. Reading aunownedreference after the strong owner's last use may legitimately giveweak. Optional binding there is worse than force-unwrap: it turns a loud crash into a silent wrong answer.nil - Better than : don't build the cycle. Factor the shared data into a third type both sides reference, turning the cycle into a tree.
weak - Next best: redesign the API so the object is only reachable through a strong reference. works but shifts correctness onto you and spreads through a codebase — treat it as a patch, not a design.
withExtendedLifetime - Keep side effects local. Publishing metrics or firing a global effect from
deinitsequences against optimizer decisions. Usedeinitat the call site instead, and leavedeferfor verification.deinit - Xcode's Optimize Object Lifetimes build setting shortens observed lifetimes toward the guaranteed minimum, and will surface exactly these bugs.
- **对象的保证生命周期结束于最后一次使用,而非闭合括号处。**观测到的生命周期是优化器的涌现属性,会发生变化。依赖运行时机的代码是潜在bug。
deinit - **/
weak仅用于打破引用循环——别无他用。**在强所有者最后一次使用后读取unowned引用可能合法地得到weak。此处的可选绑定比强制解包更糟:它将明显的崩溃转为静默的错误结果。nil - **比更好的方案:不要构建循环。**将共享数据提取为第三个类型,让双方都引用它,将循环转为树结构。
weak - 次优方案:重新设计API,使对象仅能通过强引用访问。有效,但将正确性转移给你并扩散到代码库——将其视为补丁,而非设计方案。
withExtendedLifetime - **保持副作用局部化。**从
deinit发布指标或触发全局效果与优化器决策冲突。改用调用站点的deinit,让defer仅用于验证。deinit - Xcode的**优化对象生命周期(Optimize Object Lifetimes)**构建设置会将观测到的生命周期缩短至保证最小值,并会暴露此类bug。
11. Testing — Swift Testing by default
11. 测试——默认使用Swift Testing
Use Swift Testing for new tests. XCTest remains required for exactly three things: UI automation (), performance metrics (), and tests that must be written in Objective-C or that catch Objective-C exceptions.
XCUIApplicationXCTMetric- on any function — global, static, or instance;
@Test,async, and global-actor-isolated all work.throws - takes ordinary expressions. No family of
#expect(...)-style functions to memorize —XCTAssertEqual,#expect(a == b),#expect(list.isEmpty)all capture and display subexpression values on failure.#expect(!x.contains(y)) - to stop the test on failure, and to unwrap an optional safely. This replaces
try #require(...)and lets you choose per-expectation.continueAfterFailure = false - Suites are s. A fresh instance is created per test function, so state can't leak between tests. Use
structfor setup; only use ainit/classwhen you needactorfor teardown. Nest suites to group.deinit - Parameterize instead of copy-pasting or looping. runs each case independently, in parallel, individually re-runnable, with the failing argument named in the results. Two argument collections produce the full cross product — use
@Test(arguments: [...])when you want matched pairs instead.zip() - Traits carry intent: /
.enabled(if:)for conditions (never comment a test out — a disabled test still compiles),.disabled("reason")for tracking,.bug(url)to relate tests across files and targets,.tags(...),.timeLimitwhen a test genuinely can't run in parallel. Use.serializedrather than a runtime@availablecheck so the testing library knows.#available - for a test failing on something outside your control — it keeps compiling and running and tells you when the issue is fixed, unlike
withKnownIssue { }..disabled - for callbacks that fire N times;
confirmationfor one-shot callbacks with no async overload.withCheckedContinuation - Tests run in parallel by default, in randomized order. That's a feature: it surfaces hidden inter-test dependencies. Refactor rather than reaching for .
.serialized - Exit tests — — cover
#expect(processExitsWith: .failure) { ... }/preconditionpaths in an isolated child process. macOS, Linux, FreeBSD, Windows only.fatalError - Migrating: both frameworks coexist in one target, so migrate incrementally and write new tests in Swift Testing today. Test framework interoperability (swift-testing ST-0021; check your Xcode version for availability) lets helpers that wrap be called from Swift Testing tests and vice versa; set the mode to complete or strict (not limited, and never none) so cross-framework issues stay errors and point you at the
XCTFailreplacement.Issue.record
新测试使用Swift Testing。XCTest仅在以下三种情况下是必需的:UI自动化()、性能指标()、必须用Objective-C编写或捕获Objective-C异常的测试。
XCUIApplicationXCTMetric- 在任意函数上使用——全局、静态或实例函数;
@Test、async和全局Actor隔离的函数均支持。throws - **接受普通表达式。**无需记忆
#expect(...)系列函数——XCTAssertEqual、#expect(a == b)、#expect(list.isEmpty)在失败时都会捕获并显示子表达式的值。#expect(!x.contains(y)) - ****用于在失败时停止测试,并安全解包可选值。这替代了
try #require(...),并允许按期望选择是否停止。continueAfterFailure = false - **测试套件是。**每个测试函数会创建一个新实例,因此状态不会在测试间泄漏。使用
struct进行设置;仅在需要init进行清理时使用deinit/class。嵌套套件进行分组。actor - 参数化而非复制粘贴或循环。会独立运行每个用例,并行执行,可单独重新运行,结果中会显示失败的参数。两个参数集合会生成完整的笛卡尔积——当需要匹配对时使用
@Test(arguments: [...])。zip() - 特性传递意图:/
.enabled(if:)用于条件(永远不要注释掉测试——禁用的测试仍会编译),.disabled("reason")用于跟踪,.bug(url)用于关联跨文件和目标的测试,.tags(...),.timeLimit用于确实无法并行运行的测试。使用.serialized而非运行时@available检查,以便测试库知晓。#available - ****用于因外部因素导致失败的测试——它会保持编译和运行,并在问题修复时通知你,与
withKnownIssue { }不同。.disabled - ****用于触发N次的回调;
confirmation用于无异步重载的一次性回调。withCheckedContinuation - **测试默认并行运行,顺序随机。**这是特性:它会暴露隐藏的测试间依赖。重构而非使用。
.serialized - 退出测试————在隔离的子进程中覆盖
#expect(processExitsWith: .failure) { ... }/precondition路径。仅支持macOS、Linux、FreeBSD、Windows。fatalError - 迁移:两个框架可在同一目标中共存,因此可逐步迁移,今天就用Swift Testing编写新测试。测试框架互操作性(swift-testing ST-0021;检查你的Xcode版本是否支持)允许包装的助手函数被Swift Testing测试调用,反之亦然;将模式设置为complete或strict(不要设置为limited,更不要设置为none),以便跨框架问题保持为错误并指向
XCTFail替代方案。Issue.record
12. Macros
12. 宏
Reach for a macro when you're writing code the compiler could derive — and only then.
- Macros are type-checked before expansion. Arguments are checked against the macro's declared signature, so misuse is a clean error at the call site, not a mess inside generated code.
- Freestanding () produce an expression or declaration. Attached (
#foo) augment a declaration in one of five roles: member, peer, accessor, member-attribute, conformance. Roles compose —@Foois member + member-attribute + conformance.@Observable - Test macros as pure syntax-tree transforms with . It's the fastest loop, and it's how you avoid bugs in code nobody reads. Set a breakpoint in
assertMacroExpansionandexpansionthe syntax node to learn its shape.po - Emit real diagnostics when the macro doesn't apply. Throw an error, or use for warnings and fix-its at a specific location. Never let a macro silently generate code that won't compile.
context.addDiagnostic - Expanded code is ordinary Swift: inspectable ("Expand Macro"), debuggable, steppable.
仅在编写编译器可推导的代码时使用宏。
- **宏在展开前会进行类型检查。**参数会根据宏的声明签名进行检查,因此误用会在调用点产生清晰的错误,而非生成代码内部的混乱。
- **独立宏()**生成表达式或声明。**附加宏(
#foo)**以五种角色之一增强声明:成员、同级、访问器、成员属性、一致性。角色可组合——@Foo是成员+成员属性+一致性。@Observable - 将宏作为纯语法树转换进行测试,使用。这是最快的循环,可避免无人阅读的代码中的bug。在
assertMacroExpansion中设置断点,使用expansion查看语法节点的形状。po - **当宏不适用时发出真实诊断信息。**抛出错误,或使用在特定位置发出警告和修复建议。永远不要让宏静默生成无法编译的代码。
context.addDiagnostic - 展开后的代码是普通Swift代码:可检查(「Expand Macro」)、可调试、可单步执行。
13. Logging and debugging
13. 日志与调试
- from
Logger, notos. Create one per subsystem and category. Messages are stored in an optimized form and only rendered when displayed, so logging is cheap enough to leave in.print - Non-numeric interpolations are redacted by default. Opt in per value with only for data that is genuinely not personal. Use
privacy: .publicwhen you need to correlate values without exposing them..private(mask: .hash) - Levels control persistence and cost: (never persisted, fastest — the message construction is optimized away entirely when not streaming),
debug,info(default),notice,error(most persistent, slowest). Log atfault/errorfor the things you'll want in a bug report.fault - Log a correlation ID (a task or request UUID) and you can filter a whole failure's history out of a device log archive without reproducing it. , then filter by subsystem in Console.
log collect --device --start ... - and
format:are free — use them so logs are readable and column-selectable.align: - LLDB understands Swift tasks: it steps through across threads,
awaitshows priority and children, and named tasks show up in both the debugger and Instruments' Swift Concurrency template.swift task info
- **使用模块的
os而非Logger。**为每个子系统和类别创建一个实例。消息以优化形式存储,仅在显示时渲染,因此日志开销足够低,可保留在代码中。print - **非数值插值默认会被脱敏。**仅对真正非个人的数据使用选择加入。当需要关联值但不暴露它们时,使用
privacy: .public。.private(mask: .hash) - 级别控制持久性和开销:(永不持久化,最快——不流式传输时消息构造会被完全优化掉)、
debug、info(默认)、notice、error(最持久,最慢)。使用fault/error级别记录你希望在bug报告中看到的内容。fault - 记录关联ID(任务或请求UUID),你无需重现即可从设备日志归档中过滤出整个失败的历史。使用,然后在Console中按子系统过滤。
log collect --device --start ... - 和
format:是免费的——使用它们让日志可读且可按列选择。align: - LLDB理解Swift任务:它可跨线程单步执行,
await显示优先级和子任务,命名任务会出现在调试器和Instruments的Swift Concurrency模板中。swift task info
14. Unsafe code and interop
14. 不安全代码与互操作性
- "Unsafe" means the API cannot fully validate its input, so violating its preconditions is undefined behavior — not that it crashes. Safe APIs do trap deliberately; a clean fatal error is the safe outcome.
- Prefer over
Span. Since Swift 6.2 there is a safe, non-escaping, equally fast way to get at contiguous storage. Reserve raw pointers for C interop.Unsafe*Pointer - If you must use pointers: keep the unsafe region as small as possible, use buffer pointers (address + count) rather than bare pointers so bounds are tracked, never let a pointer escape the closure that vends it, and run the Address Sanitizer.
- Enable strict memory safety in security-critical modules — it forces every unsafe use to be acknowledged in source, which is what makes an audit possible. Swift 6.4's attribute (unreleased) lets you turn it on for individual functions.
@diagnose - Interop is bidirectional and incremental. C, Objective-C, and C++ types map into Swift directly (including C++ value semantics, containers as Swift collections, and move-only types as ). Swift 6.3's
~Copyableattribute exposes Swift functions back to C (with@cwhen the declaration already exists in a header). Adopt Swift one file at a time; don't rewrite.@implementation
- 「不安全」意味着API无法完全验证输入,因此违反前置条件会导致未定义行为——并非一定会崩溃。安全API会故意触发陷阱;清晰的致命错误是安全的结果。
- **优先使用而非
Span。**自Swift 6.2起,已有安全、非逃逸、速度相当的方式访问连续存储。仅在与C交互时保留原始指针。Unsafe*Pointer - 如果必须使用指针:将不安全区域尽可能缩小,使用缓冲区指针(地址+计数)而非裸指针,以便跟踪边界,永远不要让指针逃逸出提供它的闭包,并运行地址 sanitizer。
- 在安全关键模块中启用严格内存安全——它会强制每个不安全使用在源代码中得到确认,这是审计的前提。未发布的Swift 6.4的属性允许为单个函数启用它。
@diagnose - **互操作性是双向且渐进的。**C、Objective-C和C++类型可直接映射到Swift(包括C++值语义、容器作为Swift集合、仅移动类型作为)。Swift 6.3的
~Copyable属性将Swift函数暴露给C(当声明已在头文件中存在时使用@c)。一次采用一个Swift文件;不要重写整个代码库。@implementation
15. Modern syntax you should be using
15. 你应该使用的现代语法
Agents routinely write the older, longer form of all of these.
Rows marked ⚠ are Swift 6.4, which has not shipped. The current release is 6.3.x. Their proposals are accepted and implemented in main, so they are safe to plan around and unsafe to write today — check the project's toolchain before using one, and prefer the older form if it targets 6.3 or earlier.
| Instead of | Write | Since |
|---|---|---|
Nested ternaries; an immediately-called closure to initialize a | | 5.9 |
| Overloads for 1, 2, 3… arguments | parameter packs ( | 5.9 |
| | 5.9 |
| Polling an object for changes | | 6.2 |
| concrete notification types ( | 6.2 |
| the Subprocess package ( | 6.2+ |
| Hand-rolled string index math | Swift Regex — literals for brevity, | 5.7 |
| | 6.2 |
| | 6.2 |
Manual | | 6.4 ⚠ |
| Rebuilding a dictionary by hand to use the key | | 6.4 ⚠ |
| | 6.4 ⚠ |
| module selector | 6.3 |
| Blanket "warnings as errors" | | 6.4 ⚠ |
| | 6.4 ⚠ |
| Manually parsing binary formats with pointers | Swift Binary Parsing ( | 6.2 |
| Awkward test function names | raw identifiers: | 6.0 |
Also worth knowing: Swift Regex parsers compose with Foundation's real parsers (, ) — never hand-roll date or number parsing inside a regex. Make the locale explicit rather than inheriting the system's. And use or (atomic groups) to stop a pattern backtracking across a whole input.
.date(...).currency(...)NegativeLookaheadLocalAI工具通常会编写这些语法的旧版、冗长形式。
**标记⚠的行是Swift 6.4的特性,尚未发布。**当前发布版本是6.3.x。它们的提案已被接受并在主分支实现,因此可安全规划,但目前不宜使用——在使用前检查项目的工具链,如果目标版本是6.3或更早,优先使用旧版形式。
| 替代写法 | 推荐写法 | 起始版本 |
|---|---|---|
嵌套三元运算符;立即调用闭包初始化 | | 5.9 |
| 针对1、2、3…个参数的重载 | 参数包( | 5.9 |
每个属性都使用 | | 5.9 |
| 轮询对象获取变更 | | 6.2 |
使用字符串类型 | 具体通知类型( | 6.2 |
| Subprocess包( | 6.2+ |
| 手动字符串索引计算 | Swift Regex —— 字面量用于简洁, | 5.7 |
热点路径中固定大小的 | | 6.2 |
| | 6.2 |
手动处理 | | 6.4 ⚠ |
| 手动重建字典以使用键 | | 6.4 ⚠ |
| | 6.4 ⚠ |
类型遮蔽模块时的 | 模块选择器 | 6.3 |
| 全盘「警告视为错误」 | | 6.4 ⚠ |
因 | | 6.4 ⚠ |
| 使用指针手动解析二进制格式 | Swift Binary Parsing( | 6.2 |
| 笨拙的测试函数名称 | 原始标识符: | 6.0 |
另外值得注意:Swift Regex解析器可与Foundation的真实解析器组合使用(、)——永远不要在正则表达式中手动解析日期或数字。显式指定区域设置而非继承系统设置。使用或(原子组)防止模式回溯整个输入。
.date(...).currency(...)NegativeLookaheadLocal16. Migrating an existing codebase to Swift 6
16. 将现有代码库迁移至Swift 6
The order matters, and mixing steps is how migrations stall.
- Build with the new compiler first. Source compatibility means this should just work, in Swift 5 mode.
- Per target, enable complete concurrency checking (Swift 5 mode + all Swift 6 warnings). Start with the UI/app layer, not the frameworks below it — much of it is already main-actor-annotated by the SDK, so the fix rate is high.
- Fix the warnings, cheapest first. Expect hundreds of warnings from a handful of root causes: globals that should be
var, free functions that belong onlet, one public struct that needs@MainActor. A single line can clear dozens.: Sendable - Flip the target to the Swift 6 language mode to lock the work in.
- Move to the next target and repeat.
- Refactor afterwards, separately. Never combine a significant refactor with enabling data-race safety — you'll have to back out both.
You can turn strict checking back off and ship; every fix you made is a genuine improvement that survives. Enable Approachable Concurrency and, for app modules, main-actor-by-default before you start — both dramatically reduce the number of errors you'll see, and Xcode ships migration tooling that applies many of the changes for you (swift.org/migration).
顺序很重要,混合步骤会导致迁移停滞。
- **首先使用新编译器构建。**源代码兼容性意味着在Swift 5模式下应该可以直接运行。
- 按目标启用完整并发检查(Swift 5模式 + 所有Swift 6警告)。从UI/应用层开始,而非下方的框架——SDK已为其大部分内容添加了主Actor注解,因此修复率很高。
- **优先修复成本最低的警告。**预计会有数百个警告,但根源只有少数几个:应设为的
let全局变量,应放在var上的自由函数,需要@MainActor的单个公开结构体。一行代码即可清除数十个警告。: Sendable - 将目标切换至Swift 6语言模式以锁定工作成果。
- 移动到下一个目标并重复。
- **之后单独进行重构。**永远不要将重大重构与启用数据竞争安全结合——你将不得不回滚两者。
你可以关闭严格检查并发布;所做的每个修复都是真正的改进,会保留下来。在开始前启用易用并发,对于应用模块,启用默认主Actor——两者都会大幅减少你将看到的错误数量,Xcode提供了迁移工具可自动应用许多更改(swift.org/migration)。
Quick Reference
快速参考
| Need | Reach for | Not |
|---|---|---|
| A data type | | |
| Shared mutable state | | |
| Move work off the main thread | | |
| A library API's isolation | | |
| Fixed number of parallel jobs | | N unstructured |
| Dynamic number of parallel jobs | | one task per element, unbounded |
| Children that return nothing | | |
| Work tied to a UI event | | making the callback |
| Fixing a data race | stop sharing the object | |
| A shared model class | non- | |
Blocking primitive across | nothing — restructure | |
| Polymorphism | | |
| Heterogeneous collection | | a class hierarchy |
| Shared behavior, no customization | constrained | a new protocol |
| A customization point | protocol requirement | a method only in an extension |
| Breaking a reference cycle | restructure to a tree | |
| Removing matching elements | | |
| Direct access to contiguous memory | | |
| Fixed-size buffer in a hot path | | |
| A new test | | |
| The same test over many inputs | | a |
| Halting a test on failure | | |
| A temporarily broken test | | |
| Diagnostics in shipping code | | |
| Deciding to optimize | Instruments on a profiled test | intuition |
| 需求 | 首选方案 | 不推荐方案 |
|---|---|---|
| 数据类型 | | 无标识或共享需求的 |
| 共享可变状态 | | |
| 将任务移出主线程 | | |
| 库API的隔离 | | |
| 固定数量的并行任务 | | N个非结构化 |
| 动态数量的并行任务 | | 每个元素一个任务,无限制 |
| 无返回值的子任务 | | 从不排空的 |
| 与UI事件绑定的工作 | 回调内部的 | 将回调设为 |
| 修复数据竞争 | 停止共享对象 | |
| 共享模型类 | 非 | |
| 无——重构代码 | |
| 多态 | | |
| 异构集合 | | 类层次结构 |
| 共享行为,无需定制 | 约束 | 新协议 |
| 定制点 | 协议要求 | 仅在扩展中的方法 |
| 打破引用循环 | 重构为树结构 | |
| 删除匹配元素 | | 循环中使用 |
| 直接访问连续内存 | | |
| 热点路径中的固定大小缓冲区 | | |
| 新测试 | | |
| 多输入的同一测试 | | |
| 失败时停止测试 | | |
| 临时失败的测试 | | |
| 发布代码中的诊断 | | |
| 决定是否优化 | 对分析后的测试使用Instruments | 直觉判断 |
",