Какая хорошая альтернатива статическим хранимым свойствам общих типов в swift?

Так как статические хранимые свойства пока не поддерживаются для родовых типов в swift, интересно, какая хорошая альтернатива.

Мой конкретный вариант использования - это то, что я хочу построить ORM в быстром. У меня есть протокол Entity, который имеет связанный тип для первичного ключа, поскольку некоторые сущности будут иметь целое число как их id, а некоторые будут иметь строку и т.д. Таким образом, общий протокол Entity.

Теперь у меня также есть тип EntityCollection<T: Entity>, который управляет коллекциями сущностей, и, как вы можете видеть, он также является общим. Цель EntityCollection заключается в том, что он позволяет вам использовать коллекции объектов, как если бы они были нормальными массивами, не зная, что там есть база данных. EntityCollection будет заботиться о запросе и кешировании и быть максимально оптимизированным.

Я хотел использовать статические свойства в EntityCollection для хранения всех объектов, которые уже были извлечены из базы данных. Таким образом, если два отдельных экземпляра EntityCollection хотят получить один и тот же объект из базы данных, база данных будет запрашиваться только один раз.

Вы, ребята, знаете, как еще я мог это достичь?

Ответ 1

Причина, по которой Swift в настоящее время не поддерживает статические хранимые свойства в универсальных типах, заключается в том, что для каждой специализации универсальных заполнителей требуется отдельное хранилище свойств - об этом подробнее поговорим в этих вопросах и ответах.

Однако мы можем реализовать это сами с помощью глобального словаря (помните, что статические свойства - это не что иное, как глобальные свойства, помещенные в пространство имен данного типа). Есть несколько препятствий, которые нужно преодолеть в этом.

Первое препятствие заключается в том, что нам нужен тип ключа. В идеале это будет значение метатипа для универсального заполнителя (ей) типа; однако метатипы в настоящее время не могут соответствовать протоколам, и поэтому не являются Hashable. Чтобы это исправить, мы можем создать оболочку:

/// 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] значение для T.self ключа.


Однако здесь существует потенциальная проблема производительности, связанная с использованием методов получения и установки; значения массива пострадают от копирования при мутации (мутация вызывает геттер для получения временного массива, этот массив мутирует, а затем вызывается сеттер).

(надеюсь, мы скоро получим обобщенные адреса)

В зависимости от того, влияет ли это на производительность, вы можете реализовать статический метод для выполнения мутации значений массива на месте:

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(_:_:) здесь, чтобы гарантировать, что у нас есть доступ на _loadedEntities к _loadedEntities во всей полноте с withLoadedEntities(_:) чтобы гарантировать, что 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 и многие другие сервисы, унаследованные от него, только с одним статическим экземпляром. И проблема в том, что все службы используют свою собственную модель (например: UserService с использованием UserModel..)

Короче я пробовал следующий код. И это работает!

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

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

    private init() {}
}

Надеюсь, это поможет.

Я думаю, что трюк сам BaseService не будет использоваться напрямую, поэтому НЕТ НУЖНО ИМЕТЬ статическое хранимое свойство. (P.S. Желаю быструю поддержку абстрактного класса, BaseService должен быть)

Ответ 4

Оказывается, что, хотя свойства недопустимы, методы и вычисленные свойства. Итак, вы можете сделать что-то вроде этого:

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

Или:

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

Ответ 5

Все, что я могу придумать, - это выделить понятие источника (откуда происходит сбор), а затем сама коллекция. И затем сделайте источник ответственным за кеширование. В этот момент источник может быть экземпляром, поэтому он может хранить все необходимые кэш файлы, и ваш 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 { ... }

class WebEntityCollection: SequenceType {...}

будет работать, если у вас есть типичный веб-интерфейс веб-данных. Затем вы можете сделать что-то в соответствии с:

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

Ответ 6

Это не идеально, но это решение, которое я придумал, чтобы соответствовать моим потребностям.

Я использую неуниверсальный класс для хранения данных. В моем случае я использую его для хранения синглетонов. У меня есть следующий класс:

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 берет универсальный 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.

Я решил реализовать это, сделав синглтон статическим для каждого общего экземпляра, но это может или не может быть вашим намерением или потребностью.

Ответ 7

В зависимости от того, сколько типов вам нужно поддерживать, и от того, подходит ли вам наследование (не), условное соответствие также может помочь:

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

Ответ 8

Что-то вроде этого?

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