Avatar ·

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

Всем привет! Я новичок в Swift и столкнулся с проблемой при реализации паттерна делегата. У меня есть протокол `MyDelegate` и класс `ViewController`, который должен быть делегатом для другого класса `Service`. Я хочу, чтобы свойство `delegate` в `Service` было слабой ссылкой (weak), чтобы избежать цикла сильных ссылок и утечек памяти. Вот мой код: ```swift protocol MyDelegate { func didSomething() } class Service { weak var delegate: MyDelegate? // Ошибка: 'weak' cannot be applied to non-class type 'MyDelegate' func performTask() { delegate?.didSomething() } } class ViewController: MyDelegate { func didSomething() { print("Работа выполнена!") } } ``` Xcode выдает ошибку: `'weak' cannot be applied to non-class type 'MyDelegate'`. Я понимаю, что `weak` работает только с ссылочными типами (классами), но протоколы могут быть приняты как классами, так и структурами. Как правильно сделать слабую ссылку на протокол в чистом Swift, не используя `@objc`? Везде советуют добавлять `@objc`, но я читал, что это не рекомендуется в современном Swift. Есть ли альтернатива? Заранее спасибо за помощь! <<>>

Решение: ограничение протокола только для классов

Проблема в том, что 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.