如何在Swift中声明一个弱引用数组?

234

我希望在Swift中存储弱引用数组。这个数组本身不应该是弱引用,而它的元素应该是。我认为Cocoa的NSPointerArray提供了一个非类型安全版本。


1
假如没有更好的答案,那么可以考虑创建一个弱引用另一个对象的容器对象,然后创建一个该类型对象的数组。 - nielsbot
1
为什么不使用NSPointerArray? - Bastian
2
好的,我更喜欢带参数类型的东西。我想我可以在 NSPointerArray 周围创建一个带参数的包装器,但是想看看是否有其他选择。 - Bill
你尝试过 Array<weak AnyObject> 吗? - Cy-4AH
6
另一种选择是使用NSHashTable。它基本上是一个NSSet,但允许您指定它如何引用其中包含的对象。 - Mick MacCallum
显示剩余3条评论
18个回答

191
创建一个通用的包装器,如下所示:
class Weak<T: AnyObject> {
  weak var value : T?
  init (value: T) {
    self.value = value
  }
}

将此类的实例添加到您的数组中。
class Stuff {}
var weakly : [Weak<Stuff>] = [Weak(value: Stuff()), Weak(value: Stuff())]

在定义Weak时,可以使用structclass

另外,为了帮助收割数组内容,您可以采取以下措施:

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

使用AnyObject应该替换为T,但我认为当前的Swift语言不允许定义这样的扩展。

19
当值被释放时,如何从数组中移除包装对象? - Sulthan
10
是的,它导致编译器崩溃。 - GoZoner
2
@GoZoner 如果你需要 count 呢? - Sulthan
6
请在一个新问题中发布您的问题代码;如果问题可能是由于您的代码引起的,没有理由扣分我的答案!请注意,此处的“ding”为口语用语,意为扣分或责备。 - GoZoner
2
@EdGamble 提供的代码在原样下能够工作,但是如果你将 Stuff 类替换成协议,则会失败;请参考这个相关问题 - Theo
显示剩余12条评论

85
您可以使用带有weakObjectsHashTable的NSHashTable。NSHashTable<ObjectType>.weakObjectsHashTable()

对于Swift 3:NSHashTable<ObjectType>.weakObjects()

NSHashTable类参考
 

在OS X v10.5及更高版本中可用。

   

iOS 6.0及更高版本可用。


2
这很聪明,但像GoZoner的答案一样,它不能处理类型为“Any”但不是“AnyObject”的情况,例如协议。 - Aaron Brager
@SteveWilford 但是协议可以由类实现,这将使其成为引用类型。 - Aaron Brager
4
协议可以扩展类,然后您可以将其用作弱引用(例如,protocol MyProtocol: class)。 - Yasmin Tiomkin
8
当我使用MyProtocol: classNSHashTable<MyProtocol>.weakObjects()时,编译器报错了。错误信息为“'NSHashTable'要求'MyProtocol'是一个类类型”。 - Greg
1
需要考虑的唯一一件事是:NSHashTable.count 方法存在一些缓存问题,因此将对象设置为 nil 并立即调用 count 会给出错误的值。通常在代码库中不是问题,但单元测试会因此失败。 - XmasRights
显示剩余2条评论

46

一种函数式编程方法

无需额外的类。

只需定义一个闭包数组() -> Foo?并使用[weak foo]以弱引用方式捕获foo实例即可。

let foo = Foo()

var foos = [() -> Foo?]()
foos.append({ [weak foo] in return foo })

foos.forEach { $0()?.doSomething() }

非常好。我认为你失去了compactMap,需要使用filter { $0() != nil } - MH175
2
如果您想要更方便/描述性的语法,您也可以声明一个类型别名 WeakArray<T> = [() -> T?] - MH175

17

虽然现在已经有点晚了,但你可以试试我的聚会。我将其实现为一个Set而不是一个Array。

WeakObjectSet

class WeakObject<T: AnyObject>: Equatable, Hashable {
    weak var object: T?
    init(object: T) {
        self.object = object
    }

