在Swift中实现脚标功能

脚标(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)的棋子从红色变为白色。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341

推荐阅读更多精彩内容