Almost Manual ARC in Swift. Ownership Modifiers and Lifetime Guarantees
or
Mastering withExtendedLifetime
and Parameter Ownership Modifiers (borrowing
and consuming
) in Swift
Let’s be honest — in the age of clickbait titles, you’ve got to do what you’ve got to do. “Almost Manual ARC in Swift: Ownership Modifiers and Lifetime Guarantees” sounds like we’re unlocking a secret Apple has been hiding since 2011. But hey, if it gets you to scroll — mission accomplished.😄
Swift continues to evolve, introducing features that enhance memory management, performance, and code clarity. Among these are withExtendedLifetime
and parameter ownership modifiers (borrowing
and consuming
). These tools provide developers with fine-grained control over object lifetimes and ownership semantics, enabling safer and more efficient code. In this article, we’ll explore both the simplified and full signatures of withExtendedLifetime
, as well as the practical use of borrowing
and consuming
, with examples and explanations.
Understanding withExtendedLifetime
The withExtendedLifetime
function ensures that a given instance is not deallocated before the execution of a closure completes. This is particularly useful for managing resources like files, network connections, or asynchronous tasks where premature deallocation could lead to runtime errors.
Simplified Signature (Pseudo-code for Demonstration)
func withExtendedLifetime<T>(_ x: T, _ body: () -> Void)
This simplified version is often used to demonstrate the concept. It takes an object x and a closure body, ensuring that x remains alive until body finishes executing.
Full Signature (Real Implementation)
Here’s the actual signature of withExtendedLifetime from Swift’s standard library:
public func withExtendedLifetime<T, E, Result>(
_ x: borrowing T,
_ body: () throws(E) -> Result
) throws(E) -> Result
where E : Error, T : ~Copyable, Result : ~Copyable
And its variant with a parameter passed to the closure:
public func withExtendedLifetime<T, E, Result>(
_ x: borrowing T,
_ body: (borrowing T) throws(E) -> Result
) throws(E) -> Result
where E : Error, T : ~Copyable, Result : ~Copyable
Key Details:
borrowing T
: Indicates that the function borrows the value without taking ownership.throws(E)
: Allows the function to throw errors of type E.Result
: The return type of the closure, which is also returned bywithExtendedLifetime
.T : ~Copyable
: Ensures that the typeT
is not trivially copyable, aligning with Swift’s ownership model.
For simplicity, we’ll use the pseudo-code version in examples, but the full signature demonstrates the power and flexibility of withExtendedLifetime
.
Practical Example: Using withExtendedLifetime
Let’s consider a scenario where we create a temporary resource and use it in an asynchronous task. Without withExtendedLifetime
, the resource might be deallocated prematurely.
import Foundation
import Combine
final class Resource: Sendable {
let name: String
init(name: String) {
self.name = name
print("Resource '\(name)' created")
}
deinit {
print("❌ Resource '\(name)' deallocated")
}
func use() {
print("🎯 Using resource '\(name)'")
}
}
func createResource() -> Resource {
return Resource(name: "TemporaryResource")
}
func exampleWithoutExtendedLifetime() {
// Create a temporary resource
let resource = createResource()
// Use the resource outside the block
DispatchQueue.global().async { [weak resource] in
print("Starting work with the resource")
resource?.use()
print("Finishing work with the resource")
}
print("Block completed")
}
exampleWithExtendedLifetime()
Output Without withExtendedLifetime
:
Resource 'TemporaryResource' created
Block completed
❌ Resource 'TemporaryResource' deallocated
Starting work with the resource
Finishing work with the resource
Here, the resource is deallocated before the asynchronous task completes, leading to potential issue (As result we can’t find 🎯 Using resource 'TemporaryResource'
there).
Now, let’s fix this using withExtendedLifetime
:
func exampleWithExtendedLifetime() {
// Create a temporary resource
let resource = createResource()
// Extend the lifetime of the resource
withExtendedLifetime(resource) {
DispatchQueue.global().async { [weak resource] in
print("Starting work with the resource")
resource?.use()
print("Finishing work with the resource")
}
}
print("Block completed")
}
exampleWithExtendedLifetime()
Output With withExtendedLifetime
:
Resource 'TemporaryResource' created
Starting work with the resource
🎯 Using resource 'TemporaryResource'
Finishing work with the resource
Finishing work with the resource
Block completed
❌ Resource 'TemporaryResource' deallocated
In this case, the resource remains alive until the asynchronous task completes, ensuring safe usage.
Understanding borrowing and consuming in Swift Ownership Model
Swift is evolving its ownership system to improve safety and performance. Two important concepts in this area are borrowing and consuming parameters. In this article, we’ll explain what they mean, how to use them, and what you need to know to experiment with these features today.
What Are borrowing and consuming?
-
borrowing
means temporarily accessing a value without increasing its reference count (no ARC retain). It’s a lightweight way to read data without affecting the lifetime of the object. -
consuming
means taking ownership of a value — you “consume” it, so it can no longer be used after the call. This prevents accidental reuse and enforces safer memory management.
Together, these modifiers help write safer and more efficient Swift code, especially when working with resources that should not be duplicated or misused.
Swift Version Requirement
These ownership modifiers are available starting from Swift 5.9+ and are actively being developed for future Swift versions. You can check the latest supported Swift versions and features at the official Swift website:
https://swift.org/download/; and of course, you can check Swift version on your machine via swift --version
terminal command.
Already in Use in the Swift Standard Library
Although borrowing
and consuming
are still experimental language features, they are already used in Swift’s Standard Library.
For example, the signature of withExtendedLifetime
is defined internally like this (as of Swift 5.9+ toolchains):
public func withExtendedLifetime<T, E, Result>(
_ x: borrowing T,
_ body: (borrowing T) throws(E) -> Result
) throws(E) -> Result
This shows how borrowing
is leveraged inside the standard library to avoid unnecessary ARC operations and to make guarantees about lifetimes. However, you can’t yet use borrowing or consuming in your own function signatures unless you opt into experimental language features.
How to Use Them Yourself (Experimental)
To write your own functions with borrowing or consuming modifiers, you must:
- Use Swift 5.9 or later with nightly or development toolchains.
- Explicitly enable experimental features using the compiler flag:
-enable-experimental-feature NoncopyableGenerics
This means they are not yet part of stable Swift releases and require nightly or development toolchains.
For more detailed info about these features, see this discussion by Swift contributors:
- https://forums.swift.org/t/noncopyable-generics-in-swift-a-code-walkthrough/70862
- https://github.com/swiftlang/swift-evolution/blob/main/proposals/0377-parameter-ownership-modifiers.md
Try It Yourself: Swift Package Playground
To help you experiment easily, I created a Swift Package Manager (SPM) project with these flags enabled. You can clone it from my GitHub repository
This project is set up as a lightweight Playground-style SPM app with the required flags in place:
swiftSettings: [
.unsafeFlags(["-enable-experimental-feature", "NoncopyableGenerics"])
]
Example Code and Console Output
struct Person: ~Copyable {
let name: String
}
// borrowing: read-only, no ARC retain
func printPersonName(_ person: borrowing Person) {
print("Hello, \(person.name)")
}
// consuming: we take the object, it can no longer be used afterwards
func sayGoodbye(_ person: consuming Person) {
print("Goodbye, \(person.name)")
}
func test() {
let person = Person(name: "John Doe")
printPersonName(person) // ✅ no ARC retain
sayGoodbye(person) // ✅ object is now considered "consumed"
printPersonName(person) // ❌ error: person has already been used
}
test()
Console Output:
Hello, John Doe
Hello, John Doe
Attempting to use person after sayGoodbye causes a compile-time error ('person' used after consume
), preventing unsafe reuse of moved data.
Summary
borrowing
and consuming
are powerful modifiers in Swift’s evolving ownership model.
They help manage memory safety by controlling how values are accessed and transferred.
Currently, these features require Swift 5.9+ and enabling experimental flags.
Feel free to reach out if you want me to add more examples or help set this up!