Why not support var myArray: [weak MyClass]

This must have been asked many times. My searches failed. Please advise.

Weak or unowned collections can be handy.
It seems odd if i have one i can choose weak or unowned. More than one i cannot.

What have i missed?
Today i made an unowned wrapper and a computed property to allow the calling code to access my array as normal. And had my unowned array behind it.

I look forward to reading past threads and proposals.
Thx

I think the reason why [weak MyClass] doesn't exist is because of its behaviour when the reference is deallocated. weak vars need to be optional, because it turns nil when the reference is deallocated. Unowned just assumes that its allocated and crashes if its not.

In [weak MyClass], if one of the references is deallocated, what would happen? Would it remove itself from the array? That might cause some confusion when elements go poof randomly. Would it be like an optional and turn nil? Then the definition [weak MyClass] might not sufficently make it clear that its not [MyClass] but rather [MyClass?].

Of course, there may be ways to achieve a weak array in swift already, via property wrappers or whatnot. Syntax to do it easily in swift, however, might need to go through a few revisions before being added.

1 Like

Here's a naΓ―ve implementation of a weak array:

final class Weak<A: AnyObject> {
  weak var unbox: A?
  init(_ value: A?) {
    self.unbox = value
  }
}

struct WeakArray<Element: AnyObject> {
  private var storage: [Weak<Element>] = []
  init() {}
  init<C: Collection>(_ elems: C) where C.Element == Element {
    storage = elems.map(Weak.init)
  }
  mutating func purge() {
    storage.removeAll(where: { $0.unbox == nil })
  }
}

extension WeakArray: MutableCollection {
  var startIndex: Int { storage.startIndex }
  var endIndex: Int { storage.endIndex }

  subscript(_ index: Int) -> Element? {
    get { storage[index].unbox }
    set {
      defer { purge() }
      storage[index] = Weak(newValue)
    }
  }
  func index(after i: Int) -> Int {
    storage.index(after: i)
  }
}

extension WeakArray: RangeReplaceableCollection {
  mutating func replaceSubrange<C>(_ subrange: Range<Int>, with newElements: C) where C : Collection, Element? == C.Element {
    defer { purge() }
    storage.replaceSubrange(subrange, with: newElements.map(Weak.init))
  }
}
2 Likes

Note that getting an element may return nil if the box is empty, eg. the object has no strong references keeping it alive. All mutating access, e.g. calling a setter or adding/inserting/removing items, will automatically purge the collection of empty boxes.

That on its own could be very easily addressed by requiring to use [weak MyClass?], just like for weak properties.

While I don't know if there are other, more concerning aspects (e.g. non-negligible performance impacts that result from arrays behaving like value types, but doing memory allocations and deallocations on the heap under the hood), the way I would interpret this syntax opens up ambiguities:
A literal reading of this would mean that once an object in the collection of weak instances deallocates, it is replaced with nil. That's how properties are handled. While this can be fine, I think often people would assume that instead it is removed from the collection.
This then also opens the interesting question of how much people would "waste" memory by keeping collections of nil objects around... :smiley:

Of course either option would be possible, my point is just that [weak MyClass], imo, is not precise enough to become a language feature (like this). @sveinhal's example kind of shows this: A weak collection warrants its own custom implementation that is tailored to whatever concrete behavior is wanted. Maybe a small package that offers some of the more common ways to do this would be a good project?

2 Likes

Weak references are not separate types; they are modifiers which apply to a particular storage location.

class SomeClass {}

weak var weakRef: SomeClass?
var strongOptionalRef: SomeClass?

type(of: weakRef) == type(of: strongOptionalRef) // true

If we made them separate types (so the above returns false), that would probably be even more awkward to use.

For instance, let's say they were separate types:

  • Does extension Collection where Element == SomeClass apply to weak SomeClass?
  • Does weak SomeClass copy all of the protocol conformances of regular SomeClass?
  • Does weak SomeClass have the same superclass as regular SomeClass?

People would probably expect most of those things, but we'd need to add a lot of implicit magic to support them. There are cases where, by trying to be helpful in this way, we make the language more confusing.

Alternatively, it is trivial to make a little wrapper as @sveinhal showed. It's a bit more manual, but overall it allows for much simpler language model because it makes it clear that you shouldn't have any of the above expectations -- Weak<UITableView> (in a wrapper struct) doesn't inherit from UIView, and it's much clearer why that is.

4 Likes

I believe the OP is asking if we have this:

var myArray: [MyClass?]
if let x = myArray[42] { ... }

as a syntax sugar for:

enum Optional<Wrapped> { case none, some(Wrapped) }
var myArray: [Optional<MyClass>]
if case .some(let x) = myArray[42] { ... }

why not to have:

var myArray: [weak MyClass]
if let x = myArray[42] { ... }

as a syntax sugar for:

struct Weak<Wrapped: AnyObject> { weak var wrapped: Wrapped? }
var myArray: [Weak<MyClass>]
if let x = myArray[42].wrapped { ... }

I'd say the reason is – this is not used frequently enough to warrant a special treatment.

2 Likes
6 Likes

That's an interesting consideration. What'd be the best way to temporarily retain, do the operation and then release all (currently non nil) WeakHolders' elements without the brute-force solution of temporary allocating and then dropping the corresponding array of N strong references? I am thinking of calling through C or Obj-C to reach out for (the unavailable in Swift) retain/release, would that fly?

struct WeakHolder<Wrapped: AnyObject> {
    weak var wrapped: Wrapped?
}

// O(N) storage 😒
func perform<Wrapped: AnyObject>(_ items: [WeakHolder<Wrapped>], _ execute: () -> Void) {
    withExtendedLifetime(items.map { $0.wrapped }, execute)
}

// O(1) storage πŸ‘
func perform<Wrapped: AnyObject>(_ items: [WeakHolder<Wrapped>], _ execute: () -> Void) {
    items.forEach { holder in
        if let wrapped = holder.wrapped {
            retain(wrapped) // via C or Obj-c
        }
    }
    execute()
    items.forEach { holder in
        if let wrapped = holder.wrapped {
            release(wrapped) // via C or Obj-c
        }
    }
}

On a side note, is it possible to achieve this (a method v a standalone function):

typealias WeakArray<Wrapped: AnyObject> = [WeakHolder<Wrapped>]

extension WeakArray {
    func perform(_ execute: () -> Void) {
        withExtendedLifetime(
            map { $0.wrapped }, // πŸ›‘ Value of type 'Element' has no member 'wrapped'
            execute
        )
    }
}

It's nice to see I was on the right path and to learn the other considerations.

Having the weak elements be optionally null I think would be expected.

Another option would be to allow unowned instead of weak. The use case I encountered was a tree (inherited code) that is centrally built and only child references need be strong, other various graph-like connections should not be strong.

unowned seems like it is safer to use in a collection, but I guess it still just allows the unsuspecting users to shoot the other foot. (Crash versus leak/retain)

Wrapping is easy enough and I would not expect to expose this outward to a wider audience of developers.

I appreciate all the great conversation and links to related topics.