    var hashValue: Int {
        if let object = self.object { return unsafeAddressOf(object).hashValue }
        else { return 0 }
    }
}

func == <T> (lhs: WeakObject<T>, rhs: WeakObject<T>) -> Bool {
    return lhs.object === rhs.object
}


class WeakObjectSet<T: AnyObject> {
    var objects: Set<WeakObject<T>>

    init() {
        self.objects = Set<WeakObject<T>>([])
    }

    init(objects: [T]) {
        self.objects = Set<WeakObject<T>>(objects.map { WeakObject(object: $0) })
    }

    var allObjects: [T] {
        return objects.flatMap { $0.object }
    }

    func contains(object: T) -> Bool {
        return self.objects.contains(WeakObject(object: object))
    }

    func addObject(object: T) {
        self.objects.unionInPlace([WeakObject(object: object)])
    }

    func addObjects(objects: [T]) {
        self.objects.unionInPlace(objects.map { WeakObject(object: $0) })
    }
}

使用方法

var alice: NSString? = "Alice"
var bob: NSString? = "Bob"
var cathline: NSString? = "Cathline"

var persons = WeakObjectSet<NSString>()
persons.addObject(bob!)
print(persons.allObjects) // [Bob]

persons.addObject(bob!)
print(persons.allObjects) // [Bob]

persons.addObjects([alice!, cathline!])
print(persons.allObjects) // [Alice, Cathline, Bob]

alice = nil
print(persons.allObjects) // [Cathline, Bob]

bob = nil
print(persons.allObjects) // [Cathline]

请注意,WeakObjectSet不接受String类型,而是需要使用NSString。因为String类型不是AnyType。 我的Swift版本是Apple Swift version 2.2 (swiftlang-703.0.18.1 clang-703.0.29)。可以从Gist中获取代码。 https://gist.github.com/codelynx/30d3c42a833321f17d39 ** 于2017年11月添加
我已将代码更新为Swift 4。
// Swift 4, Xcode Version 9.1 (9B55)

class WeakObject<T: AnyObject>: Equatable, Hashable {
    weak var object: T?
    init(object: T) {
        self.object = object
    }

    var hashValue: Int {
        if var object = object { return UnsafeMutablePointer<T>(&object).hashValue }
        return 0
    }

    static func == (lhs: WeakObject<T>, rhs: WeakObject<T>) -> Bool {
        return lhs.object === rhs.object
    }
}

class WeakObjectSet<T: AnyObject> {
    var objects: Set<WeakObject<T>>

    init() {
        self.objects = Set<WeakObject<T>>([])
    }

    init(objects: [T]) {
        self.objects = Set<WeakObject<T>>(objects.map { WeakObject(object: $0) })
    }

    var allObjects: [T] {
        return objects.flatMap { $0.object }
    }

    func contains(_ object: T) -> Bool {
        return self.objects.contains(WeakObject(object: object))
    }

    func addObject(_ object: T) {
        self.objects.formUnion([WeakObject(object: object)])
    }

    func addObjects(_ objects: [T]) {
        self.objects.formUnion(objects.map { WeakObject(object: $0) })
    }
}

正如gokeji所提到的那样,根据使用中的代码,我发现NSString不会被释放。

我思考了一下,并编写了以下的MyString类。

// typealias MyString = NSString
class MyString: CustomStringConvertible {
    var string: String
    init(string: String) {
        self.string = string
    }
    deinit {
        print("relasing: \(string)")
    }
    var description: String {
        return self.string
    }
}

然后像这样将NSString替换为MyString。 奇怪的是,它能够运行。

var alice: MyString? = MyString(string: "Alice")
var bob: MyString? = MyString(string: "Bob")
var cathline: MyString? = MyString(string: "Cathline")

var persons = WeakObjectSet<MyString>()

persons.addObject(bob!)
print(persons.allObjects) // [Bob]

persons.addObject(bob!)
print(persons.allObjects) // [Bob]

