Отложенный расчёт на Swift удерживает владельца, если задача захватывает его с сильной ссылкой. Используйте слабый захват, чтобы владелец мог исчезнуть во время ожидания, или захватите независимый снимок значений, если расчёт должен завершиться без него.

Для разработчика Mac, создающего небольшой предварительный просмотр расчёта в настольном приложении, практическая задача — намеренно выбрать срок жизни объекта. Этот пример ненадолго приостанавливает выполнение, складывает искусственные значения [4, 8, 12] и форматирует итог. Всё выполняется локально в памяти.

Какой срок жизни должен быть у расчёта?

Списки захвата Swift позволяют указать, что именно замыкает удерживает. Сильный захват удерживает ссылочный экземпляр; слабая ссылка позволяет освободить его и после этого становится nil. Это задокументированное поведение языка, не зависящее от продолжительности ожидания расчёта. Автоматическое подсчёт ссылок

Сначала выберите нужный контракт, затем синтаксис захвата. В следующей таблице правила языка применены к отложенному предварительному просмотру в настольном приложении:

Желаемое поведение Стратегия захвата Что произойдёт после того, как вызывающий код освободит владельца?
Завершить работу с тем же владельцем [self] Задача сохраняет владельца доступным для своей работы.
Продолжить, только если владелец остаётся доступен [weak self] После ожидания задача проверяет, существует ли ещё владелец.
Завершить работу со значениями, выбранными при планировании [samples = self.samples] Задача удерживает захваченные значения, не удерживая этого владельца.

Для строки со снимком есть важное условие: в этом примере захватывается массив целых чисел. Выражения в списке захвата вычисляются при создании замыкания. Если вместо этого захватить экземпляр класса, будет удерживаться ссылка на этот экземпляр; независимая копия его изменяемого содержимого создана не будет. Выражения

Есть и отдельный вопрос о значениях. В вариантах с сильным и слабым захватом значения владельца считываются после задержки. В варианте со снимком используются значения на момент планирования. Если входные данные предварительного просмотра могут измениться во время ожидания, решите, какое состояние должен отражать результат.

Запустите полное локальное сравнение на Mac

Используйте macOS 13 или более поздней версии и установленную цепочку инструментов Swift 6 или новее. Перегрузка Task.sleep(for:) с указанием длительности, применённая здесь, документирована для macOS 13 и новее. Она приостанавливает задачу, не блокируя базовый поток, и выбрасывает CancellationError, если задачу отменят до окончания ожидания. sleep(for:tolerance:clock:)

Сохраните следующую полную программу в файле CaptureDemo.swift. Калькулятор, замыкания задач и демонстрация выполняются на @MainActor, что обеспечивает последовательный доступ к их состоянию. В документации Swift описаны как изоляция класса главным актором, так и явная аннотация замыкания, используемая здесь. Конкурентность

import Foundation

enum CapturePolicy: String {
    case strong, weak, snapshot
}

@MainActor
func formatTotal(_ samples: [Int]) -> String {
    let total = samples.reduce(0, +)
    let formatter = NumberFormatter()
    formatter.numberStyle = .decimal
    formatter.locale = Locale(identifier: "en_US_POSIX")
    formatter.usesGroupingSeparator = false
    formatter.maximumFractionDigits = 0

    guard let text = formatter.string(from: NSNumber(value: total)) else {
        return "Formatting unavailable"
    }
    return "Total: \(text)"
}

@MainActor
final class Calculator {
    var samples = [4, 8, 12]

    func start(_ policy: CapturePolicy) -> Task<String, Error> {
        switch policy {
        case .strong:
            return Task { @MainActor [self] in
                try await Task.sleep(for: .milliseconds(300))
                try Task.checkCancellation()
                return formatTotal(self.samples)
            }

        case .weak:
            return Task { @MainActor [weak self] in
                try await Task.sleep(for: .milliseconds(300))
                try Task.checkCancellation()
                guard let self else {
                    return "Owner unavailable"
                }
                return formatTotal(self.samples)
            }

        case .snapshot:
            return Task { @MainActor [samples = self.samples] in
                try await Task.sleep(for: .milliseconds(300))
                try Task.checkCancellation()
                return formatTotal(samples)
            }
        }
    }
}

@main
struct CaptureDemo {
    @MainActor
    static func demonstrate(
        _ policy: CapturePolicy,
        cancel: Bool = false
    ) async {
        var owner: Calculator? = Calculator()
        weak var probe = owner
        let task = owner!.start(policy)
        owner = nil

        let label = cancel ? "\(policy.rawValue), cancelled" : policy.rawValue
        print("\(label) retains owner: \(probe != nil)")

        if cancel {
            task.cancel()
        }

        do {
            let result = try await task.value
            print(result)
        } catch is CancellationError {
            print("Cancelled")
        } catch {
            print("Calculation failed: \(error)")
        }
    }

    @MainActor
    static func main() async {
        await demonstrate(.strong)
        await demonstrate(.weak)
        await demonstrate(.snapshot)
        await demonstrate(.snapshot, cancel: true)
    }
}

Для сложения целых чисел используется reduce(0, +) из стандартной библиотеки. NumberFormatter из Foundation преобразует сумму в текст для отображения; его метод string(from:) возвращает необязательную строку, поэтому программа явно сообщает об ошибке форматирования. В примере сумма равна 24. reduce(::), NumberFormatter, string(from:)

В Терминале перейдите в папку с файлом и выполните следующие команды. Целевая платформа указывает macOS 13 для архитектуры процессора вашего Mac. Параметр -parse-as-library разрешает использовать в файле явную точку входа @main. Компилятор Swift принимает исходные файлы и параметры командной строки; команда swiftc -help выводит список параметров, доступных в установленной цепочке инструментов. Руководство по командной строке Swift

