Как сделать слабую ссылку на протокол в чистом Swift (без @objc)? Полное руководство с примерами
Представьте: вы пишете приложение на Swift, используете паттерн Delegate, и вдруг — бам! — ваши объекты не уничтожаются, память растёт, а приложение начинает тормозить. Знакомо? Скорее всего, вы столкнулись с retain cycle (циклом сильных ссылок). Классическая проблема, когда делегат держит сильную ссылку на владельца, а владелец — на делегата. Решение, которое приходит в голову первым — сделать ссылку на протокол слабой (weak). Но тут Swift подкидывает сюрприз: вы не можете просто так объявить weak var delegate: MyProtocol, если протокол не помечен @objc. Компилятор выдаст ошибку. Почему? И главное — как это обойти?
В этой статье мы разберём, как сделать weak protocol reference в чистом Swift без @objc. Вы узнаете, почему это вообще проблема, какие есть решения, и как писать красивый, безопасный код без утечек памяти. Поехали!
Почему weak protocol reference не работает «из коробки»?
Давайте вспомним, как работает weak в Swift. Слабая ссылка (weak) может существовать только для reference types (классов). Почему? Потому что только объекты классов имеют управление памятью через ARC (Automatic Reference Counting). Структуры (struct) и перечисления (enum) — это value types, они копируются, и для них понятие «слабой ссылки» не имеет смысла.
Когда вы пишете:
weak var delegate: MyProtocol?
Компилятор не знает, будет ли MyProtocol реализован классом, структурой или перечислением. А вдруг это структура? Тогда weak нельзя применить. Отсюда и ошибка: «'weak' may only be applied to class and class-bound protocol types».
Раньше (в Objective-C) все протоколы по умолчанию могли быть реализованы только классами, поэтому weak работал. В чистом Swift протоколы универсальны — и это прекрасно, но добавляет сложностей.
Решение №1: class-bound protocol (протокол, ограниченный классами)
Самый простой и распространённый способ — явно указать, что протокол может быть реализован только классами. Для этого используется ключевое слово AnyObject (или устаревшее class) в объявлении протокола.
Пример:
protocol MyDelegate: AnyObject {
func didSomething()
}
class ViewController {
weak var delegate: MyDelegate?
func doSomething() {
delegate?.didSomething()
}
}
class SomeClass: MyDelegate {
func didSomething() {
print("Делегат сработал!")
}
}
// Использование
let vc = ViewController()
let some = SomeClass()
vc.delegate = some
vc.doSomething() // Выведет: "Делегат сработал!"
Всё работает. MyDelegate теперь может быть реализован только классом (class), а weak — легален. Это рекомендованный Apple подход для делегатов.
Почему это хорошо?
- Простота и читаемость.
- Полная поддержка ARC.
- Нет необходимости в
@objc.
Минусы?
- Вы теряете возможность реализовать протокол структурой. Но для делегатов это обычно и не нужно.
Решение №2: Использование generic wrapper (для экзотических случаев)
Что если вы обязаны разрешить протоколу быть реализованным и классами, и структурами, но при этом хотите слабую ссылку? Такое бывает редко, но возможно. Например, вы пишете библиотеку, где пользователь может передать и класс, и структуру. Тогда на помощь приходит обёртка.
Идея: создать структуру, которая хранит слабую ссылку на объект, реализующий протокол. Для классов — weak, для структур — просто копия.
Пример:
protocol MyProtocol {
func performAction()
}
struct WeakReference<T: AnyObject> {
weak var object: T?
}
// Использование для классов
class MyClass: MyProtocol {
func performAction() {
print("Класс работает")
}
}
var ref = WeakReference<MyClass>()
let obj = MyClass()
ref.object = obj
ref.object?.performAction() // "Класс работает"
Этот подход не решает проблему напрямую для протоколов, но позволяет обернуть слабую ссылку. Для полноценного решения потребуется enum с двумя кейсами (для класса и структуры), но это уже оверхед.
Когда использовать: только если вы пишете обобщённый код и не можете ограничить протокол классами. В 99% случаев это избыточно.
Решение №3: @objc протокол (мост в Objective-C)
Если вы не против использовать @objc, это тоже вариант. Протоколы, помеченные @objc, автоматически считаются class-bound (как в старом добром Objective-C).
@objc protocol MyDelegate {
func didSomething()
}
class ViewController {
weak var delegate: MyDelegate?
// ... остальной код
}
Плюсы: работает, просто.
Минусы:
- Привязка к Objective-C runtime.
- Не все типы Swift можно использовать (например,
enumс associated values). - Меньше производительность (но для делегатов это не критично).
Мы не рекомендуем этот путь для нового кода, если нет необходимости в совместимости с Objective-C. Чистый Swift — наше всё.
Как выбрать правильный подход?
Давайте подведём итоги в виде небольшой таблицы решений:
| Ситуация | Решение | Рекомендация |
|---|---|---|
| Вы пишете делегат (самый частый случай) | Используйте protocol MyDelegate: AnyObject |
✅ Лучший выбор |
| Протокол уже есть, менять его нельзя | Используйте обёртку WeakReference или храните ссылку как Optional без weak (но следите за циклами) |
⚠️ Только если нет другого выхода |
| Нужна совместимость с Objective-C | @objc protocol |
✅ Если код смешанный
БлогиРекомендаций по книгамЧитайте также |