persons.addObjects([alice!, cathline!])
print(persons.allObjects) // [Alice, Cathline, Bob]

alice = nil
print(persons.allObjects) // [Cathline, Bob]

bob = nil
print(persons.allObjects) // [Cathline]

然后我发现了一个奇怪的页面,可能与这个问题有关。

弱引用保留已释放的NSString(仅限XC9 + iOS模拟器)

https://bugs.swift.org/browse/SR-5511

它说这个问题已经被解决了,但我想知道这是否仍然与这个问题有关。 无论如何,MyString或NSString之间的行为差异超出了这个范围,但如果有人能解决这个问题,我会非常感激。


1
我已将代码更新为Swift4,请查看答案的后半部分。似乎NSString存在一些释放问题,但它仍应该适用于您的自定义类对象。 - Kaz Yoshikawa
非常感谢您查看并更新答案,@KazYoshikawa!我后来也意识到自定义类可以工作,而 NSString 则不行。 - gokeji
非常感谢您提供这个答案。我尝试使用它,在运行内存泄漏仪器后,我发现一些泄漏来自于 WeakObjectSet 类。我的使用方式是对诸如 UITableViewsUICollectionViews 之类的 UIKit 组件进行弱引用。这些组件实现了一个名为 A 的协议,在我的类中,我声明了一个弱集合,像这样:var set = WeakObjectSet<A>()。当 ViewController 取消显示并且其视图被移除时,set 的表现是正确的,但是内存工具显示在 specialized WeakObject.init(object:) 处发生了泄漏。 - Guy Daher
2
我有这样的经验,即由UnsafeMutablePointer<T>(&object)返回的指针可能会随机更改(与withUnsafePointer相同)。 我现在使用由NSHashTable支持的版本:https://gist.github.com/simonseyer/cf73e733355501405982042f760d2a7d。 - simonseyer
我意识到在解除分配后,hashValue的值会发生变化。我已经更新了最近Xcode和Swift的WeakObjectSet代码。WeakObjectSet.swift - Swift 5 https://gist.github.com/codelynx/73919293f4166edd767f77a4cd178274 - Kaz Yoshikawa
显示剩余7条评论

14
这不是我的解决方案。 我在苹果开发者论坛上找到了它
@GoZoner提供了一个很好的答案,但它会使Swift编译器崩溃。
这里有一个版本的弱对象容器,不会使当前发布的编译器崩溃。
struct WeakContainer<T where T: AnyObject> {
    weak var _value : T?

    init (value: T) {
        _value = value
    }

    func get() -> T? {
        return _value
    }
}

您可以创建这些容器的数组:
let myArray: Array<WeakContainer<MyClass>> = [myObject1, myObject2]

1
很奇怪,但不再适用于结构体。对我来说会显示“EXC_BAD_ACCESS”。使用类就能正常运行。 - mente
7
结构体是值类型,它们不应该与之一起使用。崩溃发生在运行时而不是编译时,这是编译器的一个错误。 - David Goodine

11
如何使用函数式风格的包装器?
class Class1 {}

func captureWeakly<T> (_ target:T) -> (() -> T?) where T: AnyObject {
    return { [weak target] in
        return target
    }
}

let obj1 = Class1()
let obj2 = Class1()
let obj3 = Class1()
let captured1 = captureWeakly(obj1)
let captured2 = captureWeakly(obj2)
let captured3 = captureWeakly(obj3)

只需调用返回的闭包以检查目标是否仍然存在。

let isAlive = captured1() != nil
let theValue = captured1()!

您可以将这些闭包存储到数组中。

let array1 = Array<() -> (Class1?)>([captured1, captured2, captured3])

你可以通过调用闭包来检索弱捕获的值。

let values = Array(array1.map({ $0() }))

其实,你不需要一个函数来创建闭包。直接捕获一个对象即可。
let captured3 = { [weak obj3] in return obj3 }

