Как сделать слабую ссылку на протокол в чистом Swift (без @objc)
Решение: ограничение протокола только для классов
Проблема в том, что weak работает только с ссылочными типами (классами). Протокол по умолчанию может быть принят как классами, так и структурами (value types). Чтобы использовать weak, нужно явно указать, что протокол может быть принят только классами. Для этого используется ключевое слово AnyObject (или class в старом синтаксисе).
Правильный код:
protocol MyDelegate: AnyObject { // или protocol MyDelegate: class { (устаревший вариант)
func didSomething()
}
class Service {
weak var delegate: MyDelegate? // Теперь работает!
func performTask() {
delegate?.didSomething()
}
}
class ViewController: MyDelegate { // Только классы могут принять этот протокол
func didSomething() {
print("Работа выполнена!")
}
}
Почему это работает?
AnyObject— это протокол, которому соответствуют все классы. Добавляя его в список наследования вашего протокола, вы ограничиваете его использование только классами.- Компилятор теперь знает, что
delegateможет быть только экземпляром класса, а значит, к нему применима семантика ссылочных типов. - Структуры и перечисления больше не смогут принять этот протокол — если попытаетесь, получите ошибку компиляции.
Важные нюансы:
- Синтаксис: Используйте
protocol MyDelegate: AnyObject { }. Вариантprotocol MyDelegate: class { }считается устаревшим, хотя всё ещё работает. - Проверка на nil: Слабая ссылка всегда опциональна (
?), так как объект может быть деаллоцирован в любой момент. Всегда проверяйтеdelegate?.перед вызовом. - Альтернативы: Если вам нужно поддерживать и структуры, и классы, используйте замыкания или
Combineдля обратной связи. Но для делегатов это стандартное решение.
Полный пример с жизненным циклом:
protocol ServiceDelegate: AnyObject {
func serviceDidFinish()
}
class DataService {
weak var delegate: ServiceDelegate?
func start() {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.delegate?.serviceDidFinish()
}
}
}
class MainViewController: ServiceDelegate {
let service = DataService()
init() {
service.delegate = self
service.start()
}
func serviceDidFinish() {
print("Данные загружены!")
}
deinit {
print("ViewController удалён")
}
}
// Использование
var vc: MainViewController? = MainViewController()
vc = nil // Печатает "ViewController удалён" — утечки нет!
Этот паттерн широко используется в UIKit (например, UITableViewDelegate помечен как : AnyObject). Всегда делайте ваши протоколы-делегаты наследуемыми от AnyObject — это предотвратит случайные утечки памяти.
Альтернативный подход: использование замыканий вместо протокола
Если вам нужно избежать ограничения "только для классов" или вы работаете с value types (структурами), рассмотрите замену протокола на замыкание (closure). Это особенно удобно для простых callback-ов.
class Service {
var onFinish: (() -> Void)? // Замыкание — ссылочный тип
func performTask() {
// ... работа ...
onFinish?()
}
}
class ViewController {
let service = Service()
init() {
service.onFinish = { [weak self] in
// self может быть nil, если ViewController уже удалён
self?.handleFinish()
}
}
func handleFinish() {
print("Работа выполнена!")
}
}
Преимущества:
- Не нужно объявлять отдельный протокол.
- Работает с любыми типами (структуры, классы).
- Гибкость: можно передавать несколько замыканий (например,
onSuccessиonFailure).
Недостатки:
- Сложнее читать при большом количестве callback-ов.
- Нет строгой типизации, как у протокола (легко ошибиться в сигнатуре).
- Требуется явно писать
[weak self]внутри замыкания, чтобы избежать цикла.
Этот подход хорошо подходит для простых случаев. Для сложных делегатов с множеством методов (например, UITableViewDataSource) лучше использовать протокол с AnyObject.