web-dev-qa-db-fra.com

Quelle est une bonne alternative aux propriétés stockées statiques de types génériques dans swift?

Étant donné que les propriétés stockées statiques ne sont pas (encore) prises en charge pour les types génériques dans Swift, je me demande quelle est la bonne alternative.

Mon cas d'utilisation spécifique est que je souhaite créer un ORM dans Swift. J'ai un protocole Entity qui a un type associé pour la clé primaire, car certaines entités auront un entier comme leur id et d'autres une chaîne, etc. Cela rend donc le protocole Entity générique.

Maintenant, j'ai aussi un type EntityCollection<T: Entity>, qui gère des collections d'entités et, comme vous pouvez le constater, il est également générique. L'objectif de EntityCollection est de vous permettre d'utiliser des collections d'entités comme s'il s'agissait de tableaux normaux sans avoir à vous rendre compte de l'existence d'une base de données. EntityCollection se charge des requêtes et de la mise en cache et est optimisé au maximum.

Je voulais utiliser des propriétés statiques sur la variable EntityCollection pour stocker toutes les entités déjà extraites de la base de données. Ainsi, si deux instances distinctes de EntityCollection veulent extraire la même entité de la base de données, la base de données ne sera interrogée qu'une seule fois.

Avez-vous une idée de la façon dont je pourrais atteindre cet objectif?

18
Evert

La raison pour laquelle Swift ne prend actuellement pas en charge les propriétés stockées statiques sur les types génériques est qu’il faudrait un stockage de propriété distinct pour chaque spécialisation du ou des espaces génériques génériques - il existe une discussion plus poussée à ce sujet dans le présent article .

Nous pouvons toutefois implémenter cela nous-mêmes avec un dictionnaire global (rappelez-vous que les propriétés statiques ne sont rien de plus que des propriétés globales espacées d'un type donné). Il y a cependant quelques obstacles à surmonter pour le faire.

Le premier obstacle est qu'il nous faut un type de clé. Idéalement, il s'agirait de la valeur de métatype pour le ou les espaces génériques génériques du type; Cependant, les métatypes ne peuvent pas actuellement se conformer aux protocoles et ne sont donc pas Hashable. Pour résoudre ce problème, nous pouvons construire un 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
  }

  var hashValue: Int {
    return ObjectIdentifier(base).hashValue
  }
}

La seconde est que chaque valeur du dictionnaire peut être d'un type différent; heureusement, cela peut être facilement résolu en effaçant simplement Any et en renvoyant quand nous en avons besoin.

Alors, voici à quoi cela ressemblerait:

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()]

Nous sommes en mesure de maintenir l'invariant que le métatype utilisé pour la clé décrit le type d'élément de la valeur du tableau via l'implémentation de loadedEntities, car nous ne stockons qu'une valeur [T] pour une clé T.self.


Il existe toutefois un problème de performances potentiel lié à l’utilisation d’un getter et d’un setter; les valeurs du tableau subiront une copie lors de la mutation (la mutation appelle le getter pour obtenir un tableau temporaire, ce tableau est muté et le setter est appelé).

(j'espère que nous aurons bientôt des adresses généralisées ...)

Selon qu'il s'agit ou non d'un problème de performances, vous pouvez implémenter une méthode statique pour effectuer une mutation sur place des valeurs du tableau:

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
}

Il y a pas mal de choses ici, décompressons un peu:

  • Nous supprimons d’abord le tableau du dictionnaire (s’il existe).
  • Nous appliquons ensuite les mutations au tableau. Comme il est maintenant référencé de manière unique (ne figure plus dans le dictionnaire), il peut être muté sur place.
  • Nous avons ensuite réinséré le tableau muté dans le dictionnaire (en utilisant defer afin de pouvoir retourner proprement de body et de remettre le tableau en place).

Nous utilisons with(_:_:) ici afin de nous assurer un accès en écriture à _loadedEntities tout au long de withLoadedEntities(_:) afin de nous assurer que Swift détecte les violations d'accès exclusives comme celles-ci:

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

Je ne sais pas si j'aime ça ou pas, mais j'ai utilisé une propriété calculée statique:

private extension Array where Element: String {
    static var allIdentifiers: [String] {
        get {
            return ["String 1", "String 2"]
        }
    }
}

Pensées?

9
Jerry

Il y a une heure, j'ai un problème presque comme le vôtre. Je souhaite également disposer d'une classe BaseService et de nombreux autres services hérités de celle-ci avec une seule instance statique. Et le problème est que tous les services utilisent leur propre modèle (ex: UserService using UserModel ..)

En bref, j'ai essayé de suivre le code. Et il fonctionne!.

class BaseService<Model> where Model:BaseModel {
    var models:[Model]?;
}

class UserService : BaseService<User> {
    static let shared = UserService();

    private init() {}
}

J'espère que ça aide. 

Je pense que le truc était que BaseService lui-même ne sera pas utilisé directement, donc PAS BESOIN D'AVOIR une propriété stockée statique. (P.S. J'aimerais que Swift prenne en charge la classe abstraite, BaseService devrait l'être)

3
Tulga Orosoo

Il s'avère que, bien que les propriétés ne soient pas autorisées, les méthodes et les propriétés calculées le sont. Donc, vous pouvez faire quelque chose comme ça:

class MyClass<T> {
    static func myValue() -> String { return "MyValue" }
}

Ou:

class MyClass<T> {
    static var myValue: String { return "MyValue" }
}
1
Caleb Kleveter

Selon le nombre de types à prendre en charge et si - héritage n'est pas une option pour vous, la conformité conditionnelle peut également faire l'affaire:

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
0
Fran Pugl

Ce n'est pas idéal, mais c'est la solution que j'ai proposée pour répondre à mes besoins.

J'utilise une classe non générique pour stocker les données. Dans mon cas, je l'utilise pour stocker des singletons. J'ai la classe suivante:

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
    }
}

Ceci est fondamentalement juste une cache.

La fonction singleton prend le générique responsable du singleton et une fermeture qui renvoie une nouvelle instance du singleton. 

Il génère une clé de chaîne à partir du nom de la classe d'instance générique et vérifie le dictionnaire (singletons) pour voir s'il existe déjà. Sinon, il appelle la fermeture pour la créer et la stocker, sinon elle la renvoie.

A partir d'une classe générique, vous pouvez utiliser un static property comme décrit par Caleb. Par exemple:

open class Something<G> {
    open static var number: Int {
        return GenericStatic.singleton(for: self) {
            print("Creating singleton for \(String(describing: self))")
            return 5
        }
    }
}

En testant ce qui suit, vous pouvez voir que chaque singleton est créé uniquement une fois par type générique:

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>"

Cette solution peut permettre de comprendre pourquoi cela n’est pas géré automatiquement dans Swift.

J'ai choisi d'implémenter cela en rendant le singleton statique pour chaque instance générique, mais cela peut être ou ne pas être votre intention ou votre besoin.

0
cue8chalk

Tout ce que je peux proposer, c'est de séparer la notion de source (d'où provient la collection), puis la collection elle-même. Et ensuite, rendez la source responsable de la mise en cache. À ce stade, la source peut être en réalité une instance. Elle peut donc conserver tous les caches souhaités et votre entité EntityCollection est simplement responsable de la gestion d'un protocole CollectionType et/ou SequenceType autour de la source.

Quelque chose comme:

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 { ... }

classe WebEntityCollection: SequenceType {...}

fonctionnerait si vous avez une interface de données Web paginée typique. Ensuite, vous pourriez faire quelque chose comme:

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 { ... }
0
David Berry