Какая хорошая альтернатива статическим хранимым свойствам общих типов в 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
}
}