swift --version
swiftc -parse-as-library -swift-version 6 -target "$(uname -m)-apple-macos13.0" CaptureDemo.swift -o CaptureDemo
./CaptureDemo

Ожидаемый вывод приложения приведён ниже. Он следует из правил захвата и отмены; в исследовательской среде для этой рукописи компилятор Swift отсутствовал, поэтому программу проверили, но не запускали.

strong retains owner: true
Total: 24
weak retains owner: false
Owner unavailable
snapshot retains owner: false
Total: 24
snapshot, cancelled retains owner: false
Cancelled

Слабый probe позволяет проверить наличие объекта, не добавляя ещё одной сильной ссылки. Каждая демонстрация удаляет собственную сильную ссылку перед ожиданием задачи. Поскольку демонстрация и замыкания задач используют один и тот же актор, только что созданное замыкание не может вклиниться в эту синхронную подготовку. Благодаря этому сравнение не зависит от догадок о том, как быстро запускается задача. Конкурентность

Почему важно, где выполняется проверка слабой ссылки?

В ветке со слабым захватом guard let self стоит после задержки. Поэтому задача может ждать, не удерживая владельца сильной ссылкой. Если владелец уже исчез, расчёт завершается с явным результатом Owner unavailable.

Если выполнить разворачивание до задержки, срок жизни изменится. Следующий фрагмент показывает замену тела замыкания в ветке со слабым захватом; это не отдельная программа:

// Inside the existing [weak self] task closure:
guard let self else {
    return "Owner unavailable"
}
try await Task.sleep(for: .milliseconds(300))
try Task.checkCancellation()
return formatTotal(self.samples)

При успешной привязке необязательного значения создаётся сильная локальная ссылка, которая снова понадобится после приостановки. Поэтому после успешной привязки владелец остаётся доступным во время ожидания. Это следует из правил Swift для сильных ссылок: слабый захват определяет, как замыкание изначально получает доступ к владельцу, а развёрнутая локальная переменная определяет, как последующий код его удерживает. Автоматическое подсчёт ссылок

Для контракта «ждать, не удерживая владельца, а затем выполнить расчёт, только если он всё ещё существует» оставьте разворачивание после ожидания и сразу после него выполните небольшой расчёт. При дополнительных точках приостановки после разворачивания нужно заново решить, каким должен быть срок жизни объекта.

Захват unowned требует, чтобы ссылочный экземпляр оставался действительным при каждом обращении к нему. Он не обеспечивает проверку отсутствия, нужную в этом примере, где владелец намеренно освобождается во время задержки. Автоматическое подсчёт ссылок

Останавливает ли расчёт освобождение дескриптора задачи?

Задача продолжает выполняться, даже если её дескриптор отброшен. Сохранённый дескриптор позволяет дождаться результата или запросить отмену. Apple также документирует, что после завершения задача незамедлительно освобождает замыкание операции и удерживаемые им ссылки. Поэтому конечный по времени сильный захват продлевает срок жизни владельца на время работы, но не обязательно создаёт постоянный цикл ссылок. Task

Отмена и владение — разные контракты. В последней демонстрации через дескриптор запрашивается отмена. Выбрасывающий ошибку вызов ожидания и последующий Task.checkCancellation() дают операции возможность остановиться, а вызывающий код сообщает об этом результате. Отмена в Swift кооперативная: её запрос не прерывает принудительно произвольный код расчёта. Task

Если при отмене результат не должен выдаваться, не заменяйте ожидание с выбрасыванием ошибки на try?, продолжая затем выполнение без условий. Подавление этой ошибки позволит перейти к расчёту, если его не остановит другая проверка отмены. Аналогично, присваивание nil переменной владельца не является запросом отмены.

Для предварительного просмотра в настольном приложении это даёт три полезных средства управления: остаётся ли владелец выделенным в памяти, какие входные значения используются в расчёте и следует ли останавливать операцию. Выбирайте каждое из них явно, не ожидая, что слабый захват решит все три задачи.

Что изменить для предварительного просмотра в настольном приложении?

Используйте слабый вариант, если ожидающий предварительный просмотр имеет смысл, только пока его владелец доступен. Используйте вариант со снимком, если выбранные искусственные входные данные должны дать результат независимо от владельца. Используйте сильный вариант, если для обоснованного завершения операции необходим владелец.

Срок жизни объекта также отличается от видимости интерфейса. Закрытие окна само по себе не доказывает, что все сильные ссылки на его калькулятор удалены. Если при закрытии предварительного просмотра приложение должно остановить ожидающую работу, свяжите это событие приложения с методом cancel() сохранённого дескриптора задачи; расчёт при этом всё равно должен поддерживать отмену. Task

Оставьте нагрузку в этом примере небольшой. Изоляция главным актором упрощает доступ к состоянию, а ожидание приостанавливает выполнение, но синхронное суммирование всё равно выполняется на этом акторе. В руководстве по конкурентности Swift объясняется, что главный актор также обрабатывает работу интерфейса. Для существенно более крупного расчёта нужна отдельная схема выполнения, передающая рабочему потоку подходящие значения. Конкурентность

Для этого локального расчёта решающая проверка проста: удалите ссылку вызывающего кода до запуска отложенной операции и посмотрите на результат. Сильный захват сохраняет владельца, слабый позволяет получить явный результат о его отсутствии, а снимок массива целых чисел сохраняет только входные данные, необходимые для завершения.

Источники