问题描述
在练习代码时,发现UserDefaults可以存储Set类型的数据,但是UserDefaults没有合适的方法来读取Set类型的数据:
class Favorites {
private var resorts: Set<String>
private let key = "Favorites"
init() {
resorts = UserDefaults.standard.array(forKey: key) // 报错
resorts = []
}
func save() {
UserDefaults.standard.set(resorts, forKey: key)
}
}
报错内容为:
Cannot assign value of type '[Any]?' to type 'Set<String>'
表示无法将类型“[Any]?”的值分配给类型“Set<String>”。
问题原因
UserDefaults 支持的基础数据类型包括 Array、Dictionary、String、Number(Int、Double 等)和 Data,但是它不直接支持 Set。因此,当试图使用 UserDefaults.standard.array(forKey:) 加载一个 Set<String> 类型时,会发生类型不匹配。
解决方案
为了正确加载和保存 Set<String>,可以先将 Set 转换为 Array,然后在需要时再转换回 Set。
class Favorites {
private var resorts: Set<String>
private let key = "Favorites"
init() {
if let resortsSave = UserDefaults.standard.array(forKey: key) as? [String] {
resorts = Set(resortsSave) // 将 Array 转换为 Set
print("初始化成功")
} else {
// 如果没有数据,使用空的集合
resorts = []
}
}
func save() {
UserDefaults.standard.set(Array(resorts), forKey: key) // 将 Set 转换为 Array
}
}
关键改动说明
1、从 UserDefaults 加载数据时:
使用 UserDefaults.standard.array(forKey:) 加载 Array<String> 类型。
将 Array 转换为 Set:Set(resortsSave)。
2、保存数据到 UserDefaults 时:
将 Set 转换为 Array:Array(resorts)。
为什么这样解决?
Set 是一个无序集合,而 UserDefaults 支持的 Array 是有序的列表。通过在加载和保存时在 Set 和 Array 之间转换,可以保持数据的兼容性,同时利用 Set 的高效集合操作。
为什么使用as? [String]
在 Swift 中,UserDefaults 是一个通用的存储系统,但它并不会强制要求存储的数据类型与读取时完全一致。因此,as? [String] 用于安全地将从 UserDefaults 中读取的数据尝试转换为 Array<String> 类型。
原因解析
1、UserDefaults.standard.array(forKey:) 的返回值类型
array(forKey:) 返回的是一个 Any? 类型。
这意味着它可能是任何类型(如果有值)或者 nil(如果没有找到对应的键值)。
2、需要进行类型匹配
期望读取的值是 [String] 类型(一个字符串数组)。
但由于 UserDefaults 是非类型化的存储,因此必须使用类型转换。
使用 as? [String] 可以安全地尝试将返回值转换为 [String],如果转换失败,则返回 nil。
3、为什么不是直接用 as! 强制转换?
强制转换(as!)在类型不匹配时会引发运行时崩溃。
而 as? 是一种安全的方式,它会在类型不匹配时返回 nil,避免崩溃。
扩展知识
为什么 UserDefaults.standard.array(forKey:) 返回的是 Any? 类型?
UserDefaults 是一种通用存储系统,用来存储各种数据类型(如 String、Int、Bool、Array 等),但在底层,它并不直接绑定某一种特定的类型,而是使用了 Any。因此:
当调用 array(forKey:) 时,它会尝试读取与指定键相关联的值,但它的返回值是一个泛型的 Any?,表示“可能是任何类型或 nil”。
需要对它进行类型转换,以确保拿到的值符合期望的类型(在这个例子中是 [Any] 或 [String])。
func array(forKey defaultName: String) -> [Any]? // 返回的是 [Any]? 类型
设计原因:array(forKey:) 的设计假定用户可能存储的是任意数组类型,因此返回 Any,具体的类型需要用户手动确认。
为什么 UserDefaults.standard.bool(forKey:) 返回的不是 Any??
这是因为 bool(forKey:) 和其他特定方法(如 integer(forKey:))是专门为某些基本数据类型设计的便捷方法。这些方法直接返回预期类型的数据值,并提供默认值。
具体方法签名:
func bool(forKey defaultName: String) -> Bool // 直接返回 Bool 值
重要特点:
如果键存在且对应的值是 Bool,就返回该值。
如果键不存在,返回 false(默认值)。
不需要 as? 或类型转换,因为返回值已经是明确的 Bool。
设计原因:布尔值是非常常见的类型,因此专门提供了便捷方法,简化开发者操作。
为什么 array(forKey:) 和 bool(forKey:) 的行为不同?
核心原因:两者的适用场景不同
array(forKey:) 的目标
支持通用数组存储,允许存储任何类型的数组(如 [String]、[Int])。因此,它返回的是 Any?,需要手动确认类型。
bool(forKey:) 的目标
专门设计用于布尔值,只有一种可能的数据类型,因此直接返回 Bool,无需额外的类型检查。
示例对比
array(forKey:)
if let array = UserDefaults.standard.array(forKey: "myKey") as? [String] {
// 成功转换为 [String]
print(array)
} else {
// 键不存在,或者类型不匹配
print("没有找到字符串数组")
}
因为返回值是 Any?,所以需要使用 as? [String] 明确类型。
bool(forKey:)
let isOn = UserDefaults.standard.bool(forKey: "isSwitchOn")
print(isOn) // true 或 false(默认值)
不需要类型转换,因为返回值已经明确为 Bool。
如果键不存在,它会返回默认值 false。
为什么 bool(forKey:) 不需要类型检查,而 array(forKey:) 需要?
这归因于 底层数据存储的差异:
1、布尔值
布尔值是一种非常基础的数据类型,并且 UserDefaults 内部可以轻松检测和处理是否是 Bool 类型。
如果不是布尔值,它会返回默认值而不是引发崩溃。
2、数组
数组本身可能包含不同的数据类型(如 [Int]、[String]、[Any]),UserDefaults 无法明确知道存储的数组具体元素的类型。因此,它只能以最通用的 Any 类型返回。
相关文章
1、Swift UserDefaults.standard返回类型:https://fangjunyu.com/2024/12/24/swift-userdefaults-standard%e8%bf%94%e5%9b%9e%e7%b1%bb%e5%9e%8b/
2、Swift使用UserDefaults保存数据:https://fangjunyu.com/2024/05/26/swift%e4%bd%bf%e7%94%a8userdefaults%e4%bf%9d%e5%ad%98%e6%95%b0%e6%8d%ae/