What is a good alternative for static stored properties of generic types in swift?

The reason that Swift doesn’t currently support static stored properties on generic types is that separate property storage would be required for each specialisation of the generic placeholder(s) – there’s more discussion of this in this Q&A.

We can however implement this ourselves with a global dictionary (remember that static properties are nothing more than global properties namespaced to a given type). There are a few obstacles to overcome in doing this though.

The first obstacle is that we need a key type. Ideally this would be the metatype value for the generic placeholder(s) of the type; however metatypes can’t currently conform to protocols, and so therefore aren’t Hashable. To fix this, we can build a wrapper:

/// Hashable wrapper for any metatype value.
struct AnyHashableMetatype : Hashable {

  static func ==(lhs: AnyHashableMetatype, rhs: AnyHashableMetatype) -> Bool {
    return lhs.base == rhs.base
  }

  let base: Any.Type

  init(_ base: Any.Type) {
    self.base = base
  }

  func hash(into hasher: inout Hasher) {
    hasher.combine(ObjectIdentifier(base))
  }
  // Pre Swift 4.2:
  // var hashValue: Int { return ObjectIdentifier(base).hashValue }
}

The second is that each value of the dictionary can be a different type; fortunately that can be easily solved by just erasing to Any and casting back when we need to.

So here’s what that would look like:

protocol Entity {
  associatedtype PrimaryKey
}

struct Foo : Entity {
  typealias PrimaryKey = String
}

struct Bar : Entity {
  typealias PrimaryKey = Int
}

// Make sure this is in a seperate file along with EntityCollection in order to
// maintain the invariant that the metatype used for the key describes the
// element type of the array value.
fileprivate var _loadedEntities = [AnyHashableMetatype: Any]()

struct EntityCollection<T : Entity> {

  static var loadedEntities: [T] {
    get {
      return _loadedEntities[AnyHashableMetatype(T.self), default: []] as! [T]
    }
    set {
      _loadedEntities[AnyHashableMetatype(T.self)] = newValue
    }
  }

  // ...
}

EntityCollection<Foo>.loadedEntities += [Foo(), Foo()]
EntityCollection<Bar>.loadedEntities.append(Bar())

print(EntityCollection<Foo>.loadedEntities) // [Foo(), Foo()]
print(EntityCollection<Bar>.loadedEntities) // [Bar()]

We are able to maintain the invariant that the metatype used for the key describes the element type of the array value through the implementation of loadedEntities, as we only store a [T] value for a T.self key.


There is a potential performance issue here however from using a getter and setter; the array values will suffer from copying on mutation (mutating calls the getter to get a temporary array, that array is mutated and then the setter is called).

(hopefully we get generalised addressors soon…)

Depending on whether this is a performance concern, you could implement a static method to perform in-place mutation of the array values:

func with<T, R>(
  _ value: inout T, _ mutations: (inout T) throws -> R
) rethrows -> R {
  return try mutations(&value)
}

extension EntityCollection {

  static func withLoadedEntities<R>(
    _ body: (inout [T]) throws -> R
  ) rethrows -> R {
    return try with(&_loadedEntities) { dict -> R in
      let key = AnyHashableMetatype(T.self)
      var entities = (dict.removeValue(forKey: key) ?? []) as! [T]
      defer {
        dict.updateValue(entities, forKey: key)
      }
      return try body(&entities)
    }
  }
}

EntityCollection<Foo>.withLoadedEntities { entities in
  entities += [Foo(), Foo()] // in-place mutation of the array
}

There’s quite a bit going on here, let’s unpack it a bit:

  • We first remove the array from the dictionary (if it exists).
  • We then apply the mutations to the array. As it’s now uniquely referenced (no longer present in the dictionary), it can be mutated in-place.
  • We then put the mutated array back in the dictionary (using defer so we can neatly return from body and then put the array back).

We’re using with(_:_:) here in order to ensure we have write access to _loadedEntities throughout the entirety of withLoadedEntities(_:) to ensure that Swift catches exclusive access violations like this:

EntityCollection<Foo>.withLoadedEntities { entities in
  entities += [Foo(), Foo()]
  EntityCollection<Foo>.withLoadedEntities { print($0) } // crash!
}

Leave a Comment

Hata!: SQLSTATE[HY000] [1045] Access denied for user 'divattrend_liink'@'localhost' (using password: YES)