What is a good alternative for static stored prope

2020-08-09 05:06发布

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?

9条回答
Ridiculous、
2楼-- · 2020-08-09 05:44

All I can come up with is to separate out the notion of source (where the collection comes from) and then collection itself. And then the make the source responsible for caching. At that point the source can actually be an instance, so it can keep whatever caches it wants/needs to and your EntityCollection is just responsible for maintaining a CollectionType and/or SequenceType protocol around the source.

Something like:

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

class WebEntityCollection : SequenceType { ... }

would work if you have a typical paged web data interface. Then you could do something along the lines of:

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 { ... }
查看更多
Luminary・发光体
3楼-- · 2020-08-09 05:48

This isn't ideal, but this is the solution I came up with to fit my needs.

I'm using a non-generic class to store the data. In my case, I'm using it to store singletons. I have the following class:

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

This is basically just a cache.

The function singleton takes the generic that is responsible for the singleton and a closure that returns a new instance of the singleton.

It generates a string key from the generic instance class name and checks the dictionary (singletons) to see if it already exists. If not, it calls the closure to create and store it, otherwise it returns it.

From a generic class, you can use a static property as described by Caleb. For example:

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

Testing the following, you can see that each singleton is only created once per generic type:

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

This solution may offer some insight into why this isn't handled automatically in Swift.

I chose to implement this by making the singleton static to each generic instance, but that may or may not be your intention or need.

查看更多
甜甜的少女心
4楼-- · 2020-08-09 05:53

Depending on how many types you need to support and whether inheritance is (not) an option for you, conditional conformance could also do the trick:

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
查看更多
Rolldiameter
5楼-- · 2020-08-09 05:55

An hour ago i have a problem almost like yours. I also want to have a BaseService class and many other services inherited from this one with only one static instance. And the problem is all services use their own model (ex: UserService using UserModel..)

In short I tried following code. And it works!.

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

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

    private init() {}
}

Hope it helps.

I think the trick was BaseService itself will not be used directly so NO NEED TO HAVE static stored property. (P.S. I wish swift supports abstract class, BaseService should be)

查看更多
兄弟一词,经得起流年.
6楼-- · 2020-08-09 06:03

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!
}
查看更多
姐就是有狂的资本
7楼-- · 2020-08-09 06:03

It turns out that, although properties are not allowed, methods and computed properties are. So you can do something like this:

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

Or:

class MyClass<T> {
    static var myValue: String { return "MyValue" }
}
查看更多
登录 后发表回答