3
如何创建一个弱对象数组(或称为集合)是问题所在。 - David H
通过这个解决方案,你甚至可以创建一个包含多个值的数组,例如 var array: [(x: Int, y: () -> T?)]。这正是我一直在寻找的。 - jboi
1
@DavidH 我更新了我的答案来回答这个问题。希望这可以帮到你。 - eonil
我_喜欢_这种方法,我认为它非常聪明。我使用这个策略做了一个类的实现。谢谢! - Ale Ravasio
对于 let values = Array(array1.map({ $0() })) 部分我不是很确定。由于这不再是一个带有弱引用的闭包数组,所以只有在该数组被释放时,values 才会被保留。如果我的理解是正确的,那么需要注意的是,你永远不应该像 self.items = Array(array1.map({ $0() })) 这样保留该数组,因为这会打破它的目的。 - Matic Oblak

10

您可以创建一个包装对象来保存一个弱指针,来实现该功能。

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

然后将它们用在数组中
var weakThings = WeakThing<Foo>[]()

必须使用“weak”变量的“class”。 - Bill
3
谁说的?上面的代码对我来说很好用。唯一的要求是成为弱引用的对象必须是一个类,而不是持有弱引用的对象。 - Joshua Weinberg
5
如果 Foo 是一个协议,该怎么办? - onmyway133
据我所知,如果协议声明仅由类实现,则会起作用。protocol Protocol : class { ... } - olejnjak
如果 Foo 是一个协议,那么它应该可以工作。然而,如果你使用的是 Swift 2,一些协议会导致编译器错误。详情和解决方法请参见这个问题 - Theo
显示剩余2条评论

9

详情

  • Swift 5.1,Xcode 11.3.1

解决方案

struct WeakObject<Object: AnyObject> { weak var object: Object? }

选项1

@propertyWrapper
struct WeakElements<Collect, Element> where Collect: RangeReplaceableCollection, Collect.Element == Optional<Element>, Element: AnyObject {
    private var weakObjects = [WeakObject<Element>]()

    init(wrappedValue value: Collect) { save(collection: value) }

    private mutating func save(collection: Collect) {
        weakObjects = collection.map { WeakObject(object: $0) }
    }

    var wrappedValue: Collect {
        get { Collect(weakObjects.map { $0.object }) }
        set (newValues) { save(collection: newValues) }
    }
}

选项1使用方法

class Class1 { // or struct
    @WeakElements var weakObjectsArray = [UIView?]() // Use like regular array. With any objects

    func test() {
        weakObjectsArray.append(UIView())
        weakObjectsArray.forEach { print($0) }
    }
}

Option 2
选项2
struct WeakObjectsArray<Object> where Object: AnyObject {
    private var weakObjects = [WeakObject<Object>]()
}

extension WeakObjectsArray {
    typealias SubSequence = WeakObjectsArray<Object>
    typealias Element = Optional<Object>
    typealias Index = Int
    var startIndex: Index { weakObjects.startIndex }
    var endIndex: Index { weakObjects.endIndex }
    func index(after i: Index) -> Index { weakObjects.index(after: i) }
    subscript(position: Index) -> Element {
        get { weakObjects[position].object }
        set (newValue) { weakObjects[position] = WeakObject(object: newValue) }
    }
    var count: Int { return weakObjects.count }
    var isEmpty: Bool { return weakObjects.isEmpty }
}

extension WeakObjectsArray: RangeReplaceableCollection {
    mutating func replaceSubrange<C : Collection>( _ subrange: Range<Index>, with newElements: C) where Element == C.Element {
        weakObjects.replaceSubrange(subrange, with: newElements.map { WeakObject(object: $0) })
    }
}

选项2的使用方法

class Class2 { // or struct
    var weakObjectsArray = WeakObjectsArray<UIView>() // Use like regular array. With any objects

    func test() {
        weakObjectsArray.append(UIView())
        weakObjectsArray.forEach { print($0) }
    }
}

完整示例

不要忘记粘贴解决方案代码

import UIKit

class ViewController: UIViewController {

