Swift 中的 NSUserDefaults - 实现类型安全

NSUserDefaults in Swift - implementing type safety

关于 Swift 和 Cocoa 的问题之一是使用 NSUserDefaults,因为没有类型信息并且总是需要转换 [=26= 的结果] 到你期望得到的。这是不安全和不切实际的。我决定解决这个问题,使 NSUserDefaults 在 Swift 领域更加实用,并希望在此过程中学到一些东西。这是我一开始的目标:

  1. 完全的类型安全:每个键都有一个与之关联的类型。设置值时,只应接受该类型的值,获取值时,结果应具有正确的类型
  2. 全局键列表,含义和内容清晰。该列表应该易于创建、修改和扩展
  3. 语法简洁,尽可能使用下标。例如,这将 完美:

    3.1。设置:UserDefaults[.MyKey] = value

    3.2。得到:让值 = UserDefaults[.MyKey]

  4. 支持符合NSCoding协议的classes 自动[取消]归档它们

  5. 支持 NSUserDefaults
  6. 接受的所有 属性 列表类型

我首先创建了这个通用结构:

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

请注意每个键如何关联一个类型。它表示要保存的信息的类型。此外,名称 属性 是用作 NSUserDefaults 键的字符串。

键可以全部列在一个常量文件中,或者在每个文件的基础上使用扩展名添加到靠近它们用于存储数据的位置。

然后我创建了一个"UserDefaults"class负责处理getting/setting的信息:

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(...)(太长!)或对所有内容都使用 class 方法,我更喜欢这种方法。此外,这允许通过使用具有不同存储值的多个 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 个。现在这很头疼,这就是我需要很多帮助的地方。

根据文档,

属性 列表类型是:

A default object must be a property list, that is, an instance of (or for collections a combination of instances of): NSData, NSString, NSNumber, NSDate, NSArray, or 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 显然将数组和整数解释为结构,而不是它们的 Objective-C class 对应物。然而:

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

也不会工作,因为任何值类型,而不仅仅是那些具有 Obj-C 的 class 表亲的值类型,都会被接受,并且 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 会接受任何 swift classes 和 swift 数组、字典、数字(转换为 NSNumber 的整数、浮点数、布尔值等)、字符串...

不幸的是,AFAIK 这不存在。

问题:

我如何编写泛型函数(或重载泛型函数的集合),其泛型类型 T 可以是任何引用类型任何 Swift 可以转换为 Objective-C?

中的引用类型的类型

已解决: 在我得到的答案的帮助下,我得到了我想要的。如果有人想看一下我的解决方案,here 是。

我想介绍一下我的想法。 (提前为我糟糕的英语道歉。)

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 class。我已经将 UDKey 最初的工作分离到 BiMap 中,后者将类型 to/from 的值转换为另一种类型的值。

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

因此,set/get可以接受的类型由BiMap进行,不再局限于as可以自动转换的类型 from/to AnyObject(更具体地说,类型 NSUserDefaults 可以接受。)。

因为 BiMap 是一个泛型 class,您可以轻松地创建它的子类型,交换您想要的任意两种类型。

这里是完整的源代码。 (但还有一些错误尚未修复..) https://gist.github.com/hisui/47f170a9e193168dc946

我不是要吹牛,但是...哦,我在跟谁开玩笑,我 totally do

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