脚标(Subscripts)是高级语言的特性之一,如果使用得当,可以显著提高代码的可读性和易用性。
脚标功能的实现非常像操作符的重载,它可以让我们使用原生的结构类型构建成类似checkerBoard[2][3]这样的东东,而不是臃肿的checkerBoard.objectAt(x: 2, y: 3)函数形式。
在本教程中,我们将会通过在Playground中创建简单的跳棋游戏来探索脚标。我们将会看到使用脚标去移动棋盘上的棋子。OK!让我们开始使用脚标吧!
开始
创建全新的Playground文件,并添加下面的代码:
struct Checkerboard {
enum Square: String {
case Empty = "\u{25AA}\u{fe0f}" // Black square
case Red = "\u{1f534}" // Red piece
case White = "\u{26AA}\u{fe0f}" // White piece
}
typealias Coordinate = (x: Int, y: Int)
private var squares: [[Square]] = [
[ .Empty, .Red, .Empty, .Red, .Empty, .Red, .Empty, .Red ],
[ .Red, .Empty, .Red, .Empty, .Red, .Empty, .Red, .Empty ],
[ .Empty, .Red, .Empty, .Red, .Empty, .Red, .Empty, .Red ],
[ .Empty, .Empty, .Empty, .Empty, .Empty, .Empty, .Empty, .Empty ],
[ .Empty, .Empty, .Empty, .Empty, .Empty, .Empty, .Empty, .Empty ],
[ .White, .Empty, .White, .Empty, .White, .Empty, .White, .Empty ],
[ .Empty, .White, .Empty, .White, .Empty, .White, .Empty, .White ],
[ .White, .Empty, .White, .Empty, .White, .Empty, .White, .Empty ]
]
}
extension Checkerboard: CustomStringConvertible {
var description: String {
return squares.map {
row in row.map { $0.rawValue }.joinWithSeparator("")
}.joinWithSeparator("\n") + "\n"
}
}
Checkerboard结构包含三个定义:
- Square 代表棋盘中方块的状态。.Empty代表一个空方块。.Red和.White代表方格上的红色或白色棋子。
- Coordinate 是一个别名,用来替代含有两个整型的元组。我们将会使用这个类型访问棋盘上的方格。
- squares 是存储棋盘状态的二维数组。
代码的最后是符合** CustomStringConvertibleFinally **协议的扩展,这个扩展可以打印棋盘到控制台。
通过菜单** View/Debug Area/Show Debug Area **打开控制台,然后输入下面的代码到Playground的底部:
var checkerboard = Checkerboard()
print(checkerboard)
这段代码初始化一个** Checkerboard 类型的实例,然后打印出 description 属性的值到控制台,这个属性来自于 CustomStringConvertible **协议,控制台的输出应该类似于下面这样:
▪️🔴▪️🔴▪️🔴▪️🔴
🔴▪️🔴▪️🔴▪️🔴▪️
▪️🔴▪️🔴▪️🔴▪️🔴
▪️▪️▪️▪️▪️▪️▪️▪️
▪️▪️▪️▪️▪️▪️▪️▪️
⚪️▪️⚪️▪️⚪️▪️⚪️▪️
▪️⚪️▪️⚪️▪️⚪️▪️⚪️
⚪️▪️⚪️▪️⚪️▪️⚪️▪️
开始设置棋子
纵观控制台,我们可以非常容易的知道每个棋子都占据棋盘上的哪个位置。但是,我们的玩家并不会知道棋子的这些坐标,因为** squares 数组被标记为了 private 。这是非常重要的一点: squares 数组是棋盘的实现,然而通过 Checkerboard **我们不可以知道任何关于棋盘的实现。
结构内部实现的细节应该是对外部用户屏蔽的,因此** squares **数组要保持私有。
考虑到这点,我们将添加两个方法到** Checkerboard **结构中,用于找出和设置指定棋子的坐标。
添加下面的方法到** Checkerboard **结构,就在private var squares定义的下面:
func pieceAt(coordinate: Coordinate) -> Square {
return squares[coordinate.y][coordinate.x]
}
mutating func setPieceAt(coordinate: Coordinate, to newValue: Square) {
squares[coordinate.y][coordinate.x] = newValue
}
这样,我们就可以通过元组访问squares数组了,而不是直接访问数组。实际上,存储数组的数组机制要完全对外部用户屏蔽。
定义脚标
你可能注意到新添加的方法类似于属性的getter和setter的混合,或者应该实现一个计算属性来替代这两个方法?但不幸的是,我们不能这样做。这两个方法需要一个坐标参数,而计算属性是不能带参数的,那我们就只能使用方法了吗?
答案当然是否定的,我们可以利用脚标!看下对于脚标的定义:
subscript(parameterList) -> ReturnType {
get {
// return someValue of ReturnType
}
set (newValue) {
// set someValue of ReturnType to newValue
}
}
脚标的定义混合了函数和计算属性的定义语法:
- 第一部分看起来像函数定义,使用了参数列表和返回值。这里使用特定的** subscript 关键字替代了 func **和函数名称。
- 主体看起来像计算属性,使用了getter和setter。
函数和属性的结合突出了脚标的强大功能:提供一种快捷方式访问可索引集合的元素。一会儿我们还会对它有更多的了解,首先看下面的这个例子:
替换** pieceAt(_:) 和 setPieceAt(_:to:) **方法为脚标的形式:
subscript(coordinate: Coordinate) -> Square {
get {
return squares[coordinate.y][coordinate.x]
}
set {
squares[coordinate.y][coordinate.x] = newValue
}
}
脚标的 getter 和 setter 实现了之前被替换的方法:
- 给 getter 一个** Coordinate **,返回指定行列square。
- 给 setter 一个** Coordinate **和值,对指定行列的square设置它的值。
添加下面的代码到playground的底部:
let coordinate = (x: 3, y: 2)
print(checkerboard[coordinate])
checkerboard[coordinate] = .White
print(checkerboard)
playground 将棋盘(3, 2)的红色棋子改变为白色,并且将棋盘输出到控制台中:
▪️🔴▪️🔴▪️🔴▪️🔴
🔴▪️🔴▪️🔴▪️🔴▪️
▪️🔴▪️⚪️▪️🔴▪️🔴
▪️▪️▪️▪️▪️▪️▪️▪️
▪️▪️▪️▪️▪️▪️▪️▪️
⚪️▪️⚪️▪️⚪️▪️⚪️▪️
▪️⚪️▪️⚪️▪️⚪️▪️⚪️
⚪️▪️⚪️▪️⚪️▪️⚪️▪️
你现在可以使用** checkerboard[coordinate] **的形式找出指定坐标的棋子,并且设置它。
对比脚标、属性和函数
脚标与计算属性在很多方面类似:
- 都包含getter和setter
- setter是可选,这意味着脚标可以是读写或只读。
- 一个只读脚标不能有明确的** get 或 set **,它本身就是一个getter。
- 在setter里面,有一个默认的参数** newValue **,它的类型与脚标的返回值相同。
- 脚标的用户体验一定是比不用好记和便于使用,最好类似于A(1)。
与计算属性最大的不同就是脚标本身没有属性名称。它像** 操作符重载 **,利用脚标可以让我们重写swift语言层面的方括号[],因为方括号便于访问集合中的元素。
脚标与函数类似,因为它们都有参数列表和返回值,但是有以下几点不同:
- 脚标的参数在默认情况下没有扩展名,如果想要使用扩展名需要显式的添加。
- 脚标不能使用** inout 或者默认参数,但是 variadic (...) **是允许的。
- 脚标不能抛出error。这意味着getter报错的话要通过它的返回值,而setter的报错则不能抛错或通过返回值。
添加第二个脚标
脚标与函数的第二个相似点是它可以重载。这意味着可以创建多个脚标,只要它们有不同的参数或返回值。
添加下面的代码到现存脚标定义的下面:
subscript(x: Int, y: Int) -> Square {
get {
return self[(x: x, y: y)]
}
set {
self[(x: x, y: y)] = newValue
}
}
上面的代码添加了第二个** Checkerboard 的脚标,使用两个整型替代 Coordinate 元组。第二个脚标的实现实际上是通过调用第一个脚标的 self[(x: x, y: y)] **方法。
在playground中使用第二种脚标形式打印:
print(checkerboard[1, 2])
checkerboard[1, 2] = .White
print(checkerboard)
这里应该看到(1,2)的棋子从红色变为白色。