¿Cuál es una buena alternativa para las propiedades almacenadas estáticas de tipos genéricos en swift?
generics orm (8)
Dado que las propiedades almacenadas estáticas no son (todavía) compatibles con los tipos genéricos en swift, me pregunto cuál es una buena alternativa.
Mi caso de uso específico es que quiero construir un ORM en swift. Tengo un protocolo de Entity
que tiene un tipo asociado para la clave principal, ya que algunas entidades tendrán un entero como su id
y otras tendrán una cadena, etc. Así que eso hace que el protocolo de Entity
genérico.
Ahora también tengo un tipo EntityCollection<T: Entity>
, que administra colecciones de entidades y, como puede ver, también es genérico. El objetivo de EntityCollection
es que le permite usar colecciones de entidades como si fueran matrices normales sin tener que estar enterado de que hay una base de datos detrás. EntityCollection
se encargará de las consultas y el almacenamiento en caché y estará lo más optimizado posible.
Quería usar propiedades estáticas en EntityCollection
para almacenar todas las entidades que ya se han recuperado de la base de datos. De modo que si dos instancias separadas de EntityCollection
desean obtener la misma entidad de la base de datos, se consultará la base de datos solo una vez.
¿Tenéis idea de cómo podría lograr eso?
¿Algo como esto?
protocol Entity {
}
class EntityCollection {
static var cachedResults = [Entity]()
func findById(id: Int) -> Entity? {
// Search cache for entity with id from table
// Return result if exists else...
// Query database
// If entry exists in the database append it to the cache and return it else...
// Return nil
}
}
Dependiendo de cuántos tipos necesite admitir y de si la inheritance es (no) una opción para usted, la conformidad condicional también podría hacer el truco:
final class A<T> {}
final class B {}
final class C {}
extension A where T == B {
static var stored: [T] = []
}
extension A where T == C {
static var stored: [T] = []
}
let a1 = A<B>()
A<B>.stored = [B()]
A<B>.stored
let a2 = A<C>()
A<C>.stored = [C()]
A<C>.stored
Esto no es ideal, pero esta es la solución que se me ocurrió para satisfacer mis necesidades.
Estoy usando una clase no genérica para almacenar los datos. En mi caso, lo estoy usando para almacenar singletons. Tengo la siguiente clase:
private class GenericStatic {
private static var singletons: [String:Any] = [:]
static func singleton<GenericInstance, SingletonType>(for generic: GenericInstance, _ newInstance: () -> SingletonType) -> SingletonType {
let key = "/(String(describing: GenericInstance.self))./(String(describing: SingletonType.self))"
if singletons[key] == nil {
singletons[key] = newInstance()
}
return singletons[key] as! SingletonType
}
}
Esto es básicamente un caché.
La función singleton
toma el genérico responsable del singleton y un cierre que devuelve una nueva instancia del singleton.
Genera una clave de cadena a partir del nombre de clase de instancia genérico y comprueba el diccionario ( singletons
) para ver si ya existe. Si no, llama al cierre para crearlo y almacenarlo, de lo contrario, lo devuelve.
Desde una clase genérica, puede usar una propiedad estática como lo describe Caleb. Por ejemplo:
open class Something<G> {
open static var number: Int {
return GenericStatic.singleton(for: self) {
print("Creating singleton for /(String(describing: self))")
return 5
}
}
}
Al probar lo siguiente, puede ver que cada singleton solo se crea una vez por tipo genérico :
print(Something<Int>.number) // prints "Creating singleton for Something<Int>" followed by 5
print(Something<Int>.number) // prints 5
print(Something<String>.number) // prints "Creating singleton for Something<String>"
Esta solución puede ofrecer una idea de por qué esto no se maneja automáticamente en Swift.
Elegí implementar esto haciendo el singleton estático para cada instancia genérica, pero esa puede o no ser su intención o necesidad.
Hace una hora tengo un problema casi como el tuyo. También quiero tener una clase BaseService y muchos otros servicios heredados de esta con solo una instancia estática. Y el problema es que todos los servicios usan su propio modelo (por ejemplo, UserService utilizando UserModel ..)
En resumen he intentado seguir el código. ¡Y funciona!.
class BaseService<Model> where Model:BaseModel {
var models:[Model]?;
}
class UserService : BaseService<User> {
static let shared = UserService();
private init() {}
}
Espero eso ayude.
Creo que el truco fue que BaseService no se utilizará directamente, por lo que NO NECESITA TENER propiedad almacenada estática. (PS deseo swift soporta clase abstracta, BaseService debería ser)
La razón por la que Swift no admite actualmente las propiedades almacenadas estáticas en tipos genéricos es que se requeriría un almacenamiento de propiedades por separado para cada especialización de los marcadores de posición genéricos; hay más información al respecto en esta sección de preguntas y respuestas .
Sin embargo, podemos implementar esto nosotros mismos con un diccionario global (recuerde que las propiedades estáticas no son más que propiedades globales con un nombre dado a un tipo dado). Sin embargo, hay algunos obstáculos que superar para hacer esto.
El primer obstáculo es que necesitamos un tipo de llave. Idealmente, este sería el valor de metatipo para los marcadores de posición genéricos del tipo; sin embargo, los metatipos actualmente no se ajustan a los protocolos y, por lo tanto, no son Hashable
. Para solucionar esto, podemos construir una envoltura :
/// 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
}
var hashValue: Int {
return ObjectIdentifier(base).hashValue
}
}
La segunda es que cada valor del diccionario puede ser de un tipo diferente; Afortunadamente, eso se puede resolver fácilmente simplemente borrando a Any
y devolviéndolo cuando sea necesario.
Así que aquí está el aspecto que tendría:
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()]
Podemos mantener el invariante que el metatipo usado para la clave describe el tipo de elemento del valor de la matriz a través de la implementación de las loadedEntities
, ya que solo almacenamos un valor [T]
para una clave T.self
.
Hay un problema potencial de rendimiento aquí, sin embargo, el uso de un getter y setter; los valores de la matriz sufrirán la copia en la mutación (la mutación llama al captador para obtener una matriz temporal, esa matriz se muta y luego se llama al definidor).
(Ojalá tengamos direccionadores generalizados pronto ...)
Dependiendo de si se trata de un problema de rendimiento, podría implementar un método estático para realizar una mutación in situ de los valores de la matriz:
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
}
Hay un poco de aquí, vamos a descomprimirlo un poco:
- Primero eliminamos la matriz del diccionario (si existe).
- Luego aplicamos las mutaciones a la matriz. Como ahora tiene una referencia única (ya no está presente en el diccionario), se puede mutar en el lugar.
- Luego volvemos a colocar la matriz mutada en el diccionario (usando la
defer
para que podamos regresar delbody
y devolver la matriz).
Estamos usando with(_:_:)
aquí para garantizar que tengamos acceso de escritura a _loadedEntities
en la totalidad de withLoadedEntities(_:)
para asegurarnos de que Swift withLoadedEntities(_:)
violaciones de acceso exclusivo como esta:
EntityCollection<Foo>.withLoadedEntities { entities in
entities += [Foo(), Foo()]
EntityCollection<Foo>.withLoadedEntities { print($0) } // crash!
}
No estoy seguro de si me gusta esto o no, pero usé una propiedad computada estática:
private extension Array where Element: String {
static var allIdentifiers: [String] {
get {
return ["String 1", "String 2"]
}
}
}
¿Pensamientos?
Resulta que, aunque las propiedades no están permitidas, los métodos y las propiedades calculadas sí lo son. Así que puedes hacer algo como esto:
class MyClass<T> {
static func myValue() -> String { return "MyValue" }
}
O:
class MyClass<T> {
static var myValue: String { return "MyValue" }
}
Todo lo que puedo hacer es separar la noción de origen (de dónde proviene la colección) y luego recopilarla. Y luego hacer la fuente responsable de la caché. En ese punto, la fuente puede ser realmente una instancia, por lo que puede mantener cualquier caché que desee o necesite y su EntityCollection es el único responsable de mantener un CollectionType y / o un protocolo SequenceType alrededor de la fuente.
Algo como:
protocol Entity {
associatedtype IdType : Comparable
var id : IdType { get }
}
protocol Source {
associatedtype EntityType : Entity
func first() -> [EntityType]?
func next(_: EntityType) -> [EntityType]?
}
class WebEntityGenerator <EntityType:Entity, SourceType:Source where EntityType == SourceType.EntityType> : GeneratorType { ... }
clase WebEntityCollection: SequenceType {...}
Funcionaría si tiene una interfaz de datos web paginada típica. Entonces podrías hacer algo como:
class WebQuerySource<EntityType:Entity> : Source {
var cache : [EntityType]
...
func query(query:String) -> WebEntityCollection {
...
}
}
let source = WebQuerySource<MyEntityType>(some base url)
for result in source.query(some query argument) {
}
source.query(some query argument)
.map { ... }
.filter { ... }