本文参考原文为Implementing Custom Subscripts in Swift,欢迎阅读原文。
下标是一种强大的语言功能,如果使用得当,可以显著提高代码的调用的便利性和可读性。在本教程中,我们将一起在playgroud中,通过构建一个基本跳棋游戏来探索下标。通过使用下标,你可以非常容易的在棋盘上移动一个棋子。
1、开始行动
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: typealias一个有两个Int类型数据构成的Tuple,代表棋盘上一个格子的坐标点。
我们使用Coordinate来访问棋盘上的一个位置。 -
squares: 是一个二维数组,用来表示棋盘上的每一个格子的状态。
我们现在在playground下面加入以下两行代码,
var checkerboard = Checkerboard()
print(checkerboard)
我们初始化棋盘并打印它的description属性,我们会看到如下的输出。
2、将棋子移动到棋盘对应的位置
我们每次移动棋子,其实是相当于改变棋盘上面格子(square)的状态,将square的状态设置为.Empty、.Red、.White三种状态之一。square的状态保存在squares中,但是根据面向对象的设计原则,我们应该尽可能的避免向外部暴露我们内实现的细节,这样才可以为我们程序修改升级保留足够的空间,所以我们将squares数组设置为private。所以我们这个时候要增加两个方法,一个用来访问square当前的状态,一个用来修改square的状态。
我们在 Checkerboard 内部增加如下两个方法。
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
}
我们需要注意的是,我们这两方法的参数使用的Coordinate类型,而不是数组的坐标,这样可以避免暴露内部的实现,因为如果我们参数使用数组的坐标,但未来某一天我们不再使用数组作为定义棋盘的数据结构,那么我们这两个方法就难以和未来的实现相兼容了。
现在我们可以更新square的状态了,但是我们新增的这个两个方法先得有些丑陋,他们完全不像是一个swift自有的内容,而是被我们从外部生硬的塞进来的。
3、定义下标(Subscripts)
我们现在来调整一下我们的代码,我们是否可以使用计算属性来重新定义这两个方法呢?很明显,是不行的,因为我们的方法需要参数,但计算属性是不可以有参数。但是我们可以使用下标(Subscripts).
subscript(parameterList) -> ReturnType {
get {
// return someValue of ReturnType
}
set (newValue) {
// set someValue of ReturnType to newValue
}
}
下标定义的语法同时具有函数定义和计算属性定义的语法特征。
-
首先它像一个函数的定义,有参数列表,有返回值只是用subscript关键字代替了func关键字
-
它的方法体内更像一个计算属性的定义,包括getter和setter方法
我们用下面的代码替换掉pieceAt和setPieceAt两个方法。
subscript(coordinate: Coordinate) -> Square {
get {
return squares[coordinate.y][coordinate.x]
}
set {
squares[coordinate.y][coordinate.x] = newValue
}
}
然后我们在playground的末尾增加下面的代码,将(3,2)点的坐标设置为.White状态。
let coordinate = (x: 3, y: 2)
print(checkerboard[coordinate])
checkerboard[coordinate] = .White
print(checkerboard)
我们会得到如下的输出结果。
4、比较下标、属性和函数
下标在一下几个方面很像计算属性:
- 它也是由 getter 和 setter方法构成的。
- setter方法是可选的,也就是说它既可以是可读写的,也可以是只读的。
- 一个只读的下标,不需要显示的设置get和set状态
- 对于setter方法,它有一个默认的newValue参数,类型与返回值类型相同
- 尽量使下标操作的时间复杂度为O(1)
下标与计算属相最大的不同在于,下标没有一个属性名,和重载操作符类似,下标可以覆盖swift自己的中括号[].
下标和函数的相似之处在于有参数列表,有返回值。不同点在于:
- 下标没有默认的外部参数名,如果你需要使用外部参数名,那么需要显示的指定;
- 下标不能使用inout关键字,也不能使用默认参数,但可以使用可变长参数;
- 下标不能throw错误,也就是说下标的 getter方法必须通过返回值来表示错误setter方法既不能抛出错误也不能返回错误。
5、增加第二个下标
下标可以被重载,因此我们可以定义多个下标,只要他们有不同的参数列表或返回值就可以了。我们在Checkerboard里面新增下面的代码。
subscript(x: Int, y: Int) -> Square {
get {
return self[(x: x, y: y)]
}
set {
self[(x: x, y: y)] = newValue
}
}
我们新增的这个方法,使用二维数组的坐标,作为参数。
现在你已经掌握了swift的下标了,尝试寻找机会在你自己的代码里面定义它吧,它一定会使用你的代码更具可读性。如果内容中有任何错误,请和我联系,谢谢。
网友评论