【问题标题】:What is a good alternative for static stored properties of generic types in swift?swift中泛型类型的静态存储属性有什么好的替代方法?
【发布时间】:2016-06-22 09:04:42
【问题描述】:

由于 swift 中的泛型类型(尚​​不)支持静态存储属性,我想知道有什么好的替代方案。

我的具体用例是我想快速构建一个 ORM。我有一个Entity 协议,它有一个关联类型作为主键,因为有些实体的id 会有一个整数,而有些实体会有一个字符串等。所以Entity 协议是通用的。

现在我还有一个EntityCollection<T: Entity> 类型,它管理实体集合,你可以看到它也是通用的。 EntityCollection 的目标是它让您可以像使用普通数组一样使用实体集合,而不必知道它背后有一个数据库。 EntityCollection 将负责查询和缓存并尽可能优化。

我想在EntityCollection 上使用静态属性来存储已经从数据库中获取的所有实体。这样如果EntityCollection 的两个独立实例想要从数据库中获取相同的实体,那么数据库将只被查询一次。

你们知道我还能如何实现吗?

【问题讨论】:

    标签: swift generics orm


    【解决方案1】:

    Swift 目前不支持泛型类型上的静态存储属性的原因是,泛型占位符的每个特化都需要单独的属性存储 - 对此in this Q&A 进行了更多讨论。

    但是,我们可以自己使用全局字典来实现这一点(请记住,静态属性只不过是命名为给定类型的全局属性)。不过,在此过程中需要克服一些障碍。

    第一个障碍是我们需要一个键类型。理想情况下,这将是该类型的通用占位符的元类型值;但是元类型目前不能符合协议,因此不是Hashable。要解决这个问题,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 }
    }
    

    第二个是字典的每个值可以是不同的类型;幸运的是,只需擦除到Any 并在需要时回滚即可轻松解决。

    这就是它的样子:

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

    我们能够通过loadedEntities的实现来保持用于键的元类型描述数组值的元素类型的不变性,因为我们只为T.self键存储[T]值。


    这里有一个潜在的性能问题,但是使用 getter 和 setter;数组值会因突变而受到复制(突变调用 getter 以获取临时数组,该数组发生突变,然后调用 setter)。

    (希望我们很快就能得到通用地址...)

    根据这是否是性能问题,您可以实现一个静态方法来执行数组值的就地突变:

    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
    }
    

    这里发生了很多事情,让我们解压一下:

    • 我们首先从字典中删除该数组(如果存在)。
    • 然后我们将突变应用到阵列。由于它现在被唯一引用(不再出现在字典中),因此可以就地变异。
    • 然后我们将变异的数组放回字典中(使用defer,这样我们就可以巧妙地从body 返回,然后再放回数组)。

    我们在这里使用with(_:_:) 是为了确保我们在整个withLoadedEntities(_:) 中拥有对_loadedEntities 的写访问权限,以确保Swift 捕获这样的独占访问违规:

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

    【讨论】:

      【解决方案2】:

      我不确定我是否喜欢这个,但我使用了静态计算属性:

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

      想法?

      【讨论】:

      • 我认为这会编译但每次都会创建一个新副本并且表现得像一个非静态属性
      【解决方案3】:

      一个小时前,我遇到了一个几乎和你一样的问题。我还希望有一个 BaseService 类和许多其他从这个类继承的服务,只有一个静态实例。问题是所有服务都使用自己的模型(例如:使用 UserModel 的 UserService..)

      简而言之,我尝试了以下代码。它有效!。

      class BaseService<Model> where Model:BaseModel {
          var models:[Model]?;
      }
      
      class UserService : BaseService<User> {
          static let shared = UserService();
      
          private init() {}
      }
      

      希望对您有所帮助。

      我认为诀窍是 BaseService 本身不会被直接使用,因此不需要静态存储属性。 (P.S.我希望swift支持抽象类,BaseService应该是)

      【讨论】:

      • Swift 不支持多重继承,这需要 subclassing 。所以这个解决方案不能扩展到 mixins
      【解决方案4】:

      事实证明,虽然属性是不允许的,但方法和计算属性是允许的。所以你可以这样做:

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

      或者:

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

      【讨论】:

      • 这将每次返回一个新副本,违背了拥有静态属性的目的!
      • @user1366265 不一定。稍加调整(尤其是在线程安全存在问题的情况下),您可以缓存该值,通常在全局变量中。
      【解决方案5】:

      好吧,我也遇到了同样的问题,并且能够为它设计一个合乎逻辑的解决方法。我必须使用泛型类作为处理程序来创建 urlsession 的静态实例。

      class ViewController: UIViewController {
      override func viewDidLoad() {
          super.viewDidLoad()
          let neworkHandler = NetworkHandler<String>()
          neworkHandler.download()
          neworkHandler.download()
      }
      
      
      class SessionConfigurator: NSObject{
      static var configuration:URLSessionConfiguration{
          let sessionConfig = URLSessionConfiguration.background(withIdentifier: "com.bundle.id")
          sessionConfig.isDiscretionary = true
          sessionConfig.allowsCellularAccess = true
          return sessionConfig
      }
      static var urlSession:URLSession?
      
      
      class NetworkHandler<T> :NSObject, URLSessionDelegate{
        func download(){
          if SessionConfigurator.urlSession == nil{
          SessionConfigurator.urlSession = URLSession(configuration:SessionConfigurator.configuration, delegate:self, delegateQueue: OperationQueue.main)
          }
      }
      

      【讨论】:

      • 这并不像你说的那样理想,但是 swift 存在缺点..所以这是一种解决方法。
      【解决方案6】:

      我能想到的就是分离出源的概念(集合来自哪里),然后是集合本身。然后让源负责缓存。此时源实际上可以是一个实例,因此它可以保留它想要/需要的任何缓存,并且您的 EntityCollection 只负责维护源周围的 CollectionType 和/或 SequenceType 协议。

      类似:

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

      类 WebEntityCollection : SequenceType { ... }

      如果您有一个典型的分页 Web 数据接口,则可以使用。然后你可以按照以下方式做一些事情:

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

      【讨论】:

        【解决方案7】:

        这并不理想,但这是我想出的满足我需求的解决方案。

        我正在使用非泛型类来存储数据。就我而言,我用它来存储单例。我有以下课程:

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

        这基本上只是一个缓存。

        函数singleton 采用负责单例的泛型和返回单例新实例的闭包。

        它从通用实例类名生成一个字符串键并检查字典 (singletons) 以查看它是否已经存在。如果不是,则调用闭包创建并存储它,否则返回它。

        从泛型类中,您可以使用 Caleb 描述的静态 属性。例如:

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

        测试以下内容,您可以看到每个单例仅每个泛型类型创建一次

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

        这个解决方案可能会提供一些关于为什么在 Swift 中不能自动处理的一些见解。

        我选择通过使每个通用实例的单例静态化来实现这一点,但这可能是也可能不是您的意图或需要。

        【讨论】:

          【解决方案8】:

          根据您需要支持多少类型以及inheritance 是否(不是)适合您的选项,条件一致性也可以解决问题:

          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
          

          【讨论】:

            【解决方案9】:

            这样的?

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

            【讨论】:

            • 这对我不起作用,因为我正在使用 GENERIC 类型并且这些类型不允许静态存储属性。这正是我发布这个问题的原因......
            • 也许您可以将缓存更改为 [AnyObject] 类型的全局变量并在 EntityCollection 方法中转换为 [T]?
            • 目标是有一个协议和一个扩展。不是具体的类。
            猜你喜欢
            • 2016-04-15
            • 2012-02-10
            • 1970-01-01
            • 2010-10-30
            • 2010-10-16
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多