NSUserDefaults в Swift - реализация безопасности типа

Одна из вещей, которые меня беспокоят о Swift и Cocoa вместе, работает с NSUserDefaults, потому что нет информации о типе, и всегда необходимо передать результат objectForKey тому, что вы ожидаете получить. Это небезопасно и непрактично. Я решил решить эту проблему, сделав NSUserDefaults более практичным в Swift-land и, надеюсь, чему-то научиться на этом пути. Здесь были мои цели в начале:

  • Полная безопасность типов: каждый ключ имеет один тип, связанный с ним. При настройке значения должно приниматься только значение этого типа, и при получении значения результат должен выдаваться с правильным типом
  • Глобальный список ключей, которые понятны по смыслу и содержанию. Список должен быть простым в создании, изменении и расширении.
  • Очистить синтаксис, используя по возможности индексы. Например, это быть совершенным:

    3,1. set: UserDefaults [.MyKey] = значение

    3,2. get: let value = UserDefaults [.MyKey]

  • Поддержка классов, соответствующих протоколу NSCoding, автоматически [un] архивирование их

  • Поддержка всех типов списков свойств, принятых NSUserDefaults

Я начал с создания этой общей структуры:

struct UDKey <T>  {
    init(_ n: String) { name = n }
    let name: String
}

Затем я создал эту другую структуру, которая служит контейнером для всех ключей в приложении:

struct UDKeys {}

Затем это может быть расширено, чтобы добавлять ключи там, где это необходимо:

extension UDKeys {
    static let MyKey1 = UDKey<Int>("MyKey1")
    static let MyKey2 = UDKey<[String]>("MyKey2")
}

Обратите внимание, что каждый ключ имеет тип, связанный с ним. Он представляет тип информации для сохранения. Кроме того, свойство name является строкой, которая должна использоваться как ключ для NSUserDefaults.

Ключи могут быть перечислены все в одном файле констант или добавлены с использованием расширений для каждого файла в непосредственной близости от того места, где они используются для хранения данных.

Затем я создал класс UserDefaults, ответственный за обработку/настройку информации:

class UserDefaultsClass {
    let storage = NSUserDefaults.standardUserDefaults()
    init(storage: NSUserDefaults) { self.storage = storage }
    init() {}

    // ...
}

let UserDefaults = UserDefaultsClass() // or UserDefaultsClass(storage: ...) for further customisation

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

let value = UserDefaults.myMethod(...)

Я предпочитаю такой подход к таким вещам, как UserDefaults.sharedInstance.myMethod(...) (слишком долго!) или используя методы класса для всего. Кроме того, это позволяет одновременно взаимодействовать с различными доменами, используя более одного UserDefaultsClass с различными значениями хранения.

До сих пор были рассмотрены пункты 1 и 2, но теперь начинается сложная часть: как на самом деле разрабатывать методы на UserDefaultsClass, чтобы соответствовать остальным.

Например, начнем с пункта 4. Сначала я попробовал это (этот код находится внутри UserDefaultsClass):

subscript<T: NSCoding>(key: UDKey<T>) -> T? {
    set { storage.setObject(NSKeyedArchiver.archivedDataWithRootObject(newValue), forKey: key.name) }
    get {
        if let data = storage.objectForKey(key.name) as? NSData {
            return NSKeyedUnarchiver.unarchiveObjectWithData(data) as? T
        } else { return nil }
    }
}

Но потом я узнаю, что Swift не разрешает общие индексы!! Хорошо, тогда я предполагаю, что мне придется использовать функции. Там идет половина пункта 3...

func set <T: NSCoding>(key: UDKey<T>, _ value: T) {
    storage.setObject(NSKeyedArchiver.archivedDataWithRootObject(value), forKey: key.name)
}
func get <T: NSCoding>(key: UDKey<T>) -> T? {
    if let data = storage.objectForKey(key.name) as? NSData {
        return NSKeyedUnarchiver.unarchiveObjectWithData(data) as? T
    } else { return nil }
}

И это прекрасно работает:

extension UDKeys { static let MyKey = UDKey<NSNotification>("MyKey") }

UserDefaults.set(UDKeys.MyKey, NSNotification(name: "Hello!", object: nil))
let n = UserDefaults.get(UDKeys.MyKey)

Обратите внимание, как я не могу позвонить UserDefaults.get(.MyKey). Я должен использовать UDKeys.MyKey. И я не могу этого сделать, потому что пока не существует статических переменных в общей структуре!

Затем попробуйте номер 5. Теперь это была головная боль и что там, где мне нужна большая помощь.

Типы списков свойств относятся к документам:

Объектом по умолчанию должен быть список свойств, то есть экземпляр (или для коллекций - комбинация экземпляров): NSData, NSString, NSNumber, NSDate, NSArray или NSDictionary.

Что в Swift означает Int, [Int], [[String:Bool]], [[String:[Double]]] и т.д. все типы списка свойств. Сначала я подумал, что могу просто написать это и доверять тому, кто использует этот код, чтобы помнить, что разрешены только типы plist:

