本文參考原文為Implementing Custom Subscripts in Swift,歡迎閱讀原文。
下標(biāo)是一種強大的語言功能,如果使用得當(dāng),可以顯著提高代碼的調(diào)用的便利性和可讀性。在本教程中,我們將一起在playgroud中,通過構(gòu)建一個基本跳棋游戲來探索下標(biāo)。通過使用下標(biāo),你可以非常容易的在棋盤上移動一個棋子。
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類型數(shù)據(jù)構(gòu)成的Tuple,代表棋盤上一個格子的坐標(biāo)點。
我們使用Coordinate來訪問棋盤上的一個位置。squares: 是一個二維數(shù)組,用來表示棋盤上的每一個格子的狀態(tài)。
我們現(xiàn)在在playground下面加入以下兩行代碼,
var checkerboard = Checkerboard()
print(checkerboard)
我們初始化棋盤并打印它的description屬性,我們會看到如下的輸出。

2、將棋子移動到棋盤對應(yīng)的位置
我們每次移動棋子,其實是相當(dāng)于改變棋盤上面格子(square)的狀態(tài),將square的狀態(tài)設(shè)置為.Empty、.Red、.White三種狀態(tài)之一。square的狀態(tài)保存在squares中,但是根據(jù)面向?qū)ο蟮脑O(shè)計原則,我們應(yīng)該盡可能的避免向外部暴露我們內(nèi)實現(xiàn)的細節(jié),這樣才可以為我們程序修改升級保留足夠的空間,所以我們將squares數(shù)組設(shè)置為private。所以我們這個時候要增加兩個方法,一個用來訪問square當(dāng)前的狀態(tài),一個用來修改square的狀態(tài)。
我們在 Checkerboard 內(nèi)部增加如下兩個方法。
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
}
我們需要注意的是,我們這兩方法的參數(shù)使用的Coordinate類型,而不是數(shù)組的坐標(biāo),這樣可以避免暴露內(nèi)部的實現(xiàn),因為如果我們參數(shù)使用數(shù)組的坐標(biāo),但未來某一天我們不再使用數(shù)組作為定義棋盤的數(shù)據(jù)結(jié)構(gòu),那么我們這兩個方法就難以和未來的實現(xiàn)相兼容了。
現(xiàn)在我們可以更新square的狀態(tài)了,但是我們新增的這個兩個方法先得有些丑陋,他們完全不像是一個swift自有的內(nèi)容,而是被我們從外部生硬的塞進來的。
3、定義下標(biāo)(Subscripts)
我們現(xiàn)在來調(diào)整一下我們的代碼,我們是否可以使用計算屬性來重新定義這兩個方法呢?很明顯,是不行的,因為我們的方法需要參數(shù),但計算屬性是不可以有參數(shù)。但是我們可以使用下標(biāo)(Subscripts).
subscript(parameterList) -> ReturnType {
get {
// return someValue of ReturnType
}
set (newValue) {
// set someValue of ReturnType to newValue
}
}
下標(biāo)定義的語法同時具有函數(shù)定義和計算屬性定義的語法特征。
首先它像一個函數(shù)的定義,有參數(shù)列表,有返回值只是用subscript關(guān)鍵字代替了func關(guān)鍵字
它的方法體內(nèi)更像一個計算屬性的定義,包括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)點的坐標(biāo)設(shè)置為.White狀態(tài)。
let coordinate = (x: 3, y: 2)
print(checkerboard[coordinate])
checkerboard[coordinate] = .White
print(checkerboard)
我們會得到如下的輸出結(jié)果。

4、比較下標(biāo)、屬性和函數(shù)
下標(biāo)在一下幾個方面很像計算屬性:
- 它也是由 getter 和 setter方法構(gòu)成的。
- setter方法是可選的,也就是說它既可以是可讀寫的,也可以是只讀的。
- 一個只讀的下標(biāo),不需要顯示的設(shè)置get和set狀態(tài)
- 對于setter方法,它有一個默認的newValue參數(shù),類型與返回值類型相同
- 盡量使下標(biāo)操作的時間復(fù)雜度為O(1)
下標(biāo)與計算屬相最大的不同在于,下標(biāo)沒有一個屬性名,和重載操作符類似,下標(biāo)可以覆蓋swift自己的中括號[].
下標(biāo)和函數(shù)的相似之處在于有參數(shù)列表,有返回值。不同點在于:
- 下標(biāo)沒有默認的外部參數(shù)名,如果你需要使用外部參數(shù)名,那么需要顯示的指定;
- 下標(biāo)不能使用inout關(guān)鍵字,也不能使用默認參數(shù),但可以使用可變長參數(shù);
- 下標(biāo)不能throw錯誤,也就是說下標(biāo)的 getter方法必須通過返回值來表示錯誤setter方法既不能拋出錯誤也不能返回錯誤。
5、增加第二個下標(biāo)
下標(biāo)可以被重載,因此我們可以定義多個下標(biāo),只要他們有不同的參數(shù)列表或返回值就可以了。我們在Checkerboard里面新增下面的代碼。
subscript(x: Int, y: Int) -> Square {
get {
return self[(x: x, y: y)]
}
set {
self[(x: x, y: y)] = newValue
}
}
我們新增的這個方法,使用二維數(shù)組的坐標(biāo),作為參數(shù)。
現(xiàn)在你已經(jīng)掌握了swift的下標(biāo)了,嘗試尋找機會在你自己的代碼里面定義它吧,它一定會使用你的代碼更具可讀性。如果內(nèi)容中有任何錯誤,請和我聯(lián)系,謝謝。