Avatar ·

Как объявить массив слабых ссылок (weak references) в Swift?

Всем привет! Я новичок в Swift и пытаюсь разобраться с управлением памятью. У меня есть класс `Delegate`, который используется как делегат для нескольких объектов. Я хочу хранить список этих делегатов в массиве, но не хочу, чтобы массив удерживал их сильной ссылкой, иначе возникнут утечки памяти или циклические ссылки. В Objective-C я использовал `NSPointerArray` с опцией `NSPointerFunctionsWeakMemory`. Но в Swift я не могу просто написать `weak var delegates: [Delegate]` — компилятор выдаёт ошибку: `'weak' may only be applied to class and class-bound protocol types, not array`. Вот пример моего кода: ```swift class Delegate { weak var owner: SomeClass? func didSomething() { } } class SomeClass { // Как сделать этот массив слабым? var delegates: [Delegate] = [] func addDelegate(_ delegate: Delegate) { delegates.append(delegate) } } ``` Я читал про `NSPointerArray` и `NSHashTable`, но они работают с `AnyObject` и кажутся неудобными. Есть ли более идиоматичный способ в Swift? Может быть, есть какой-то встроенный тип для слабых ссылок в коллекциях? Буду благодарен за примеры кода и объяснение, как правильно организовать такое хранение без риска утечек памяти. Заранее спасибо! <<>>

Проблема и её корень

В Swift weak может применяться только к опциональным переменным ссылочного типа (классы или class-bound протоколы). Массив — это структура, поэтому напрямую объявить weak var array: [SomeClass] нельзя. Однако есть несколько элегантных решений.

Решение 1: Обёртка WeakRef (самый популярный подход)

Создаём generic-структуру, которая хранит слабую ссылку:

class Delegate {
    var name: String
    init(name: String) { self.name = name }
    deinit { print("\(name) deallocated") }
}

struct WeakRef<T: AnyObject> {
    weak var value: T?
    init(_ value: T) { self.value = value }
}

var delegates = [WeakRef<Delegate>]()
let d1 = Delegate(name: "First")
delegates.append(WeakRef(d1))

// Когда d1 будет освобождён, его ссылка в массиве станет nil
print(delegates.compactMap { $0.value }) // [d1] пока жив

Чтобы удобно очищать массив от устаревших ссылок, добавьте метод:

extension Array where Element: WeakRef<AnyObject> {
    mutating func compact() {
        self = self.filter { $0.value != nil }
    }
}

Решение 2: Использование NSMutableArray с NSPointerFunctions

Если вам нужно взаимодействие с Objective-C или просто знакомый инструмент:

import Foundation

let weakArray = NSMutableArray()
let pointerFunctions = NSPointerFunctions.weakObjects()
let pointerArray = NSPointerArray(options: pointerFunctions)

// Добавление объектов
let obj = NSObject()
let ptr = Unmanaged.passUnretained(obj).toOpaque()
pointerArray.addPointer(ptr)

// Чтение
pointerArray.allObjects // вернёт только живые объекты

Но этот способ менее типобезопасен и требует ручного управления указателями.

Решение 3: NSHashTable (для уникальности)

Если вам нужен набор слабых ссылок без дубликатов:

let weakSet = NSHashTable<AnyObject>(options: .weakMemory)
weakSet.add(d1)
weakSet.allObjects // [d1] или пустой после освобождения

Рекомендации

  • Для чистого Swift используйте WeakRef — это идиоматично и безопасно.
  • Для коллекций с уникальными элементами — NSHashTable.
  • Для больших массивов периодически вызывайте compact() для удаления nil-элементов.
  • Не забывайте, что слабые ссылки могут стать nil в любой момент, поэтому всегда проверяйте через optional binding.

Пример полного класса с автоочисткой:

class WeakDelegates<T: AnyObject> {
    private var storage = [WeakRef<T>]()
    
    func add(_ delegate: T) {
        storage.append(WeakRef(delegate))
    }
    
    var alive: [T] {
        storage = storage.filter { $0.value != nil }
        return storage.compactMap { $0.value }
    }
}
<<>>

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

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

class SomeClass {
    private var observers = [() -> Void]()
    
    func addObserver(owner: AnyObject, closure: @escaping () -> Void) {
        // Используем слабую ссылку на owner внутри замыкания
        observers.append { [weak owner] in
            guard owner != nil else { return } // если owner умер, не вызываем
            closure()
        }
    }
    
    func notify() {
        // Очищаем мёртвые замыкания перед вызовом
        observers = observers.filter { $0() != nil } // хитрый трюк, но лучше так:
        observers = observers.filter { /* проверка */ true }
        observers.forEach { $0() }
    }
}

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

Для простых callback'ов (например, обновление UI) это отличная альтернатива классическому паттерну делегата. Минус — сложность отладки и невозможность проверить тип делегата.