func set <T: AnyObject>(key: UDKey<T>, _ value: T) {
    storage.setObject(value, forKey: key.name)
}
func get <T: AnyObject>(key: UDKey<T>) -> T? {
    return storage.objectForKey(key.name) as? T
}

Но как вы заметите, пока это прекрасно работает:

extension UDKeys { static let MyKey = UDKey<NSData>("MyKey") }

UserDefaults.set(UDKeys.MyKey, NSData())
let d = UserDefaults.get(UDKeys.MyKey)

Это не означает:

extension UDKeys { static let MyKey = UDKey<[NSData]>("MyKey") }
UserDefaults.set(UDKeys.MyKey, [NSData()])

И это не так:

extension UDKeys { static let MyKey = UDKey<[Int]>("MyKey") }
UserDefaults.set(UDKeys.MyKey, [0])

Даже это:

extension UDKeys { static let MyKey = UDKey<Int>("MyKey") }
UserDefaults.set(UDKeys.MyKey, 1)

Проблема в том, что все они являются допустимыми типами списков свойств, но Swift явно интерпретирует массивы и ints как структуры, а не как их сопоставления классов Objective-C. Однако:

func set <T: Any>(key: UDKey<T>, _ value: T)

тоже не будет работать, потому что тогда принимается любой тип значения, а не только те, у кого есть кузена класса Obj-C, а storage.setObject(value, forKey: key.name) уже недействителен, поскольку значение должно быть ссылочным типом.

Если в Swift существовал протокол, который принимал любой ссылочный тип и любой тип значения, который может быть преобразован в ссылочный тип в Objective-C (например, [Int] и другие примеры, которые я упоминаю), эта проблема будет решена:

func set <T: AnyObjectiveCObject>(key: UDKey<T>, _ value: T) {
    storage.setObject(value, forKey: key.name)
}
func get <T: AnyObjectiveCObject>(key: UDKey<T>) -> T? {
    return storage.objectForKey(key.name) as? T
}

AnyObjectiveCObject будет принимать любые быстрые классы и быстрые массивы, словари, числа (ints, float, bools и т.д., которые преобразуются в NSNumber), строки...

К сожалению, AFAIK этого не существует.

Вопрос:

Как я могу написать общую функцию (или набор перегруженных общих функций), общий тип T которой может быть любым ссылочным типом или любым типом значения, который Swift может преобразовать в ссылочный тип в Objective-C?


Решено:. С помощью полученных ответов я пришел к тому, что хотел. Если кто-то хочет взглянуть на мое решение, здесь, это так.

Ответ 1

Я не хочу хвастаться, но... о, кого я шучу, я полностью делает!

Preferences.set([NSData()], forKey: "MyKey1")
Preferences.get("MyKey1", type: type([NSData]))
Preferences.get("MyKey1") as [NSData]?

func crunch1(value: [NSData])
{
    println("Om nom 1!")
}

crunch1(Preferences.get("MyKey1")!)

Preferences.set(NSArray(object: NSData()), forKey: "MyKey2")
Preferences.get("MyKey2", type: type(NSArray))
Preferences.get("MyKey2") as NSArray?

func crunch2(value: NSArray)
{
    println("Om nom 2!")
}

crunch2(Preferences.get("MyKey2")!)

Preferences.set([[String:[Int]]](), forKey: "MyKey3")
Preferences.get("MyKey3", type: type([[String:[Int]]]))
Preferences.get("MyKey3") as [[String:[Int]]]?

func crunch3(value: [[String:[Int]]])
{
    println("Om nom 3!")
}

crunch3(Preferences.get("MyKey3")!)

Ответ 2

Я хотел бы представить свою идею. (Извините за мой бедный английский язык заранее.)

let plainKey = UDKey("Message", string)
let mixedKey
    = UDKey("Mixed"
        , array(dictionary(
            string, tuple(
                array(integer),
                optional(date)))))

let ud = UserDefaults(NSUserDefaults.standardUserDefaults())
ud.set(plainKey, "Hello")
ud.set(plainKey, 2525) // <-- compile error
ud.set(mixedKey, [ [ "(^_^;)": ([1, 2, 3], .Some(NSDate()))] ])
ud.set(mixedKey, [ [ "(^_^;)": ([1, 2, 3], .Some(NSData()))] ]) // <-- compile error

Единственное отличие состоит в том, что UDKey() теперь требует аргумент # 2, значение класса BiMap. Я отсоединил работу из UDKey в BiMap, которая преобразует значение типа в/из значения другого типа.

public class BiMap<A, B> {
    public func AtoB(a: A) -> B?
    public func BtoA(b: B) -> A?
}

Следовательно, типы, которые set/get могут принимать, выполняются BiMap и больше не ограничены типами as, которые могут автоматически отбрасываться  от/до AnyObject (точнее, типы NSUserDefaults могут принимать).

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

Вот полный исходный код. (Но есть ошибки еще не исправлены..) https://gist.github.com/hisui/47f170a9e193168dc946