Since static stored properties are not (yet) supported for generic types in swift, I wonder what is a good alternative.
My specific use-case is that I want to build an ORM in swift. I have an Entity
protocol which has an associatedtype for the primary key, since some entities will have an integer as their id
and some will have a string etc. So that makes the Entity
protocol generic.
Now I also have an EntityCollection<T: Entity>
type, which manages collections of entities and as you can see it is also generic. The goal of EntityCollection
is that it lets you use collections of entities as if they were normal arrays without having to be aware that there's a database behind it. EntityCollection
will take care of querying and caching and being as optimized as possible.
I wanted to use static properties on the EntityCollection
to store all the entities that have already been fetched from the database. So that if two separate instances of EntityCollection
want to fetch the same entity from the database, the database will be queried only once.
Do you guys have any idea how else I could achieve that?
Swift lets you create properties and methods that belong to a type, rather than to instances of a type. This is helpful for organizing your data meaningfully by storing shared data. Swift calls these shared properties “static properties”, and you create one just by using the static keyword.
Swift 4 language provides 'Generic' features to write flexible and reusable functions and types. Generics are used to avoid duplication and to provide abstraction. Swift 4 standard libraries are built with generics code. Swift 4s 'Arrays' and 'Dictionary' types belong to generic collections.
Swift allows us to use a static prefix on methods and properties to associate them with the type that they're declared on rather than the instance. We can also use static properties to create singletons of our objects which, as you have probably heard before is a huge anti-pattern.
Generic Where Clauses Type constraints, as described in Type Constraints, enable you to define requirements on the type parameters associated with a generic function, subscript, or type. It can also be useful to define requirements for associated types. You do this by defining a generic where clause.
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:
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! }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With