Avatar ·

Как сделать слабую ссылку на протокол в чистом Swift (без @objc)

Всем привет! Я изучаю Swift и пытаюсь реализовать паттерн делегата, но столкнулся с проблемой утечки памяти. У меня есть протокол: ```swift protocol MyDelegate: class { func didSomething() } class MyClass { var delegate: MyDelegate? func doSomething() { delegate?.didSomething() } } ``` Я знаю, что в Objective-C можно было использовать `weak` для протоколов, помеченных `@objc`. Но я хочу писать на чистом Swift. Когда я пытаюсь сделать: ```swift weak var delegate: MyDelegate? ``` Компилятор выдает ошибку: `'weak' may only be applied to class and class-bound protocol types`. Я прочитал, что нужно наследовать протокол от `AnyObject` или использовать `class` в определении протокола. Но я не до конца понимаю, как это работает и в чем разница. Мой главный вопрос: как правильно объявить протокол, чтобы можно было использовать `weak` для свойства делегата, и при этом избежать retain cycle? Я хочу, чтобы делегат мог быть любым типом (класс, структура, enum), но при этом ссылка на него была слабой. Возможно ли это в чистом Swift? Или обязательно нужно ограничивать протокол только классами? Буду благодарен за подробное объяснение с примерами кода. Заранее спасибо! <<>>

Почему weak работает только с классами?

В Swift weak ссылки предназначены для работы с ссылочными типами (классами), так как они используют подсчет ссылок (ARC). Структуры и перечисления — это значимые типы, они копируются при присваивании и не участвуют в ARC. Следовательно, концепция "слабой ссылки" для них не имеет смысла. Именно поэтому компилятор требует, чтобы протокол был ограничен классами.

Правильное решение: наследование от AnyObject

Начиная с Swift 4, рекомендуется использовать AnyObject вместо устаревшего ключевого слова class для ограничения протокола только ссылочными типами. Это делает код более читаемым и соответствует современным стандартам Swift.

Вот правильная реализация:

// 1. Определяем протокол, который может использовать только классы
protocol MyDelegate: AnyObject {
    func didSomething()
    func didFinishTask()
}

// 2. Класс-владелец, который хранит слабую ссылку на делегата
class TaskManager {
    weak var delegate: MyDelegate?  // ✅ Теперь работает!
    
    func startTask() {
        print("Задача начата")
        delegate?.didSomething()
    }
    
    func finishTask() {
        print("Задача завершена")
        delegate?.didFinishTask()
    }
}

// 3. Класс-делегат (должен быть классом, а не структурой)
class ViewController: MyDelegate {
    let taskManager = TaskManager()
    
    init() {
        taskManager.delegate = self  // Создаем связь
    }
    
    func didSomething() {
        print("Делегат: задача начата")
    }
    
    func didFinishTask() {
        print("Делегат: задача завершена")
    }
    
    deinit {
        print("ViewController уничтожен")
    }
}

// Использование
var vc: ViewController? = ViewController()
vc?.taskManager.startTask()  // Выведет: "Задача начата" и "Делегат: задача начата"
vc?.taskManager.finishTask() // Выведет: "Задача завершена" и "Делегат: задача завершена"
vc = nil                      // Выведет: "ViewController уничтожен" — нет утечки!

Что произойдет без weak?

Если не использовать weak, возникнет retain cycle:

class TaskManager {
    var delegate: MyDelegate?  // ❌ Сильная ссылка!
}

В этом случае TaskManager держит сильную ссылку на ViewController, а ViewController держит сильную ссылку на TaskManager (через свойство taskManager). Оба объекта никогда не будут уничтожены — это утечка памяти.

Альтернатива: замыкания вместо делегата

Если вам нужно, чтобы делегатом могла быть структура, используйте замыкания:

class TaskManager {
    var onStart: (() -> Void)?
    var onFinish: (() -> Void)?
    
    func startTask() {
        onStart?()
    }
}

Замыкания можно захватывать слабо с помощью [weak self], что решает проблему retain cycle.

Ключевые выводы:

  • Всегда наследуйте протокол делегата от AnyObject, чтобы можно было использовать weak.
  • Используйте weak var delegate в классе-владельце для избежания циклических ссылок.
  • Делегат должен быть классом, так как структуры и перечисления не поддерживают слабые ссылки.
  • Проверяйте deinit в классе-делегате, чтобы убедиться, что объекты корректно освобождаются.
<<>>

Дополнительный подход: использование Associated Object через objc_getAssociatedObject

Если вам абсолютно необходимо хранить слабую ссылку на протокол, который не ограничен классами (например, для структур), можно использовать обертку на основе Objective-C runtime. Это не "чистый" Swift, но иногда полезно:

class WeakBox: AnyObject {
    weak var value: AnyObject?
    
    init(_ value: AnyObject?) {
        self.value = value
    }
}

protocol MyDelegate {
    func didSomething()
}

class MyClass {
    private var delegateBox: WeakBox?
    
    var delegate: MyDelegate? {
        get { delegateBox?.value as? MyDelegate }
        set { delegateBox = newValue.map { WeakBox($0 as AnyObject) } }
    }
}

Этот способ работает, но имеет недостатки:

  • Требуется приведение типов через as? MyDelegate
  • Небезопасно для структур — они будут упакованы в AnyObject как объекты Objective-C
  • Структуры при присваивании копируются, что может привести к неожиданному поведению

Поэтому рекомендую придерживаться первого подхода с AnyObject и классами. Это стандартный и безопасный способ для паттерна делегата в Swift.