Майже ручне керування ARC у Swift. Модифікатори володіння та гарантії часу життя

або
Опанування withExtendedLifetime і модифікаторів володіння параметрами (borrowing та consuming) у Swift
Будьмо чесними: в епоху клікбейтних заголовків доводиться вдаватися до різних хитрощів. «Майже ручне керування ARC у Swift. Модифікатори володіння та гарантії часу життя» звучить так, ніби ми відкриваємо секрет, який Apple приховувала з 2011 року. Але якщо завдяки цьому ви читаєте далі, мети досягнуто.😄
Swift продовжує розвиватися: нові можливості поліпшують керування пам’яттю, продуктивність і зрозумілість коду. Серед них withExtendedLifetime і модифікатори володіння параметрами (borrowing та consuming). Ці інструменти дають розробникам точніший контроль над часом життя об’єктів і семантикою володіння, допомагаючи писати безпечніший та ефективніший код. У цій статті ми розглянемо спрощену й повну сигнатури withExtendedLifetime, а також практичне застосування borrowing та consuming із прикладами й поясненнями.
Як працює withExtendedLifetime
Функція withExtendedLifetime гарантує, що переданий екземпляр не буде звільнено з пам’яті до завершення виконання замикання. Це особливо корисно, коли синхронна робота залежить від того, чи залишається ресурс живим, наприклад файл або мережеве з’єднання. Функція не чекає завершення асинхронної роботи, запланованої всередині замикання.
Спрощена сигнатура (псевдокод для демонстрації)
func withExtendedLifetime<T>(_ x: T, _ body: () -> Void)Цю спрощену версію часто використовують, щоб пояснити принцип роботи. Вона приймає об’єкт x і замикання body та гарантує, що x залишатиметься живим до завершення виконання body.
Повна сигнатура (реальна реалізація)
Ось форма сигнатури withExtendedLifetime зі стандартної бібліотеки Swift 6:
public func withExtendedLifetime<T, E, Result>(
_ x: borrowing T,
_ body: () throws(E) -> Result
) throws(E) -> Result
where E : Error, T : ~Copyable, Result : ~CopyableА ось її варіант із параметром, який передається в замикання:
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Ключові деталі:
borrowing T: Означає, що функція позичає значення, не перебираючи володіння ним.throws(E): Дозволяє функції викидати помилки типу E.Result: Тип результату замикання, який також повертаєwithExtendedLifetime.T : ~Copyable: Скасовує неявну вимогу Copyable дляT, тож функція може приймати як копійовані, так і некопійовані типи. Це не вимагає, щоб тип був некопійованим.
Для простоти в прикладах ми використовуватимемо версію з псевдокоду, але повна сигнатура показує можливості й гнучкість withExtendedLifetime.
Практичний приклад: використання withExtendedLifetime
Розгляньмо ситуацію, коли ми створюємо тимчасовий ресурс і використовуємо його в асинхронній задачі. Слабке захоплення не утримує його в пам’яті, тож ресурс може бути звільнений до того, як задача його використає.
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")
}
exampleWithoutExtendedLifetime()Один із можливих варіантів виводу без withExtendedLifetime:
Resource 'TemporaryResource' created
Block completed
❌ Resource 'TemporaryResource' deallocated
Starting work with the resource
Finishing work with the resourceУ цьому варіанті виконання ресурс звільняється до того, як асинхронна задача його використає, тому повідомлення 🎯 Using resource 'TemporaryResource' відсутнє. Час виконання задачі не гарантований: під час іншого запуску вона може встигнути використати ресурс до його звільнення.
Тепер поєднаймо сильне захоплення з withExtendedLifetime:
func exampleWithExtendedLifetime() {
// Create a temporary resource
let resource = createResource()
// Keep the resource alive while the queued work waits to run
DispatchQueue.global().async { [resource] in
// Extend its lifetime through the synchronous work below
withExtendedLifetime(resource) {
print("Starting work with the resource")
resource.use()
print("Finishing work with the resource")
}
}
print("Block completed")
}
exampleWithExtendedLifetime()Один із можливих варіантів виводу з withExtendedLifetime:
Resource 'TemporaryResource' created
Block completed
Starting work with the resource
🎯 Using resource 'TemporaryResource'
Finishing work with the resource
❌ Resource 'TemporaryResource' deallocatedСильне захоплення утримує ресурс у пам’яті до виконання замикання в черзі. Усередині цього замикання withExtendedLifetime гарантує, що ресурс залишатиметься живим протягом усієї синхронної роботи, зокрема під час виведення останнього повідомлення в лог. Обгортання лише виклику, який планує задачу, не дає такої гарантії. Повідомлення «Block completed» може з’явитися в іншому місці, оскільки планування й виконання відбуваються паралельно.
Як працюють borrowing і consuming у моделі володіння Swift
Swift розвиває свою систему володіння, щоб підвищити безпеку та продуктивність. Два важливі поняття в цій сфері: borrowing і consuming для параметрів. У цій статті ми розберемо, що вони означають, як ними користуватися та що потрібно знати, аби вже сьогодні поекспериментувати з цими можливостями.
Що таке borrowing і consuming?
-
borrowingозначає тимчасовий доступ до значення без перебирання володіння ним. Код, що викликає функцію, забезпечує час життя значення на час виклику. Це може усунути retain, потрібний для передавання володіння, але не гарантує, що тіло функції не виконуватиме операцій ARC або копіювання. -
consumingозначає перебирання володіння значенням. Некопійований аргумент не можна повторно використати після його споживання. Для копійованого аргументу Swift може передати копію або посилання з додатковим retain, тож код, що викликає функцію, зможе знову використати своє початкове значення.
Разом ці модифікатори допомагають писати безпечніший та ефективніший код Swift, особливо під час роботи з ресурсами, які не слід дублювати або використовувати неналежним чином.
Вимоги до версії Swift
Ці модифікатори володіння доступні у стабільній Swift 5.9 і новіших версіях. Повні сигнатури, наведені вище, також використовують типізовані помилки та узагальнення з некопійованими типами, які з’явилися у Swift 6.0. Перевірити актуальні підтримувані версії Swift та їхні можливості можна на офіційному сайті:
swift.org/download; і, звісно, версію Swift на своєму комп’ютері можна перевірити командою термінала swift --version.
Уже використовуються в стандартній бібліотеці Swift
borrowing та consuming є стабільними можливостями мови; їх також використовують у стандартній бібліотеці Swift.
Наприклад, у тулчейнах Swift 6 сигнатура withExtendedLifetime має таку форму (обмеження узагальнених типів тут опущено):
public func withExtendedLifetime<T, E, Result>(
_ x: borrowing T,
_ body: (borrowing T) throws(E) -> Result
) throws(E) -> ResultЦе показує, як borrowing застосовують у стандартній бібліотеці, щоб уникнути зайвих операцій ARC, тоді як функція гарантує час життя значення. Ви також можете використовувати borrowing і consuming у сигнатурах власних функцій без експериментальних прапорців.
Як використовувати їх у власному коді
Щоб писати власні функції з модифікаторами borrowing або consuming:
- Використовуйте стабільний тулчейн Swift 5.9 або новішої версії для модифікаторів параметрів і наведеного нижче прикладу з некопійованим Person.
- Використовуйте Swift 6.0 або новішої версії для наведених вище сигнатур із типізованими помилками та узагальненнями з некопійованими типами.
У попередніх розробницьких тулчейнах для узагальнень із некопійованими типами використовували такий прапорець:
-enable-experimental-feature NoncopyableGenerics
У стабільній Swift 6.0 і новіших версіях цей прапорець для цих можливостей не потрібний. Він був розробницьким прапорцем для узагальнень із некопійованими типами, а не вимогою для модифікаторів параметрів borrowing і consuming.
Докладніше про ці можливості можна прочитати в обговоренні за участю розробників Swift:
- 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
Спробуйте самі: Swift-пакет у стилі Playground
Щоб вам було простіше експериментувати, я створив проєкт Swift Package Manager (SPM) зі старішою конфігурацією прапорця, наведеною нижче. У стабільному тулчейні Swift 6.0 або новішої версії цей прапорець не потрібний. Його можна клонувати з мого репозиторію на GitHub
Цей проєкт налаштовано як легкий SPM-застосунок у стилі Playground із такою конфігурацією прапорця:
swiftSettings: [
.unsafeFlags(["-enable-experimental-feature", "NoncopyableGenerics"])
]Приклад коду та вивід у консолі
struct Person: ~Copyable {
let name: String
}
// borrowing: use the value without taking ownership
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) // ✅ person remains available
sayGoodbye(person) // ✅ object is now considered "consumed"
printPersonName(person) // ❌ error: person has already been used
}
test()Діагностика компілятора:
Цей приклад навмисно не компілюється. Формулювання діагностики може відрізнятися залежно від версії компілятора:
error: 'person' used after consumeЩоб виконати коректний варіант, видаліть останній виклик printPersonName після sayGoodbye. Тоді вивід у консолі буде таким:
Hello, John Doe
Goodbye, John DoeСпроба використати person після sayGoodbye призводить до помилки компіляції ('person' used after consume), яка запобігає небезпечному повторному використанню переміщених даних.

Підсумок
borrowing та consuming є потужними модифікаторами в моделі володіння Swift, яка продовжує розвиватися.
Вони допомагають забезпечити безпечну роботу з пам’яттю, керуючи тим, як значення використовуються й передаються.
Для модифікаторів володіння й прикладу з некопійованим Person потрібна Swift 5.9 або новіша версія. Для наведених тут сигнатур із типізованими помилками та узагальненнями з некопійованими типами потрібна Swift 6.0 або новіша версія; експериментальні прапорці не потрібні.
Напишіть мені, якщо хочете, щоб я додав більше прикладів або допоміг усе налаштувати!
Додаткові матеріали
https://developer.apple.com/documentation/swift/withextendedlifetime%28_%3A_%3A%29-59dz3
https://github.com/swiftlang/swift-evolution/blob/main/proposals/0413-typed-throws.md
https://github.com/swiftlang/swift-evolution/blob/main/proposals/0427-noncopyable-generics.md