使用下标访问各种集合中的元素,如数组和字典,不仅在Swift中很常见,而且在几乎所有相对现代的编程语言中都是如此。但是,下标方式实际上是在Swift中实现的,它既非常独特,又非常强大 - 因为它允许我们将自己的类型的下标API添加到标准库中。
本周,我们来看看下载是如何在Swift中运行的,以及一些将它融入我们设计API的方式 - 包括一些在Swift 5.1中添加的全新功能。
同时小编这里有些书籍和面试资料哦(点击下载)
下标与方法
可以说,下标的最大好处是它在呼叫站点提供的令人难以置信的轻量级语法。而不是必须使用我们需要记住或查找的名称来调用特定方法,而下标允许我们仅使用其索引或键来检索值:
let fifthElement = array[4]
let accessToken = dictionary["token"]
复制代码
只需将上面两个API与它们的方法等价物进行比较:
let fifthElement = array.element(at: 4)
let accessToken = dictionary.value(forKey: "token")
复制代码
但是,虽然下标对于一组有点狭窄的用例非常方便,但如果在动态获取和设置值之外使用,也可能导致相当混乱的代码。例如,这行代码导致发送通知并不十分清楚:
notificationsToSend[.userUpdated] = Notification(value: user)
复制代码
由于上面的API用于执行操作,而不是分配值,因此可以说一个好的老式方法更合适:
send(Notification(value: user), forEvent: .userUpdated)
复制代码
所以下标绝对不是方法的替代品,而是一种更容易提供对一组基础值的访问的方法 - 无论是集合,数据库还是使用密钥路径访问的模型。
自定义下标
假设我们正在构建一个项目管理应用程序,让我们的用户通过将他们放在二维网格上来组织他们的任务。为了简单起见,我们将网格建模为行的有序Task
值数组,给定的水平宽度:
struct Grid {
var tasks: [Task]
var width: Int
}
复制代码
虽然可以通过tasks
直接下标数组(例如tasks[2]
第三个任务)来访问第一行中的任何任务,但是一旦我们开始垂直遍历网格,我们必须做一些基本的数学计算,以便计算对应于a的索引。给定的X和Y坐标集。为了避免代码重复和潜在错误,我们将这些计算封装在一个方法中 - 如下所示:
extension Grid {
func task(atX x: Int, y: Int) -> Task? {
// We choose to be a bit defensive here, and return nil
// for invalid coordinates, rather than crash. This is
// because we expect this code to be called within many
// different contexts across our app.
guard x >= 0 && y >= 0 && x < width else {
return nil
}
let index = x + y * width
guard index < tasks.count else {
return nil
}
return tasks[index]
}
}
复制代码
请注意,我们可以使用UInt
我们的坐标,而不是签名整数,这将节省我们的>= 0
检查。但是,这会将验证负担推向我们的API用户,因为在调用我们的API之前Int
首先必须将所有值转换为UInt
- 这将使得使用起来更加麻烦。
就像我们之前看过的假设Array
和Dictionary
API一样,上面的方法有效,但有点不必要的冗长。实际上,因为我们只是从集合中访问元素 - 所以我们也提供上述API的下标,如下所示:
extension Grid {
subscript(x: Int, y: Int) -> Task? {
return task(atX: x, y: y)
}
}
复制代码
只读下标与Swift中的方法非常相似,只是它们使用subscript
关键字,而不是func
后跟名称。
有了上述内容,我们现在可以非常轻松地访问网格中的任何任务,只需使用我们感兴趣的坐标下标即可:
func selectTask(at point: CGPoint) {
let x = Int(point.x / tileSize)
let y = Int(point.y / tileSize)
let task = grid[x, y]
select(task)
}
复制代码
下标的原因之所以如此,是因为访问网格中的图块的概念与从数组或字典中检索值非常相似 - 而且因为它很容易理解x
并y
引用坐标,而不需要任何额外的措辞。
超载
就像方法和自由函数一样,可以重载Swift下标,为不同的输入集提供不同的功能。例如,假设我们想要提供一个额外的下标API来访问我们网格中的整行任务,可以这样做:
extension Grid {
// We add a computed convenience property here to calculate
// the height of the grid (which might be asymetrical):
var height: Int {
return Int(ceil(Double(tasks.count) / Double(width)))
}
subscript(rowIndex: Int) -> ArraySlice<Task> {
guard rowIndex >= 0 && rowIndex < height else {
return []
}
let lowerBound = rowIndex * width
let upperBound = lowerBound + width
return tasks[lowerBound..<upperBound]
}
}
复制代码
上面我们返回一个ArraySlice
,而不是一个正确的Array
,以避免每次访问我们的下标时复制我们返回的任务范围。这与上周为计算属性提供恒定时间复杂度的方法非常相似。
虽然我们的新下标在上面看起来很棒,但是一旦我们开始使用它,我们会发现它在调用网站上看起来很模糊 - 因为默认情况下下标没有得到外部参数标签,使它看起来像我们只是访问它单个任务,而不是整行:
func selectTasksOnRow(withIndex index: Int) {
let tasks = Array(grid[index])
select(tasks)
}
复制代码
歧义是部署下标时最突出的风险之一,因为我们需要确保基于下标的API的所有用法都能让任何人阅读我们的代码足够的上下文来理解正在发生的事情 - 这绝对不是上面的情况。
值得庆幸的是,上述问题可以很容易修复,因为我们实际上可以将外部参数标签添加到下标中 - 如果我们想要 - 通过在函数参数之前添加自定义外部标签的完全相同的方式 - 通过在名称之前添加标签一个参数:
extension Grid {
// It's completely fine to use the same name for a parameter's
// external label as for its name.
subscript(rowIndex rowIndex: Int) -> ArraySlice<Task> {
...
}
}
复制代码
通过上面的调整,我们的调用站点现在看起来更加清晰 - 正如我们rowIndex
在使用新的下标时明确指出的那样:
func selectTasksOnRow(withIndex index: Int) {
// Here we make a deliberate choice to convert the returned
// ArraySlice into a proper Array, rather than always doing
// that whenever our subscript is accessed.
let tasks = Array(grid[rowIndex: index])
select(tasks)
}
复制代码
在我们设计任何类型的API时,在减少详细程度和仍然在呼叫站点提供足够清晰度之间达成这种平衡是一个常见的挑战,但在使用下标时尤为重要 - 因为它们默认不包括任何类型的措辞所有。
吸气剂,制定者和仿制药
下标和函数的另一个共同点是它们可以是*通用的*,这使我们能够在不会给我们带来无类型(或Any
)值的情况下保留类型安全性。
作为示例,让我们看一下如何使用该功能来提高常用UserDefaults
系统API 的类型安全性。
我们将从UserDefaults
泛型Key
类型开始,它带有Value
我们正在寻找的类型(几乎像幻像类型)。我们还将添加一个读写下标,它允许我们以类型安全的方式检索和存储值:
extension UserDefaults {
struct Key<Value> {
var name: String
}
subscript<T>(key: Key<T>) -> T? {
get {
return value(forKey: key.name) as? T
}
set {
setValue(newValue, forKey: key.name)
}
}
}
复制代码
newValue
我们set
块中出现的上述变量由编译器自动生成,并表示使用我们的下标分配的新值 - 与使用属性观察器时完全相同。
有了上述内容,我们现在可以UserDefaults.Key
使用计算*的类似工厂的属性*来扩展以创建我们的密钥 - 每个密钥与其相应值的确切类型相关联,从而为我们提供完整的类型安全性:
extension UserDefaults.Key {
static var bookmarks: UserDefaults.Key<[String]> {
return .init(name: "bookmarks")
}
static var notificationSnoozed: UserDefaults.Key<Bool> {
return .init(name: "notificationSnoozed")
}
}
复制代码
由于静态属性可以与点语法一起使用,因此我们现在可以轻松使用这样的UserDefaults
值:
class SettingsViewModel {
private let userDefaults: UserDefaults
init(userDefaults: UserDefaults = .standard) {
self.userDefaults = userDefaults
}
func snoozeNotifications() {
userDefaults[.notificationSnoozed] = true
}
}
复制代码
太酷了!但也许更酷的是,尝试存储不正确类型的值现在会给我们一个编译器错误:
// Error: Cannot assign value of type 'String' to type 'Bool?'
userDefaults[.notificationSnoozed] = "yep!"
复制代码
我们还可以通过添加接受默认值的第二个重载来为我们的新类型安全的下标API提供额外的功能 - 就像Dictionary
工作方式一样:
extension UserDefaults {
subscript<T>(
key: Key<T>,
default defaultProvider: @autoclosure () -> T
) -> T {
get {
return value(forKey: key.name) as? T
?? defaultProvider()
}
set {
setValue(newValue, forKey: key.name)
}
}
}
复制代码
上面我们使用@autoclosure
以避免必须评估默认值表达式,除非需要 - 这可以帮助提高重度操作的性能,并降低意外副作用的风险。有关更多信息,请查看“在设计Swift API时使用@autoclosure”。
使用我们的新下标变体,我们现在可以轻松地执行诸如将元素附加到存储在其中的数组之类的操作UserDefaults
,或者根据需要创建新数组- 所有这些都在一行代码中:
func addBookmark(named name: String) {
userDefaults[.bookmarks, default: []].append(name)
}
复制代码
同样,我们必须提供足够的上下文和措辞,以明确我们的订阅API所做的事情,遵循相同的约定,Dictionary
在这方面肯定有帮助,因为任何熟悉该订阅API的人都很有可能理解我们要做的事情对于上述。
静态下标
最后,让我们看一下作为Swift 5.1的一部分引入的新特性 - 静态下标 - 它与实例下标的工作方式大致相同,只是它们使我们能够直接对类型本身下标。
例如,我们可以使用这个新功能来提供专用类型来访问调用工具或脚本时传递的命令行参数和环境变量:
struct Arguments {
static subscript(index: Int) -> String? {
let arguments = CommandLine.arguments
guard index < arguments.count - 1 else {
return nil
}
// We discard the first command line argument here,
// since it contains the execution path of our program.
return arguments[index + 1]
}
}
struct Environment {
static subscript(key: String) -> String? {
return ProcessInfo.processInfo.environment[key]
}
}
复制代码
由于上述两个数据在程序中具有内在的通用性,因此只需通过下载我们的新类型Arguments
和Environment
类型,就可以非常方便地访问它们而不必担心传递实例- 如下所示:
let sourcePath = Arguments[0]
let targetPath = Arguments[1]
let apiToken = Environment["API_TOKEN"]
复制代码
虽然能够下标类型非常酷,但重要的是不要将特定于上下文的数据放在静态上下文中,因为这样做会大大降低代码中的可测试性和关注点分离 - 这与单身人士经常遇到的问题相同原因。
结论 是什么让Swift的许多功能 - 包括下标 - 如此强大,不仅仅是将有限数量的用例硬编码到编译器或标准库中,任何类型都可以采用它们。
特别是在构建自定义集合或使用任何其他值组时,使用下标可以让我们设计真正简洁和轻量级的API。但是,仔细考虑基于下标的API是否在每种给定情况下提供足够的上下文非常重要 - 如果不是,则方法可能是更好的选择。
你怎么看?您以前使用过自定义下标,还是会尝试一下?请通过加我们的交流群 点击此处进交流群 ,来一起交流或者发布您的问题,意见或反馈。