    @WeakElements var weakObjectsArray = [UIView?]()
    //var weakObjectsArray = WeakObjectsArray<UIView>()

    override func viewDidLoad() {
        super.viewDidLoad()
        addSubviews()
    }

    private func printArray(title: String) {
        DispatchQueue.main.async {
            print("=============================\n\(title)\ncount: \(self.weakObjectsArray.count)")
            self.weakObjectsArray.enumerated().forEach { print("\($0) \(String(describing: $1))") }
        }
    }
}

extension ViewController {

    private func createRandomRectangleAndAdd(to parentView: UIView) -> UIView {
        let view = UIView(frame: CGRect(x: Int.random(in: 0...200),
                                        y: Int.random(in: 60...200),
                                        width: Int.random(in: 0...200),
                                        height: Int.random(in: 0...200)))
        let color = UIColor(red: CGFloat.random(in: 0...255)/255,
                            green: CGFloat.random(in: 0...255)/255,
                            blue: CGFloat.random(in: 0...255)/255,
                            alpha: 1)
        view.backgroundColor = color
        parentView.addSubview(view)
        return view
    }

    private func addSubviews() {
        (0...1).forEach { _ in addView() }
        addButtons()
    }

    private func createButton(title: String, frame: CGRect, action: Selector) -> UIButton {
        let button = UIButton(frame: frame)
        button.setTitle(title, for: .normal)
        button.addTarget(self, action: action, for: .touchUpInside)
        button.setTitleColor(.blue, for: .normal)
        return button
    }

