Contents
- Async inside defer
- Task cancellation shields
- Async Result Support
- More standard library types support ~Escapable and ~Copyable
- Private member variables with default values are excluded (by default) from compiler-generated initialisers
- Borrow and Mutate Accessors
- withTemporaryAllocation using OutputSpan / OutputRawSpan
- ~Sendable
- Compiler warning for silently swallowing errors inside a Task
- isTriviallyIdentical
- Hashable conformance for UnownedTaskExecutor, Dictionary.Keys, CollectionOfOne, and EmptyCollection
- Ref and MutableRef types
- Array expression trailing closures
- The demangle function
- Software Bill of Materials Generation
This is a summary of the Swift Evolution Proposals that are marked as Implemented in 6.4. Note that I have not personally verified their implementation.
Async inside defer
func f() async {
await setUp()
defer {
await performAsyncTeardown()
}
try doSomething()
}The enclosing context – the function “f” in the above example – must be async, of course. Type inference will work in closures, too; they’ll implicitly be async if they contain any awaits in defers.
// 'f' implicitly has type '() async -> ()'
let f = {
defer { await g() }
}Cautions:
- Use of async in defer adds additional suspension points to the end of the function. Though this is not much different from the existing hazard that defer introduces, in placing code obliquely at the end of the function.
- Task cancellation works like normal inside defer, so care needs to be taken that clean-up actually happens and isn’t short-circuited. Again, this is just the same as it’s always been for non-async code inside defer (though async code is more likely to check for and react to cancellation). But, it can be guarded against using…
Task cancellation shields
This allows you to hide task cancellation status within a block (and stop automatic propagation of cancellation to subtasks). This is particularly helpful for clean-up code that wants to ensure it actually runs, even if it calls general helper functions that normally do observe cancellation.
func doWork() {
while !Task.isCancelled {
let result = crunchNumbers()
await send(.latestResult, result)
}
// Clean up
await send(.goodbye)
}
func send(_ type: MessageType, _ payload: Data? = nil) {
guard !Task.isCancelled else {
return
}
…
}The ‘goodbye’ message in that example would never actually be sent, and worse that failure is completely silent (because you typically don’t want to log “Cancelled!” everywhere, as it’s very noisy in what may be a relatively common case – depending on how you & your dependencies use task cancellation).
With Swift 6.4 you can write:
func doWork() {
while !Task.isCancelled {
let result = crunchNumbers()
await send(.latestResult, result)
}
// Clean up
withTaskCancellationShield {
await send(.goodbye)
}
}
func send(_ type: MessageType, _ payload: Data? = nil) {
guard !Task.isCancelled else {
return
}
…
}send will read false from Task.isCancelled (as would any children, function- or structured-task-wise) and thus actually send the goodbye now.
Caution:
- Hiding cancellation status from arbitrary code might lead to unbounded delays in cancellation completing, because the code under the shield sees no reason to finish promptly. Cancellation usually has some degree of time-sensitivity, e.g. if a user cancelled the task – or asked the program to quit, cancelling any work in progress – then they might tolerate a few seconds of delay, but won’t be pleased if the program just “hangs” (takes so long they start questioning whether it’s actually cancelling at all).
- Since cancellation propagation to subtasks is blocked while the shield is in place, you may need to do extra work to ensure subtasks are appropriately cancelled (e.g. if you exit the cancellation shield ‘early’, you probably do want the subtasks to then cancel too – but you’ll have to explicitly cancel them yourself).
- Conversely, task cancellation shields do not block explicit cancellation of subtasks, so you must be careful wherever you have cancellation handlers or code that might show a little too much initiative in cancelling tasks.
Async Result Support
Now there’s a way to initialise a Result from an async closure, through a new initialiser. It works exactly like you’d expect:
let result = await Result {
try await asyncWork()
}More standard library types support ~Escapable and ~Copyable
As of Swift 6.4, these protocols will no longer require the conforming types to be Copyable and Escapable:
EquatableComparableHashableCustomStringConvertibleCustomDebugStringConvertibleTextOutputStreamTextOutputStreamable
Additionally, LosslessStringConvertible no longer requires Copyable, and both Optional and Result now support ~Copyable and ~Escapable elements. The latter is an especially big improvement for users of ~Copyable or ~Escapable types, given how fundamental (and nice!) Optional is to idiomatic Swift.
struct Handle: ~Copyable {
var uuid: InlineArray<16, UInt8>
}
// Now valid in Swift 6.4, so you can e.g. use Handle as the key in a Dictionary!
extension Handle: Hashable {
static func == (lhs: borrowing Handle, rhs: borrowing Handle) -> Bool {
lhs.uuid == rhs.uuid
}
func hash(into hasher: inout Hasher) {
for b in bytes {
hasher.combine(b)
}
}
}
// Now valid in Swift 6.4, so you can e.g. use Handle in log messages!
extension Handle: CustomDebugStringConvertible {
var debugDescription: String {
"Handle<\(uuid.description)>"
}
}
// As of Swift 6.4, `createHandle` can return a `Handle?` like you'd expect!
guard let h = createHandle() else {
return
}
handleCreationTimes[h] = .now
print("Created new handle: \(h)")These are essentially bug fixes – these protocols never semantically required copyability or escapability, but because the notions of being non-copyable or non-escapable came longer after they were first defined, and the way Swift makes copyability and escapability the default, they needed their definitions to actually be changed in order to accomodate. It’s unfortunate that this wasn’t done when ~Copyable and ~Escapable were introduced in Swift 5.9 and 6.2 (respectively). Alas it’s common practice for the standard library to intentionally be left out of sync with the language (in some cases it’s because other dependencies, like borrowing, had to be developed in order to facilitate the fixes – the proposal notes that many more standard library types still need similar fixes, but lack the necessary compiler support).
Private member variables with default values are excluded (by default) from compiler-generated initialisers
A mouthful to describe, but actually a simple change – the following code will now work, as of Swift 6.4, instead of failing with the error “initializer is inaccessible due to ‘private’ protection level”:
struct S {
var x: Int
var y: Int
private var z = 0
}
let s = S(x: 1, y: 2)Previously, the initialiser that the compiler implicitly generated looked like:
private init(x: Int, y: Int, z: Int = 0)Now, it looks like:
init(x: Int, y: Int)This makes it easier to use implicit initialisers by increasing the number of situations in which you can utilise them, and also makes it easier and safer to add additional private member variables later.
Caveats:
- If you were actually relying on the private member variable(s) being part of the initialiser – and didn’t mind the implicit access restriction – then this will ultimately be a regression. However, for now Swift continues to also provide the old initialiser, for backwards compatibility (but the intent is to remove it in a future Swift release). Also, it’s not foolproof – some uncommon cases will still see a source-incompatible change.
- This does not extend to
open/public/packagestructs – if you have a struct accessible at one of those levels, its implicit initialiser will continue to behave as in prior versions of Swift, i.e. it will still include all the member variables and thus be limited to the accessibility of the most restricted variable. This is intentional, though – it’s to prevent module API changes.
Borrow and Mutate Accessors
The final piece of the Prospective Vision for Accessors, this allows for more efficient accessors on structs, in a nutshell.
In older versions of Swift, a getter had to return [semantically] a copy of the value of interest. If that value was expensive to copy (e.g. a large struct) then that made the accessor slow. And it entirely precluded getters for ~Copyable types.
struct RigidWrapper<Element: ~Copyable>: ~Copyable {
var _element: Element
var element: Element {
borrow {
return _element
}
mutate {
return &_element
}
}
}Where inlining is permitted, the above code basically boils down to direct [indexed] pointer access to _element by all callers, without any safety concerns (the compiler continues to enforce correct lifetimes, access permissions, and mutual exclusion). That’s basically as efficient as it gets.
Like many of the Swift 6.4 features, this is of direct interest to only a few Swift users (e.g. authors of packages like swift-collections) or in uncommon cases (e.g. optimising the occasional hot-spot). But it will be beneficial to all Swift users by virtue of quietly faster and more efficient types in the standard library, Foundation, and other packages.
Caveats:
- These new accessor types are only supported on
structs, not classes or actors, for now. - You can only return a single variable, which must be deterministic at compile-time. So no
if xyz { return _a } else { return _b }. - The returned values must outlive the accessor call, so these accessors can’t return a local variable or a computed value or similar (unless it too is a borrowed / mutating return with appropriate lifetime). They are really only intended to provide direct, optimally-efficient access to stored properties (though more advanced usage does permit returning stored values from outside the struct).
- You can’t mutate an object while you have borrowed or mutating references from it, through these accessors. This is part of the correctness enforcement. It’s a little heavy-handed – depending on the object’s semantics, it might be perfectly safe to mutate other stored properties within it, but for now the Swift compiler doesn’t have a way to deduce that nor does the Swift language have a way to specify that. So you may need to be careful about lifetimes and order of operations. Also…
- You can’t have a ‘regular’
getaccessor and aborrowaccessor; they are mutually exclusive. This does unfortunately mean the upgrade path is complicated, as adoptingborrowaccessors can be source-incompatible and binary-incompatible.
🗒 Swift 6.2 sort of added another approach to this, yielding accessors, though even in Swift 6.4 those remain guarded behind a compiler feature flag. Additionally, they typically don’t perform as a well, nor are as simple to write (but have other uses, for cases where the accessor needs to do something after the caller has finished with the value, such as updating an index).
withTemporaryAllocation using OutputSpan / OutputRawSpan
Now there’s a safer version of the existing withUnsafeTemporaryAllocation (first introduced in Swift 5.6). It works just like you’d expect. It comes in both “raw bytes” and typed elements versions, just like its unsafe predecessor.
let result = try withTemporaryAllocation(
of: MyResult.self,
capacity: 42
) { output -> Int in
for i in 0..<capacity {
output.append(calculateResult())
}
return processResults(output.span)
}What makes this ‘safe’ is that, unlike its predecessor:
- It guarantees to correctly deinitialise the contents of the allocation (i.e. all the elements you appended). i.e. it’ll run
deinitor any other such teardown required to properly clean up elements when they’re removed (or when the temporary allocation goes away). - Per the normal
OutputSpancontract, it won’t let you write to invalid indices nor read uninitialised elements (by default – there is an unsafe version of the subscript operator that bypasses this check, if you want to live fast and dangerous).
The API is also much nicer, as OutputSpan / OutputRawSpan are much more like regular collections than the byzantine API of UnsafeMutableBufferPointer / UnsafeRawMutableBufferPointer (used by withUnsafeTemporaryAllocation).
~Sendable
Analogous to ~Copyable and ~Escapable, it’s now possible to explicitly opt out of Sendable conformance.
The primary motivation is to provide clarity – the lack of an explicit Sendable conformance no longer has to be ambiguous as to whether sendability is or isn’t intended. It can also prevent unintended sendability changes – important for stable APIs.
// Either not sendable right now, or expected to be non-sendable in future.
public class NotThreadSafe: ~Sendable {
…
}It also enables selective sendability in a class hierarchy – e.g. a ~Sendable base class with Sendable subclasses. Previously it was possible to express the semantic of non-sendable, but in a somewhat hacky way (using @unavailable) and applying irreversibly to all descendants. Now, it’s straightforward and simple:
class Base: ~Sendable {
// Some internals which are not sendable, or the contract is to not presume sendability for subclasses.
}
class Sub: Base, @unchecked Sendable {
// Overrides whatever it was that makes `Base` non-sendable, or is just a subclass which choses to be Sendable even though it's not required by the ancestor(s).
}You can also add conditional Sendability through conditional type extensions.
// Could hold anything, including non-sendable types, so not naturally sendable.
class MyContainer<T>: ~Sendable {
var contents: T
}
// But automatically Sendable if the contents are.
extension MyContainer: Sendable where T: Sendable {}Caveats:
- Explicit
~Sendableon a type declaration, that’s then overridden by an extension, sends mixed signals and may confuse readers as to the author’s intent. ~Sendablehas to be declared on the type declaration itself, not in an extension. Similar to other ~Foo declarations.- Unlike e.g.
~Escapable, sendability is not presumed on types, so there’s no need (and it’s not supported) to declare~Sendablein generics. Not-sendable is already assumed. The~Sendabledeclaration is more for humans than the compiler, to more clearly express intent (and correspondingly it has no impact on the ABI).
Compiler warning for silently swallowing errors inside a Task
Previously, code like this would compile without any complaints, even though it is likely faulty:
Task {
try boom()
}
print("Yay!")Now, that will issue a compiler warning:
warning: Unstructured throwing task was not used, which may accidentally ignore errors thrown inside the task [#NoUseUnstructuredThrowingTask]
The fix is, of course, to do something about the potential exception – either catch & handle it inside the Task, or actually do something with the returned Task instance.
🗒 As an interesting implementation detail, this is not done merely by removing @discardableResult from the declaration. That was considered but rejected because of the API pollution it creates and because it’s semantically a bit different. Instead, this is truly a bespoke compiler warning, focused solely on whether the error is handled, not whether the [non-error] result is used.
isTriviallyIdentical
The new isTriviallyIdentical(to:) method offers a guaranteed-fast way to check two objects for equality. It is essentially an optimised version of the existing equality condition provided by Equatable and the == operator. It’s useful because it guarantees O(1) time complexity, whereas == makes no formal guarantees at all (and for many types is O(N) or worse). But unlike the existing identity comparison operator === – also O(1) – it promises to do more than just tell if two objects are the exact same pointer, and it works on value types (not just reference types).
The catch is that it’s not available for all types. It’s opt-in, initially for a fairly broad but finite set of collection types in the standard library (see the complete list in the proposal).
The way it typically works, in the initial set of implementations at least, is to check if the underlying shared storage is the same, between two objects that either utilise a copy-on-write mechanic (most standard library collections, like Array, Set, etc) or are wrappers on raw memory (Unsafe*BufferPointers and *Span).
Note that there’s nothing in its definition that restricts it to just copy-on-write or raw-pointery types (e.g. most if not all Identifiable types can implement isTriviallyIdentical(to:), as just comparing IDs1).
func update(_ contacts: [Contact]) {
guard !self.cachedContacts.isTriviallyIdentical(to: contacts) else {
// `contacts` is _guaranteed_ identical (==) to our cached copy.
return
}
// `contacts` _might_ or might not be identical to our cached copy.
// We could try `==` for a more precise check, but depending on what we're doing, it might be more efficient to just do our work - e.g. a scan of the contacts checking for some criteria is O(N) and `==` will be O(N) and possibly even slower (since it may have to check _every_ contact field for equality, not merely the subset of fields we're actually interested in).
self.cachedContacts = contacts
// <expensive operation here, like scanning all the contacts>
}Note that there’s nothing preventing the same check being part of == for the types that implement this (and many likely already contain this optimisation). But the big difference is that Equatable‘s == usually has to fall back to an exhaustive recursive check, using ==, whereas isTriviallyIdentical(to:) by contract does not.
Caveats & cautions:
- There’s no new protocol defined, so it’s very difficult to make use of this new feature with generic code. This was discussed in the proposal but no satisfying reason given for the omission – the authors seem to believe that supporting generics isn’t important.
- Similarly, the authors explicitly chose not to support this transitively on common types like
Optional, intentionally punting that work to every Swift programmer. - A fallback to
==is not a good idea in some cases, so be wary of making that a habit or pattern. Generally if you actually need to know equality precisely, you should just use==.
Hashable conformance for UnownedTaskExecutor, Dictionary.Keys, CollectionOfOne, and EmptyCollection
This simply fixes some oversights in the standard library.
Note that for CollectionOfOne and EmptyCollection conformance is conditional on the Element type being Hashable. It’s unilateral for Dictionary.Keys since Dictionary always requires Key to be Hashable, and unilateral for UnownedTaskExecutor by nature.
One obvious and notable benefit of UnownedTaskExecutor‘s conformance is that, because it’s relatively common to store collections of such executors, such as a connection pool or cache, that pool or cache can be much faster. Previously the only way to check if an executor was already in such a pool was to do a linear search (yuck!):
func pickConnection(preferring executor: UnownedTaskExecutor) -> Connection {
for (e, connection) in executorConnectionList {
if e == executor { return connection }
}
return executorConnectionList.first!.connection
}…whereas now it can be a much faster constant-time lookup:
func pickConnection(preferring executor: UnownedTaskExecutor) -> Connection {
if let connection = connectionsByExecutor[executor] {
return connection
}
return connectionsByExecutor.values.first!
}Ref and MutableRef types
These new types generalise the existing patterns of borrowed and mutable references, that have previously existed in Swift in much more restricted forms (e.g. inout parameters are essentially MutableRef parameters, and Span & MutableSpan are array equivalents).
This allows for more control and therefore more precise use of borrows / references.
// Fetch the value *once*…
var entry = MutableRef(&dictionary[key, default: 0])
// ...and repeatedly modify it, without repeatedly looking it up again in the Dictionary's hash table. Much more efficient!
for value in values {
entry.value += value
}It can be combined with still-experimental lifetime annotations to enable some pretty powerful optimisations, e.g.:
struct Vec3 {
var x, y, z: Double
@_lifetime(&self)
mutating func at(index: Int) -> MutableRef<Double> {
switch index {
case 0: return MutableRef(&x)
case 1: return MutableRef(&y)
case 2: return MutableRef(&z)
default:
fatalError("out of bounds")
}
}
}Previously – and even with the new borrow & mutate accessors – this would require at least two accesses to the struct, one to fetch the value and then another to set it later. More importantly, it would not guarantee exclusive access to the relevant field in the meantime, so other code could modify the struct in-between the get and the set. The MutableRef guarantees exclusive access to the struct while it exists.
Caveats:
- Only
Escapabletypes can be used withRefandMutableRef, for now. This is due to compiler limitations that will hopefully be addressed in future.
Array expression trailing closures
The compiler now supports trailing closure syntax for Array and Dictionary. Previously it got a bit confused because it tried to interpret the trailing closure as an array or dictionary literal, or as a random unrelated and uncalled closure, requiring more verbose initialisation syntax as a workaround. Now, intuitive code like this just works:
extension Array {
init(@ArrayBuilder build: () -> [Element]) {
self = build()
}
}
let value = [String] {
"a"
"b"
if bonusItem {
"c"
}
}This is interesting mainly in the case where you use a result builder, such as ArrayBuilder as shown above. Note that ArrayBuilder is not currently part of Swift proper, or any Apple-owned packages, but its inclusion has been proposed.
The demangle function
There’s now a built-in function for demangling Swift symbol names, in the Runtime module (that’s bundled with the Swift toolchain, alongside the stdlib etc). This won’t be of direct interest to most Swift users, but for certain package developers (such as for crash reporting) this may be very helpful.
var demangledOutputSpan: OutputSpan<UTF8.CodeUnit> = ...
do {
try demangle("$sSS7cStringSSSPys4Int8VG_tcfC", into: &demangledOutputSpan)
let utf8 = try UTF8Span(validating: demangledOutputSpan.span)
let demangledString = String(copying: utf8)
print(demangledString) // "Swift.String.init(cString: Swift.UnsafePointer<Swift.Int8>) -> Swift.String"
} catch DemanglingError.truncated(let requiredBufferSize) {
// Handle truncation - need a buffer of size requiredBufferSize
} catch {
// Handle invalid symbol
}Caution:
- While the format of mangled symbol names are pretty much guaranteed to not change, because they’re part of the Swift ABI which froze circa Swift 5, the format of unmangled symbol names are not standardised and this new function explicitly reserves the right to change its output format in future.
Software Bill of Materials Generation
This adds a swift package generate-sbom command and a --sbom-spec flag to swift build, that both generate a ‘bill of materials’ – a JSON file describing all the dependencies (recursively) of the package. It can be used for auditing and to aid in build reproducibility, among other things.
You can find examples of these JSON outputs in the proposal.
The initial implementation, in Swift 6.4, is pretty spartan, and doesn’t provide much more than what you can already see in Package.resolved. But there’s a long list of potential enhancements which will make this feature more useful.
- Technically
Identifiabledoesn’t require theidproperty to be O(1), so it isn’t formally valid to conform allIdentifiables. But in practice the function ofIdentifiableis often to short-cut an otherwise expensive equality operation, and the ID is typically a simple value type (e.g. number or string), so comparison usually is O(1). ↩︎
