Written for someone who already knows a systems language and wants the parts of Swift that are actually
peculiar, not a syntax tour. The through-line is what the compiler emits: what a value type costs and when
copy-on-write stops being free, where ARC puts its retains and which ones the optimiser removes, which of the four
dispatch mechanisms a call gets and why a method defined in a protocol extension quietly picks the wrong one, what
an existential box really is — any P measures 40 bytes and this sheet says which five words those
are — and how Swift’s isolation rules turn data races into compile errors. Cards 1–24 are the
guide; the rest is a filterable index of the language, the standard library, Foundation and the toolchain, with a dot
on every entry saying which Swift it needs — 48 of the 534 entries here want Swift 6 or newer. Press
/ to jump to the filter box; hover a clipped row for the whole entry.
Swift is a value-semantics language with automatic reference counting underneath it, and almost everything surprising follows from that one sentence. There is no tracing collector, so there is no pause and no nondeterminism — and there is instead a retain/release traffic problem and a cycle problem. Types are checked statically and generics are specialised rather than boxed, so generic code can be as fast as hand-written code — and when the compiler cannot see across a module boundary it falls back to a boxed, table-driven representation that is not.
| Construct | Runtime cost | Hidden cost |
|---|---|---|
struct of trivial fields | none — registers, like a C struct | copies are memberwise; no field reordering (see the layout card) |
class instance | heap allocation, 16-byte header, refcount traffic | every copy of the reference is an atomic increment |
Array, String, Dictionary | one word; the storage is a class instance | a write checks uniqueness; if shared, the whole buffer is copied |
any P existential | 40 bytes: 3-word buffer + metadata + witness table | a payload larger than 3 words is heap-boxed on every copy |
some P opaque type | zero — one concrete type, resolved at compile time | none; this is the one to reach for |
generic func f<T> | zero when specialised, a witness-table call when not | specialisation stops at a module boundary without @inlinable |
| a closure | two words: function pointer + context | @escaping forces the context onto the heap |
throws | a register and a branch — no unwinder, no tables | almost nothing; this is not a C++ exception |
await | a possible suspension: the frame moves to a heap-allocated async frame | state after the await may have changed — see actor reentrancy |
any P, a non-final class, a cross-module generic without @inlinable
— you have bought an indirect call and possibly a box. That is the whole performance model.ExistentialAny, InferSendableFromCaptures-default-isolation MainActor6.2: every declaration in the module is main-actor isolated unless it says otherwise-strict-memory-safety6.2: warns on every use of an unsafe construct, so @unsafe becomes a visible, auditable thingA 6.3 compiler still compiles Swift 5 code: the toolchain version and the language mode move independently, and one module in a package can be in Swift 6 mode while its neighbour is not. That is the whole migration story.
Assignment of a value type copies. That would be ruinous for an Array, so the standard library’s
value types are one word pointing at a class-instance buffer, and the copy is deferred until somebody writes.
Verified with withUnsafeBufferPointer: after var b = a both share a base address;
after b.append(4) they do not.
isKnownUniquelyReferenced only sees strong references, and only works on a
final class. It returns false for an Objective-C class, so a COW type wrapping a
bridged NSObject silently copies on every mutation.| Kind | Semantics | Reach for it when |
|---|---|---|
struct | value; copied; no inheritance; deinit only via ~Copyable | the default. Data with no identity. |
enum | value; a closed set of cases with payloads | state that is one of n things — the compiler checks you handled all n |
class | reference; identity; inheritance; deinit | identity matters, or you need deinit, or Objective-C needs it |
actor | reference + an isolation domain; access is awaited | mutable state shared across concurrency domains |
final class | reference, no subclassing | every class you do not explicitly intend to be subclassed — it unlocks direct dispatch |
let class referencelet c = SomeClass()only the reference is immutable; c.x = 1 is finemutating funcrequired to write self in a struct/enum method; self is inoutnonmutating seta setter that does not write storage — how property wrappers write through to a classd["k"]?.append(1)in-place through a subscript: no temporary copy, because the accessor is _modifyfor var item in items { item.flag = true } mutates a copy and throws it away. Mutate through the
index — items[i].flag = true — or map to a new array.A struct holding a class reference has reference semantics for that part. Copy it and both copies point at the same object. This is the single most common way a "value type" turns out to be shared mutable state.
MemoryLayout<T> is the truth, and it is available at compile time. size is the bytes the
value occupies, stride is the distance between consecutive elements in an array (size rounded up to alignment),
and the two are not the same number for anything with tail padding.
| Type | size | Why |
|---|---|---|
Int, Double, AnyObject | 8 | one word on arm64/x86-64 |
Bool | 1 | stride 1 too |
Int? | 9 (stride 16) | no spare bits in an Int, so the tag needs its own byte |
Bool? | 1 | Bool has 254 spare bit patterns; the tag hides in one |
SomeClass? | 8 | nil is the null pointer — an optional reference is free |
SomeClass?? | 8 | still free: more spare pointer bits carry the second tag |
String | 16 | two words; up to 15 UTF-8 bytes live inline (small-string form) |
Substring | 32 | a String plus a range — it keeps the whole parent alive |
Array, Set, Dictionary | 8 | one pointer to a heap buffer |
() -> Void | 16 | function pointer + context pointer |
Any | 32 | 3-word buffer + type metadata |
any P | 40 | 3-word buffer + metadata + one witness table per protocol |
any P & Q | 48 | each extra protocol adds another witness-table word |
any. A struct of four
Ints in an any P is heap-allocated every time the existential is copied. The same struct
behind some P or a specialised generic costs nothing. Prefer some; reach for any
only when you genuinely need a heterogeneous collection.Declaration order is layout order, exactly as in C. Sorting members largest-first still pays. There is no
-Wpadded; MemoryLayout in a test is how you find it.
.size(ofValue:) for an existential's dynamic typewithUnsafeBytes(of: x) { … }the bytes of a value, without a heap round tripUnsafeMutableRawPointer.allocate(byteCount:alignment:)you own the deallocatep.bindMemory(to:capacity:) / assumingMemoryBound(to:)type punning: the second asserts, the first changes the bindingclass instance header16 bytes on Apple platforms: isa pointer + refcount word, before your first stored propertyOptional<T> is an ordinary two-case enum — nothing more. Everything else is sugar and a
layout optimisation.
| Form | When it is right |
|---|---|
x! | only when nil is a programmer error you want to trap on. It is a deliberate crash, and it is sometimes correct. |
if let x { … } | the shorthand since 5.7 — no x = x needed. Scoped to the branch. |
guard let x else { return } | the one to default to: unwraps for the rest of the scope, and forces the failure path to be explicit. |
x ?? default | a value is always available. Chains: a ?? b ?? c. |
x.map / .flatMap | transforming without unwrapping. map on an optional-returning body gives T?? — that is what flatMap is for. |
if case .some(let x) | when you are already in a pattern match. |
x?.foo() | the call happens only if non-nil; the result is optional even if foo returns non-optional. |
dict["missing"]?.count = 5 is a no-op, not a crash and not an insertion. Likewise
try? collapses a thrown error into nil and discards it — convenient, and the most
common way a real error disappears from a codebase.Optional<Any> holding an Optional prints "Optional(Optional(1))" and compares oddly — unwrap before boxingString(describing: opt)gives "Optional(3)"; use opt.map(String.init) ?? ""var x: T!an implicitly-unwrapped optional: still T?, just with an implicit ! at each use. Only for the two-phase init dance and imported Objective-C?? has lower precedence than ==a ?? b == c parses as a ?? (b == c) — parenthesiseflatMap on a Sequence of optionalsrenamed compactMap; the old spelling is deprecated@autoclosure, so
cache[k] ?? expensiveRecompute() only recomputes on a miss. Your own APIs can do the same.Every class instance carries a reference count in its header. The compiler inserts swift_retain and
swift_release around every place a strong reference is copied or destroyed, then the optimiser removes
as many as it can prove are redundant. The count is atomic, so the ones it cannot remove cost real cycles on
every core.
| Qualifier | Keeps alive? | On deallocation | Cost |
|---|---|---|---|
strong (default) | yes | n/a | atomic inc/dec per copy |
weak var x: T? | no | becomes nil, observably | a side-table entry; reads are not free |
unowned let x: T | no | reading it traps | cheaper than weak; still checked |
unowned(unsafe) | no | reading it is undefined | free, and a dangling pointer if you are wrong |
Rule of thumb: weak when the reference legitimately outlives the target (delegates, caches);
unowned when the target is guaranteed to outlive the reference (a child pointing at its parent). Since
Swift 6.2 weak let is allowed, so an immutable weak reference no longer has to be a var.
guard let self needs no = self[unowned self]when the closure cannot outlive self — a synchronous callback you own[weak self, capturedValue = value]a capture list can also bind an arbitrary expression at closure-creation timeTask { [weak self] in … }a Task escapes by definition; an unqualified self here is a real retain that lasts until the task ends[weak self] inside array.map { … } is noise. Reserve capture lists for
@escaping closures, stored properties and Tasks.The optimiser may release an object immediately after its last use, which can be before the end of the
enclosing scope. Code that relies on a deinit firing at a particular moment — a lock guard, an
os_signpost pair, a file handle — needs withExtendedLifetime(x) { … } or, since
Swift 5.7, an explicit defer.
Swift picks one of four dispatch mechanisms per call, statically. Knowing which is the difference between a call that inlines and one that cannot.
| Mechanism | Used for | Cost |
|---|---|---|
| Direct (static) | structs, enums, final, private, globals, anything in an extension | a call, usually inlined away |
| Vtable | non-final class methods declared in the class body | one load + indirect call |
| Witness table | protocol requirements through any P or an unspecialised generic | one load + indirect call |
objc_msgSend | @objc dynamic, and anything inherited from NSObject that stays dynamic | a message send; enables swizzling and KVO |
bye() dispatches to the protocol extension even though
G defines its own. It is not in the witness table, because it was never a requirement, so the call is
resolved from the static type. Verified above on 6.3.3. If you want an override to win, declare it in the protocol
body. The same rule applies to class extensions: a method added in an extension of a class is not in the vtable and
cannot be overridden — a subclass redeclaring it merely shadows it.-O and -Onone can differ so muchpublic / openopen is the only one subclassable outside the module; public classes are not@inlinableships the body in the module interface so callers in other modules can specialise and inline it@objc dynamicforces objc_msgSend — required for KVO, swizzling and most Objective-C runtime tricks@_dynamicReplacementunderscored, unstable; the mechanism behind SwiftUI previews' hot reloadany P has quietly converted every call into an indirect one and every value into a
possibly-boxed 40-byte container. Use some P in parameter and return position and the same design
compiles to direct calls.A Swift generic is compiled once, against a protocol witness table — unlike a C++ template, which is compiled per instantiation. The optimiser then specialises hot instantiations back into concrete code. Where it can do that, generics are free; where it cannot, every operation is an indirect call through a table and every value is manipulated through its value witnesses (copy, destroy, project).
.swiftinterface; other modules can then specialise and inline. The body becomes ABI, so you cannot change it freely@usableFromInlinemakes an internal declaration referenceable from an @inlinable body without making it public-cross-module-optimizationwhole-package specialisation without annotating each function; costs build time@_specialize(where T == Int)underscored: emit a named specialisation eagerly. Occasionally the right answer in a hot library-enable-library-evolutionthe opposite: resilient layout, everything indirect, so the module's ABI can change without recompiling clientssome versus any — the decision that matterssome P (opaque) | any P (existential) | |
|---|---|---|
| Means | one specific type, which the caller does not get to know | any type, decided at run time, boxed |
| Layout | the concrete type — nothing added | 40 bytes; heap box if the payload exceeds 3 words |
| Dispatch | static, after specialisation | witness table, always |
| Heterogeneous array | no — all elements must be the same type | yes; this is the reason it exists |
| Associated types | fine; use a primary associated type, some Collection<Int> | fine since 5.7, but the associated type is erased |
| Write it in | parameters and return types — the default | stored properties and collections that must be mixed |
Under the ExistentialAny upcoming feature (on by default in future language modes), a bare
P in type position is an error and you must write any P — precisely so that the cost is
visible in the source. Turning it on early is a cheap way to audit a codebase.
where T.Element == U for same-type constraintsextension Array where Element: Numericconditional extension — methods that exist only for some instantiationsprotocol Collection<Element>a primary associated type; enables some Collection<Int> and any Collection<Int>func f<each T>(_ v: repeat each T)parameter packs (5.9): variadic generics, type-safeT: ~Copyableopt out of the implicit Copyable constraint so the generic also accepts move-only types (6.0)associatedtype Element: Equatablea requirement on the conforming type's own choiceA conformance is a standalone protocol witness table: a list of function pointers, one per requirement,
emitted next to the type. Nothing is added to the type itself, which is why a struct, an
enum and even a tuple-shaped generic can conform as cheaply as a class, and why conformances can be added
retroactively from another module.
{ get } may be satisfied by a let, { get set } may notinit(from: Decoder)an initialiser requirement; a non-final class must mark its implementation requiredstatic func == operators are static requirements — this is how Equatable worksassociatedtype Elementthe conformer chooses a type; the protocol can constrain itsubscript(i: Int) -> Elementsubscripts are requirements too| Protocol | Synthesised when |
|---|---|
Equatable, Hashable | every stored property conforms; for an enum, every payload does. Declare the conformance and write nothing. |
Codable | every stored property is Codable; customise with a CodingKeys enum |
CaseIterable | an enum with no associated values — gives allCases |
Comparable | only for an enum with no payloads (source order is the ordering); otherwise write < |
Sendable | inferred for a frozen value type all of whose members are Sendable; never inferred for a public type in a resilient module |
RawRepresentable | an enum with a raw-value type |
Hashable stored property and
the conformance stops being synthesised — the error appears at the declaration, not at the property that caused
it. Add : Hashable to the property's type, or write hash(into:) by hand.The @retroactive spelling (6.0) exists because two modules doing this to the same pair
produce a duplicate conformance the runtime picks arbitrarily. Writing it acknowledges the hazard; the alternative is a
wrapper type, which is usually the better answer in application code.
weak and unowned work and the existential is one wordprotocol P: Sendableevery conformer must be Sendable — a common and useful refinementtypealias Both = P & Qa composition; any P & Q carries two witness tables@objc protocolObjective-C visible; may have optional requirements, which Swift protocols cannotprotocol P { static func make() -> Self }Self requirements are why some protocols cannot be used as any P at allA Swift enum is a tagged union with a real layout optimiser behind it. The tag is packed into whatever spare bit
patterns the payloads leave: enum E { case a, b, c } is one byte, and Optional<SomeClass>
is eight, because the null pointer is a spare pattern.
| Pattern | Matches |
|---|---|
case .circle(let r) | an enum case, binding its payload |
case .rect(let w, _) | _ discards; case .rect alone matches ignoring all payloads |
case let .circle(r) where r > 1 | a guarded case — the where is part of the pattern |
case 1...9 | a range, via ~= |
case (let x, 0) | a tuple pattern — switch on tuples is a real technique |
case is Cat, case let c as Cat | a type pattern; the second binds the downcast value |
case .some(let x), case let x? | an optional; the ? suffix is the shorthand |
if case .circle(let r) = s | one-case match outside a switch; guard case is the early-exit form |
for case let .circle(r) in shapes | filter-and-bind in a loop, no compactMap needed |
case .a, .b: | alternatives in one arm; every alternative must bind the same names and types |
caseswitch must be exhaustivethe compiler proves it; adding a case then breaks every switch, which is the point@unknown defaultfor a non-frozen enum from another module: handles future cases but still warns when a known one is addedfallthroughexplicit; there is no implicit fall-through and no break required@frozen enumpromises the case list will never change, letting clients switch exhaustively across a resilient boundary@unknown default is not the same as default. A plain
default silences the exhaustiveness check for ever; @unknown default keeps it, so you still
get a warning when the library you depend on adds a case you have not handled. Use it for every enum that crosses a
module boundary you do not control.Swift guarantees that no property is ever read before it is initialised, without zeroing memory. For structs that is a simple flow analysis. For classes it produces the two-phase rule, which is the source of most of the initialiser diagnostics you will meet.
| Kind | Rule |
|---|---|
| designated | initialises every property of its own class, then delegates up with super.init |
convenience | delegates across to another initialiser of the same class; can never touch super |
required | every subclass must provide it — how protocol init requirements survive inheritance |
init? failable | may return nil; the caller gets an optional. init! is the implicitly-unwrapped form |
init memberwise | synthesised for a struct with no explicit init — writing one in an extension keeps the synthesised one |
deinit | classes and actors only; runs bottom-up through the hierarchy, no explicit super call |
mutatingvar y: Int { compute() }computed — no storage; the shorthand for a get-only propertyvar z: Int { get { … } set { … } }full form; newValue is the implicit setter parameterdidSet / willSetobservers; they do not fire from inside the type's own initialiserstatic let shared = Thing()a global or static let is lazy and atomic — the correct singleton, no dispatch_once needed@Observable / @Publishedmacro and property wrapper respectively; both turn a stored property into a change notificationGlobal and static stored properties are initialised on first use via swift_once.
That makes them the one genuinely safe lazy in the language — and, in Swift 6, the one that needs
Sendable or an actor if it is a var.
A Swift throw is not a C++ exception. There is no unwinder and no table walk: the callee sets an error
register and returns, and the caller branches on it. That is why throws is nearly free on the success
path and why it composes with async without a second mechanism.
any Errorfunc f() throws(ParseError) -> Inttyped throws (6.0): the error type is in the signature, so catch is exhaustive and no existential is boxedfunc f() rethrowsthrows only if a closure argument does — how map stays non-throwing for non-throwing bodiesfunc f() throws(Never)the spelling of "does not throw"; what rethrows collapses to for a non-throwing argumenttry f()mandatory marker at every call that can throw — the error path is always visible in the sourcetry? f()→ Int?, error discardedtry! f()trap on error; a deliberate crashdefer { … }runs on every exit from the scope, in reverse order of declarationUse typed throws where the error set is genuinely closed and the caller must handle each case — a
parser, a state machine, embedded code that cannot allocate. For everything that crosses a library boundary,
any Error is still the right default: adding a case to a typed-throws signature is a source break for
every caller.
| Failure | Mechanism | Recoverable |
|---|---|---|
| expected failure | throw | yes — that is what it is for |
| programmer error | precondition, assert, fatalError, ! | no — the process dies |
| arithmetic overflow, array bounds | a trap | no. Use &+ or addingReportingOverflow to opt out |
| task cancellation | CancellationError from try Task.checkCancellation() | yes, and it is cooperative — nothing is killed for you |
| unrecoverable system state | fatalError("…") | no; the message survives into the crash log |
-O. For conditions you are checking during developmentprecondition / preconditionFailurekept in -O, removed only by -Ounchecked. For invariants that must hold in shipping codefatalErrornever removed, even by -Ounchecked; returns Never, so the compiler knows the path endsResult<Success, Failure>an error as a value; try result.get() converts back. Mostly a callback-era tool nowLocalizedError / CustomNSErrorFoundation: what makes an error present sensibly to a user or bridge to NSErrortry? in a catch-less codebase is where errors go to die. It is the
right tool when nil genuinely means "no value", and the wrong one when it means "something failed and nobody looked".
-Wunused-result has no equivalent here; only review does.A closure value is a function pointer plus a context pointer: 16 bytes. If it does not escape, the context can live in the caller’s frame and the whole thing usually inlines away. If it escapes, the context is a heap-allocated box, and every captured variable moves into it.
$0, $1, and _ to ignore{ [weak self, n = count] in … }capture list — evaluated at closure creation, not at callf { … }trailing closure; multiple trailing closures are labelled after the first (5.3)func f(_ body: () throws -> T) rethrowsthe signature that makes a helper transparent to errors| Attribute | Means |
|---|---|
@escaping | may outlive the call — required to store it. Closure parameters are non-escaping by default |
@autoclosure | wraps the argument expression in a closure at the call site, so it is not evaluated unless used. ??, assert and the && family are built on it |
@Sendable | safe to hand to another concurrency domain; captures must be Sendable |
@MainActor | the closure runs on the main actor; calling it from elsewhere requires await |
@convention(c) | a bare C function pointer — therefore it may capture nothing |
@convention(block) | an Objective-C block; capturing is fine, ARC-managed |
consuming/borrowing | ownership of the parameter — see the ownership card |
inout | pass-by-value-result: copied in, copied back on return. Not a reference, and exclusive for the duration |
inout is not a pointer, and it enforces exclusivity. Passing the same variable
to two inout parameters, or mutating a captured variable while it is inout elsewhere, is a
compile error inside a function and a runtime trap ("Simultaneous accesses") across one. Nor may an
inout argument be captured by an escaping closure — its lifetime ends when the call returns.(Type) -> (Args) -> Rxs.map(String.init)initialisers are functions toocallAsFunctiondefine it and an instance becomes callable: instance(arg)@discardableResultsuppresses the unused-result warning at the declaration, not the calloperator +++ / precedencegroupyou can declare new operators, and their precedence group, at file scopeEvery await is a potential suspension point: the function may return to the executor and resume
later, on a different thread. The frame lives on a heap-allocated async stack, not the thread stack, which is why an
async function can suspend cheaply and why thread-local state is not reliable across an await.
| Form | Parent | Inherits |
|---|---|---|
async let, TaskGroup | the enclosing task — cannot outlive its scope | priority, task-locals, isolation, cancellation |
Task { … } | unstructured, but still inherits | priority, task-locals, actor isolation of the context |
Task.detached { … } | nothing | nothing — not the priority, not the isolation. Rarely what you want |
Task.checkCancellation(), never reads Task.isCancelled
and never awaits a cancellation-aware primitive (Task.sleep, URLSession) runs to completion
regardless. A long compute loop must check the flag itself.CancellationError if cancelled — the one-liner for a loopTask.isCancelledthe flag, for when you want to return a partial result rather than throwwithTaskCancellationHandler(operation:onCancel:)bridge cancellation to something that needs an explicit stop — a socket, a C APItry await Task.sleep(for: .seconds(1))cancellation-aware; Thread.sleep is not and blocks the cooperative poolawait Task.yield()give the executor a chance — the fix for a tight async loop that starves everything else@TaskLocal static var requestIDtask-local storage; inherited by child tasks, scoped by withValueA continuation must be resumed exactly once. The checked forms trap on a double resume and log on a
leak; the unsafe variants skip the bookkeeping and simply corrupt the task. Develop with checked, and only
consider switching if a profile actually shows it.
for await loop; the continuation carries a termination handlerfor await x in seqthe AsyncSequence loop — suspends between elementsMainActor.run { … }hop to the main actor for a block, from a nonisolated contextawait withDiscardingTaskGroupa group whose child results are discarded, so it does not accumulate memory in a long-running server loopAn actor is a reference type whose mutable state may only be touched from inside its own isolation
domain. From outside, every access is awaited and hops to the actor’s executor. The compiler enforces
this statically — that is the entire mechanism, and there is no lock you can forget to take.
awaits gives up its isolation for the duration: another call can enter and mutate state before the first
resumes. Re-check your invariants after every await inside an actor — caches especially. Actors
prevent data races, not logic races.| Spelling | Means |
|---|---|
@MainActor | isolated to the main actor — a global actor whose executor is the main thread. UI code lives here |
@globalActor | declare your own process-wide actor, e.g. one serialising all database access |
nonisolated | opt a member out of its type’s isolation: callable from anywhere, may not touch isolated state |
nonisolated(unsafe) | "I have checked this by hand" — the escape hatch for a global that predates the model |
isolated a: Counter | an isolated parameter: the whole function body runs in a’s domain |
#isolation | the caller’s isolation, as a value — how a generic API stays on the caller’s actor |
assumeIsolated { } | assert at run time that we are already on this actor, and get synchronous access. Traps if wrong |
@concurrent (6.2) | the opposite of the new default: run this async function on the shared pool, off the caller’s actor |
nonisolated(nonsending) (6.2) | an async function that runs in its caller’s isolation rather than hopping off it |
Under approachable concurrency (-default-isolation MainActor, plus
NonisolatedNonsendingByDefault), a module’s declarations are main-actor isolated unless marked
otherwise, and a plain async function stays on its caller’s actor instead of hopping to the global
pool. For an app that is mostly UI with a little background work, that inverts the annotation burden: you mark the
few things that must leave the main actor with @concurrent, instead of marking everything that must stay.
unownedExecutor — how an actor can be pinned to a specific dispatch queue@preconcurrency import Foodowngrade Sendable errors from a module that has not been audited yetSendable marks a type as safe to cross an isolation boundary. In Swift 6 language mode, passing a
non-Sendable value from one domain to another is a compile error, and that single rule is the whole
of data-race safety.
| Type | Sendable? |
|---|---|
Int, String, Array of Sendable, enums with Sendable payloads | yes, inferred |
| a struct whose members are all Sendable | inferred — but not for a public type in a library-evolution module, where you must write it |
actor | always — that is the point of an actor |
final class with only immutable Sendable storage | yes, if you declare it |
| a non-final class, or any class with mutable state | no — and @unchecked is the only way to claim otherwise |
| a closure | only if @Sendable and all captures are Sendable |
Mutex; a lie everywhere else@preconcurrencyon an import, a protocol conformance or a declaration: treat the other side as un-audited and downgrade the errors to warningsnonisolated(unsafe) var cachea global that you have reasoned about by hand. Prefer a Mutex or an actorsending x(6.0) the value is transferred, not shared: the caller must not use it afterwards, so it need not be SendableMutex<T> / Atomic<T>from the Synchronization module (6.0): a real lock and real atomics, both Sendable, both non-copyableSE-0414 taught the compiler to track regions of values that can reach each other. A freshly built,
non-Sendable value that nothing else references may be sent into another domain, because the compiler can
prove the sender no longer uses it. This is why the following compiles even though Report is not
Sendable:
-strict-concurrency=complete to see the warnings without breaking the build2. fix by modulethe language mode is per-module; move one at a time and leave the rest on 53. @preconcurrency importfor dependencies you do not control, so their un-audited types stop generating noise4. push isolation upmost warnings dissolve when a type becomes @MainActor rather than when it becomes Sendable5. 6.2 defaults last-default-isolation MainActor once the module is clean; then mark the genuinely concurrent parts @concurrentBy default Swift parameters are borrowed for the duration of the call: no retain, no copy, and the caller keeps ownership. The explicit spellings exist for the cases where the default costs a retain/release pair, and for types that cannot be copied at all.
| Convention | Callee gets | Use for |
|---|---|---|
borrowing (default) | read-only access; caller keeps the value | almost everything |
consuming | ownership; the caller may not use the value afterwards | an initialiser or a builder that stores its argument — saves a retain |
inout | exclusive read-write access, written back on return | in-place mutation |
sending | ownership and the right to cross an isolation boundary | handing a fresh object to an actor |
~Copyable is a suppressed constraint, not a new kind of type. Every generic
parameter carries an implicit : Copyable; writing <T: ~Copyable> removes it, so the
generic accepts both. This is the same trick as ~Escapable and it is why the feature could be added
without splitting the language.x's lifetime here and hand the value on; using x afterwards is an errorcopy xforce a copy where the compiler would otherwise move — occasionally needed to satisfy exclusivitydiscard selfinside a consuming method of a ~Copyable type: end the value without running deinit_read / _modify accessorsunderscored coroutine accessors: yield a borrow instead of returning a copy. What makes d[k]?.append() in-place@lifetime / dependsOnthe lifetime-dependency annotations that let Span be safe; still evolvingSwift guarantees that a variable is not read while it is being modified. Within a function that is checked at compile time; across function boundaries and for class properties and globals it is checked at run time, and a violation traps with "Simultaneous accesses to 0x…, but modification requires exclusive access".
i == j; a.swapAt(i, j) is the safe form-enforce-exclusivity=uncheckedremoves the dynamic checks. Measure first; the checks are usually cheapInlineArray<N, T>(6.2) a fixed-size array stored inline — no heap buffer, no COW check, no retainSpan<T> / RawSpan(6.2) a safe, non-escapable view over contiguous memory: the bounds-checked replacement for passing around an UnsafeBufferPointerString is a collection of Characters, and a Character is an extended grapheme
cluster — what a reader calls a character, which may be many Unicode scalars and many bytes. Everything awkward
about Swift strings follows from taking that seriously.
s[i] that looked O(1) would be O(n) and every loop over
it O(n²). Index by String.Index, or work in a view whose element really is fixed-width.String.Index?s[i..<j]→ a Substring, sharing the parent's storageString(s[range])copy out — do this before storing, or the whole parent stays alives.utf8[…]the view for byte work; also withUTF8 { buf in … } for a contiguous bufferfor ch in sgrapheme-breaking as it goes; the fastest correct way to walk a stringA String is 16 bytes. A string of 15 UTF-8 bytes or fewer is stored inline in those two words
— no allocation, no refcount, no COW check. Beyond that it points at a heap buffer, or at a bridged
NSString, or at a literal in the binary. This is why short-string work in Swift is much faster than the
allocation-per-string mental model suggests.
+ in a reduce is the classic accidental O(n²)"\(x)"string interpolation is a result builder: ExpressibleByStringInterpolation lets you type-check your own#"raw \n string"#raw strings: no escapes. Extend with more #s"""multi-line"""indentation of the closing delimiter is stripped from every line== and have different utf8 counts, and a Set<String> will deduplicate them.
If you need byte identity — a hash, a protocol frame, a filename on a case-sensitive volume — compare the
utf8 views, not the strings.The protocol hierarchy is the API contract, and each step adds a guarantee: Sequence (iterate once,
possibly destructively) → Collection (multi-pass, indexed, count) →
BidirectionalCollection (walk backwards) → RandomAccessCollection (O(1) index offset).
A generic algorithm should ask for the weakest one that works.
| Operation | Array | Set / Dictionary | String |
|---|---|---|---|
| subscript by index/key | O(1) | O(1) average | O(1) by Index; there is no integer index |
| append / insert at end | amortised O(1) | O(1) average | amortised O(1) |
| insert / remove at front | O(n) | — | O(n) |
contains | O(n) | O(1) average | O(n) |
count | O(1) | O(1) | O(n) — it counts grapheme clusters |
sort | O(n log n), introsort, not stable | — | — |
| copy on assignment | O(1) until written — then O(n) | same | same |
let s = arr[2...] has startIndex == 2, so s[0] traps with
Index out of bounds — verified. Use s.first, s.startIndex, or
Array(s) when you want a fresh 0-based value. The same aliasing keeps a big array alive behind a small
slice.d[k] = v discards itDictionary(grouping:by:)the histogram/bucketing one-linerDictionary(uniqueKeysWithValues:)traps on a duplicate; the uniquingKeysWith: form takes a merge closured.keys / d.valueslazy views, not arrays; d.values is mutable in placeordering is unspecifiedand differs between runs — the hash seed is per-process. Never depend on iteration order; sort explicitlyHashable via hash(into:)feed the same fields you compare in ==, or the invariant breaks and lookups silently missinout, so building an array or dictionary does not copy each step. reduce with + doeslazyxs.lazy.filter{}.map{} fuses the passes and allocates nothing intermediate; drop the lazy and each stage builds an arraya.withUnsafeBufferPointer { }a raw view for the hot loopContiguousArray<T>an array that can never be bridged to NSArray; for classes and objc types, faster element accessa.removeAll(keepingCapacity: true)reuse the buffer across iterationsInlineArray<4, Int>(6.2) fixed size, stored inline — no heap, no uniqueness check at allSwift has three distinct metaprogramming mechanisms and they are often confused. All three are compile-time, all three are type-checked after expansion, and none of them is a text substitution.
A @resultBuilder type turns the statements of a closure into calls on itself:
buildBlock, buildOptional, buildEither, buildArray,
buildExpression, buildFinalResult. That is the whole of how SwiftUI’s
body works — an if in a view body becomes buildEither, which is also why
the two branches must produce compatible types and why a for loop needs
ForEach rather than the language’s own loop.
| Role | Expands to | Example |
|---|---|---|
@freestanding(expression) | an expression | #URL("https://…"), checked at compile time |
@freestanding(declaration) | one or more declarations | #warning-style code generation |
@attached(member) | new members inside a type | @Observable adding its storage |
@attached(memberAttribute) | attributes on existing members | marking every property observable |
@attached(peer) | declarations beside the target | generating a completion-handler twin of an async func |
@attached(accessor) | get/set on a stored property | turning storage into a computed proxy |
@attached(extension) | a conformance and its members | adding : Codable and its implementation |
names:@Observablethe Observation module's macro: replaces ObservableObject + @Published with per-property change tracking, so a SwiftUI view redraws only for the properties it readSwift, Objective-C and C share a process, an ARC runtime and a calling convention, but not a type system. The importer does the work, and knowing where it inserts a conversion is most of the debugging.
| Swift | Objective-C / C | Bridging cost |
|---|---|---|
String | NSString * | lazy: often a wrapper, but UTF-16 conversion when the encodings differ |
Array, Dictionary, Set | NSArray etc. | O(1) wrap in, O(n) verify out; ContiguousArray opts out entirely |
Int | NSInteger | free |
Int?, String? | nullable pointer | free — if the header is audited |
| an un-audited header | anything | everything imports as T!, so nil-safety is on trust |
throws | NSError ** + BOOL/nil return | the importer rewrites the last parameter away |
| a closure | a block | free; blocks are already ARC objects |
struct | C struct | free when layout-compatible; imported as-is |
__foo so you can wrap it in a nicer Swift APINS_SWIFT_SENDABLE / NS_SWIFT_UI_ACTORannotate concurrency across the bridge — how a framework declares its Sendability@objc / @objcMembersexpose to the runtime; needed for selectors, KVO and IB outlets@objc dynamicroute through objc_msgSend so it can be swizzled or observed@nonobjchide an overload from the runtime when the mapping is ambiguousUnmanaged<T>manual retain/release for a Core Foundation object across an unannotated boundaryconst T *; UnsafeMutablePointer for T *UnsafeRawPointerconst void *; OpaquePointer for a handle whose type you do not modelwithUnsafePointer(to:)a scoped C pointer to a Swift value — valid only inside the closurestrdup / freeif C allocated it, C's free must release it; Swift's deallocator is not the same allocator-cxx-interoperability-mode=defaultC++ interop (5.9): C++ classes import as Swift structs, std::string and std::vector bridge, and Swift types can be exposed back to C++withUnsafePointer is dangling the moment the closure
returns. The same applies to array.withUnsafeBufferPointer and to passing a Swift String
to a C function that stores the pointer. If the callee keeps it, you must allocate memory that you own and free it
yourself.Every unsafe construct in Swift is spelled with Unsafe in its name. That is a deliberate design
decision: the risky surface is grep-able, and since 6.2 it is compiler-checkable too.
| Type | C equivalent | Notes |
|---|---|---|
UnsafePointer<T> | const T * | typed, bound memory |
UnsafeMutablePointer<T> | T * | you own initialize/deinitialize |
UnsafeRawPointer | const void * | untyped bytes; load(as:) requires correct alignment |
UnsafeBufferPointer<T> | pointer + count | a RandomAccessCollection, so algorithms work on it |
OpaquePointer | a handle | no element type; convert with init(_:) |
Unmanaged<T> | a +0/+1 reference | takeRetainedValue vs takeUnretainedValue is the whole API |
Span<T> (6.2) | — | the safe replacement: bounds-checked, non-escapable, no lifetime hazard |
a insidep.assumingMemoryBound(to:)assert the memory is already bound to that type — no rebinding, UB if wrongp.bindMemory(to:capacity:)actually change the binding; strict aliasing rules applyunsafeBitCast(x, to:)a reinterpret between same-sized types. Almost always the wrong tool — look for a real conversion firstunsafeDowncast(o, to:)an unchecked as!; only in a profile-driven hot path-strict-memory-safety makes every use of an unsafe construct produce a warning tagged
[#StrictMemorySafety], which you silence per-use with unsafe at the expression or
@safe/@unsafe on a declaration. It is not a new restriction — it is an audit trail, so
a reviewer can see every place the language’s guarantees were opted out of. Verified: a struct with an
UnsafeMutableRawPointer member warns "has storage involving unsafe types".
-enforce-exclusivity=unchecked removes those checks — and with
them the guarantee that inout aliasing is caught rather than silently corrupting. Measure before you
reach for it.The unit of compilation is the module, and the unit of visibility is the module too. There are no headers:
the interface is derived from the source, emitted into a .swiftmodule (binary, compiler-version-specific)
or a .swiftinterface (textual, stable, what library evolution ships).
| Flag | Effect |
|---|---|
-Onone | debug. No specialisation, no devirtualisation, no ARC elision — expect 5–100× on numeric code |
-O | release. Whole-module by default in SwiftPM release builds: specialises generics, devirtualises internal classes, elides retains |
-Osize | as -O but preferring size — less inlining and specialisation |
-Ounchecked | removes precondition, bounds and overflow checks. A trap becomes undefined behaviour. Rarely worth it |
-wmo / -enable-batch-mode | whole-module vs per-file: the trade is optimisation quality against incremental build time |
-cross-module-optimization | specialise across modules in one package without annotating each function |
-enable-library-evolution | resilient ABI: layouts become opaque, everything indirects, clients need not recompile. Required for a binary framework, costly for a local one |
-Xswiftc -O to pass a flag straight throughswift test / swift runXCTest, or Swift Testing's @Test and #expect macros (5.10+)swift package resolve / updatePackage.resolved is the lockfile — commit it for an executableswiftc -emit-sil / -emit-ir / -Ssee what the optimiser did; SIL is where specialisation and ARC elision are visibleswiftc -dump-macro-expansionsthe generated source for every macroswift-demanglereal symbols read plainly: $s8dispatch1GV5helloyyF is dispatch.G.hello() -> ()swift-format / sourcekit-lspboth ship inside the toolchain — no separate install$s8dispatch1GVAA7GreeterA2aDP5helloyyFTW demangles to "protocol witness for
Greeter.hello() in conformance G : Greeter" — the TW suffix is literally
the witness-table thunk from the dispatch card, visible in nm.Swift performance work is almost never about algorithms in the loop body. It is about four things: retain/release traffic, boxing, failed specialisation and accidental copies. In that order.
| Symptom | Cause | Fix |
|---|---|---|
swift_retain/release at the top of a profile | class references copied in a hot loop; an array of classes | final; move to structs; ContiguousArray; hoist the reference out of the loop |
swift_allocObject in a loop | an existential larger than 3 words being copied, or an escaping closure per iteration | some P instead of any P; hoist the closure |
| generic code slow only when called from another module | specialisation stopped at the boundary | @inlinable, or -cross-module-optimization |
swift_dynamicCast | as? in a loop, or is over a class hierarchy | an enum, or a protocol requirement, instead of a type test |
| array operations dominating | COW copying because a second reference exists | find the stray copy; isKnownUniquelyReferenced in a debug build to confirm |
| string work dominating | count, index(offsetBy:) or + inside a loop | work in utf8; hoist indices; reserveCapacity; append not + |
| everything slow, uniformly | you profiled a -Onone build | always measure -c release. This is the single commonest mistake |
let t = ContinuousClock().measure { … } — monotonic, and the right clock for a benchmarkos_signpostmark regions so Instruments can attribute themfinal. Replace
any P with some P in parameter and return position. Build with
-c release before believing any measurement. In most Swift codebases those three account for more
than every micro-optimisation put together.Structs of trivial fields in a ContiguousArray, iterated with for and processed by
non-escaping closures, compile to essentially the same code a C loop would: the closure inlines, the bounds check
hoists, and there is no ARC at all because nothing is a reference. Numeric Swift beaten into that shape is
competitive with C, and the Span/InlineArray types added in 6.2 exist to make that shape
expressible without dropping to Unsafe pointers.
| Message | What it really means |
|---|---|
| "The compiler is unable to type-check this expression in reasonable time" | a long literal array or a chained arithmetic expression with mixed numeric types. Split it and annotate one type |
| "Escaping closure captures mutating self" | a struct method storing a closure over self. Make the type a class, or capture a copy |
| "Cannot convert Int to CGFloat" | Swift has no implicit numeric conversions, deliberately. Write CGFloat(n) |
| "Missing argument label" | argument labels are part of the name; _ in the declaration removes one |
"Type does not conform to Sendable" at a Task { } | a captured non-Sendable value. Usually the fix is isolation, not @unchecked |
| "Main actor-isolated property cannot be referenced from a nonisolated context" | you crossed an actor boundary synchronously. await, or mark the caller @MainActor |
| "Initializer requirement can only be satisfied by a required initializer" | a non-final class conforming to a protocol with an init. Add required, or make the class final |
| "Generic parameter T could not be inferred" | a return-position-only generic. Annotate the result, or pass a metatype |
| Crash | Cause |
|---|---|
| "Unexpectedly found nil while unwrapping an Optional" | a ! or an implicitly-unwrapped optional — often an unconnected IB outlet |
| "Index out of range" | an integer index into a slice, which keeps the parent’s indices. Use startIndex or copy with Array(s) |
| "Fatal error: Not enough bits to represent the passed value" | an Int→Int32 conversion. Use Int32(exactly:) or truncatingIfNeeded: |
| arithmetic overflow trap | Swift traps rather than wrapping. &+ &- &* wrap explicitly; addingReportingOverflow reports |
| "Simultaneous accesses to 0x…" | exclusivity: the same variable inout twice, or a global mutated during its own access |
| a deadlock at launch | a synchronous wait on the main thread for something that needs the main actor. There is no reentrant lock to save you |
| "Task was cancelled" that nobody handled | cancellation is cooperative — the parent scope ended while the child was still running |
| a leak, in Instruments > Leaks | a retain cycle: a closure stored on self capturing self, or two objects both strong |
any Pa struct captured by a closurethe closure gets a copy at capture time unless the capture is inout-adjacentstate after an await inside an actorreentrancy: another call may have run. Re-check invariants, do not cache across the suspensionSubstring held long-termkeeps the entire parent string alive; String(sub) to copy outlazy var from two threadsnot atomic — unlike a global or static let, which are/