    private func addButtons() {
        view.addSubview(createButton(title: "Add",
                                     frame: CGRect(x: 10, y: 20, width: 40, height: 40),
                                     action: #selector(addView)))

        view.addSubview(createButton(title: "Delete",
                                     frame: CGRect(x: 60, y: 20, width: 60, height: 40),
                                     action: #selector(deleteView)))

        view.addSubview(createButton(title: "Remove nils",
                                     frame: CGRect(x: 120, y: 20, width: 100, height: 40),
                                     action: #selector(removeNils)))
    }

    @objc func deleteView() {
        view.subviews.first { view -> Bool in return !(view is UIButton) }?
            .removeFromSuperview()

        printArray(title: "First view deleted")
    }

    @objc func addView() {
        weakObjectsArray.append(createRandomRectangleAndAdd(to: view))
        printArray(title: "View addded")
    }

    @objc func removeNils() {
        weakObjectsArray = weakObjectsArray.filter { $0 != nil }
        printArray(title: "Remove all nil elements in weakArray")
    }
}

我对这两个选项(以及许多其他选项)的问题在于,这些类型的数组无法与协议一起使用。例如,以下代码将无法编译:protocol TP: class { } class TC { var a = WeakArray() var b = WeakObjectsArray() } - Matic Oblak
@MaticOblak 你觉得使用泛型怎么样?protocol TP: class { } class TC where TYPE: TP { var a = WeakObjectsArray() // 像普通数组一样使用,可以存储任何对象 var weakObjectsArray = [TYPE?]() } - Vasily Bodnarchuk
这个想法是,这个数组可以容纳实现相同类协议的不同类型的对象。通过使用泛型,您将其锁定为单一类型。例如,想象一下拥有这样一个数组delegates的单例。然后,您将有一些视图控制器希望使用此功能。您期望调用MyManager.delegates.append(self)。但是,如果MyManager被锁定为某种通用类型,则这不太可用。 - Matic Oblak
@MaticOblak 好的。试试这个:protocol TP: class { } class MyManager { typealias Delegate = AnyObject & TP static var delegates = [Delegate?]() } class A: TP { } class B: TP { } //MyManager.delegates.append(A()) //MyManager.delegates.append(B()) - Vasily Bodnarchuk
你现在失去了数组的通用部分,这是有点重要的 :) 我有一种感觉,这只是无法实现。目前Swift的一个限制... - Matic Oblak
显示剩余2条评论

8

我有一个相同的想法,即使用泛型创建弱容器。
因此,我为NSHashTable创建了一个包装器:

class WeakSet<ObjectType>: SequenceType {

    var count: Int {
        return weakStorage.count
    }

    private let weakStorage = NSHashTable.weakObjectsHashTable()

    func addObject(object: ObjectType) {
        guard object is AnyObject else { fatalError("Object (\(object)) should be subclass of AnyObject") }
        weakStorage.addObject(object as? AnyObject)
    }

    func removeObject(object: ObjectType) {
        guard object is AnyObject else { fatalError("Object (\(object)) should be subclass of AnyObject") }
        weakStorage.removeObject(object as? AnyObject)
    }

    func removeAllObjects() {
        weakStorage.removeAllObjects()
    }

    func containsObject(object: ObjectType) -> Bool {
        guard object is AnyObject else { fatalError("Object (\(object)) should be subclass of AnyObject") }
        return weakStorage.containsObject(object as? AnyObject)
    }

    func generate() -> AnyGenerator<ObjectType> {
        let enumerator = weakStorage.objectEnumerator()
        return anyGenerator {
            return enumerator.nextObject() as! ObjectType?
        }
    }
}

用法:

protocol MyDelegate : AnyObject {
    func doWork()
}

class MyClass: AnyObject, MyDelegate {
    fun doWork() {
        // Do delegated work.
    }
}

var delegates = WeakSet<MyDelegate>()
delegates.addObject(MyClass())

for delegate in delegates {
    delegate.doWork()
}

这并不是最好的解决方案,因为WeakSet可以使用任何类型进行初始化,如果该类型不符合AnyObject协议,则应用程序将崩溃并显示详细原因。但目前我没有看到更好的解决方案。

最初的解决方案是以以下方式定义WeakSet

class WeakSet<ObjectType: AnyObject>: SequenceType {}

但在这种情况下,WeakSet 无法使用协议进行初始化:

protocol MyDelegate : AnyObject {
    func doWork()
}

let weakSet = WeakSet<MyDelegate>()

目前上述代码无法编译(Swift 2.1,Xcode 7.1)。
这就是为什么我放弃使用AnyObject并添加了额外的保护,并带有fatalError()断言。


嗯,只需使用“for object in hashtable.allObjects”即可。 - malhal

5

由于NSPointerArray已经自动处理了大部分内容,因此我通过为其创建一个类型安全的包装器来解决问题,这避免了其他答案中的大量样板文件:

class WeakArray<T: AnyObject> {
    private let pointers = NSPointerArray.weakObjects()
    
    init (_ elements: T...) {
        elements.forEach{self.pointers.addPointer(Unmanaged.passUnretained($0).toOpaque())}
    }
    
    func get (_ index: Int) -> T? {
        if index < self.pointers.count, let pointer = self.pointers.pointer(at: index) {
            return Unmanaged<T>.fromOpaque(pointer).takeUnretainedValue()
        } else {
            return nil
        }
    }
    func append (_ element: T) {
        self.pointers.addPointer(Unmanaged.passUnretained(element).toOpaque())
    }
    func forEach (_ callback: (T) -> ()) {
        for i in 0..<self.pointers.count {
            if let element = self.get(i) {
                callback(element)
            }
        }
    }
    // implement other functionality as needed
}

用法示例:

class Foo {}
var foo: Foo? = Foo()
let array = WeakArray(foo!)
print(array.get(0)) // Optional(Foo)
foo = nil
DispatchQueue.main.async{print(array.get(0))} // nil

一开始可能需要更多的工作,但是在代码的其他部分使用起来会更加干净。如果你想让它更像一个数组,甚至可以实现下标操作,将其变为SequenceType等(但我的项目只需要appendforEach,所以我手头没有确切的代码)。


1
get 函数中要小心索引。 - Roman Filippov
很奇怪,我的实际代码是正确的,所以我不知道那个错误是如何出现在答案中的。 - John Montgomery

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接