Swift-Protocols协议

协议 定义了一个蓝图,规定了用来实现某一特定工作或者功能所必需的方法和属性。类,结构体或枚举类型都可以遵循协议,并提供具体实现来完成协议定义的方法和功能。任意能够满足协议要求的类型被称为 遵循(conform) 这个协议。
除了遵循协议的类型必须实现那些指定的规定以外,还可以对协议进行扩展,实现一些特殊的规定或者一些附加的功能,使得遵循的类型能够收益。

协议的语法

协议的定义方式与类,结构体,枚举的定义非常相似。

protocol SomeProtocol {
  // 协议内容
}

要使类遵循某个协议,需要在类型名称后加上协议名称,中间以冒号 : 分隔,作为类型定义的一部分。遵循多个协议时,各协议之间用逗号 , 分隔。

struct SomeStructure: FirstProtocol, AnotherProtocol {
  // 结构体内容
}

如果类在遵循协议的同时拥有父类,应该将父类名放在协议名之前,以逗号分隔。

class SomeClass: SomeSuperClass, FirstProtocol, AnotherProtocol {
  // 类的内容
}

对属性的规定

协议可以规定其 遵循者 提供特定名称和类型的 实例属性(instance property) 或 类属性(type property) ,而不用指定是 存储型属性(stored property) 还是 计算型属性(calculate property) 。此外还必须指明是只读的还是可读可写的。
如果协议规定属性是可读可写的,那么这个属性不能是常量或只读的计算属性。如果协议只要求属性是只读的(gettable),那个属性不仅可以是只读的,如果你代码需要的话,也可以是可写的。
协议中的通常用var来声明变量属性,在类型声明后加上 { set get } 来表示属性是可读可写的,只读属性则用 {get } 来表示。

protocol SomeProtocol {
  var mustBeSettable : Int { get set }
  var doesNotNeedToBeSettable: Int { get }
}

在协议中定义类属性(type property)时,总是使用 static 关键字作为前缀。当协议的遵循者是类时,可以使用class 或 static 关键字来声明类属性:

protocol AnotherProtocol {
  static var someTypeProperty: Int { get set }
}

如下所示,这是一个含有一个实例属性要求的协议:

protocol FullyNamed {
  var fullName: String { get }
}

FullyNamed 协议除了要求协议的遵循者提供全名属性外,对协议对遵循者的类型并没有特别的要求。这个协议表示,任何遵循 FullyNamed 协议的类型,都具有一个可读的 String 类型实例属性 fullName 。下面是一个遵循 FullyNamed 协议的简单结构体:

struct Person: FullyNamed{
  var fullName: String
}
let john = Person(fullName: "John Appleseed")
//john.fullName 为 "John Appleseed"

这个例子中定义了一个叫做 Person 的结构体,用来表示具有名字的人。从第一行代码中可以看出,它遵循了 FullyNamed 协议。Person 结构体的每一个实例都有一个 String 类型的存储型属性 fullName 。这正好满足了 FullyNamed 协议的要求,也就意味着, Person 结构体完整的 遵循 了协议。(如果协议要求未被完全满足,在编译时会报错)下面是一个更为复杂的类,它采用并遵循了 FullyNamed 协议:

class Starship: FullyNamed {
  var prefix: String?
  var name: Stringinit(name: String, prefix: String? = nil) {
    self.name = name
    self.prefix = prefix
  }
  var fullName: String {
    return (prefix != nil ? prefix! + " " : "") + name
  }
 }
  var ncc1701 = Starship(name: "Enterprise", prefix: "USS")
  // ncc1701.fullName 是 "USS Enterprise"

Starship 类把 fullName 属性实现为只读的计算型属性。每一个 Starship 类的实例都有一个名为 name 的属性和一个名为 prefix 的可选属性。 当 prefix 存在时,将 prefix 插入到 name 之前来为Starship构建 fullName , prefix 不存在时,则将直接用 name 构建 fullName 。

对方法的规定

协议可以要求其遵循者实现某些指定的实例方法或类方法。这些方法作为协议的一部分,像普通的方法一样放在协议的定义中,但是不需要大括号和方法体。可以在协议中定义具有可变参数的方法,和普通方法的定义方式相同。但是在协议的方法定义中,不支持参数默认值。
正如对属性的规定中所说的,在协议中定义类方法的时候,总是使用 static 关键字作为前缀。当协议的遵循者是类的时候,你可以在类的实现中使用 class 或者 static 来实现类方法:

protocol SomeProtocol {
  static func someTypeMethod()
}

下面的例子定义了含有一个实例方法的协议:

protocol RandomNumberGenerator {
  func random() -> Double
}

RandomNumberGenerator 协议要求其遵循者必须拥有一个名为 random , 返回值类型为 Double 的实例方法。尽管这里并未指明,但是我们假设返回值在[0,1)区间内。
RandomNumberGenerator 协议并不在意每一个随机数是怎样生成的,它只强调这里有一个随机数生成器。如下所示,下边的是一个遵循了 RandomNumberGenerator 协议的类。该类实现了一个叫做线性同余生成器(linearcongruential generator)的伪随机数算法。

class LinearCongruentialGenerator: RandomNumberGenerator {
  var lastRandom = 42.0
  let m = 139968.0
  let a = 3877.0
  let c = 29573.0
  func random() -> Double {
    lastRandom = ((lastRandom * a + c) % m)
    return lastRandom / m
  }
}
let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// 输出 : "Here's a random number: 0.37464991998171"
print("And another one: \(generator.random())")
// 输出 : "And another one: 0.729023776863283"

委托(代理)模式

委托是一种设计模式,它允许 类 或 结构体 将一些需要它们负责的功能 交由(或委托) 给其他的类型的实例。委托模式的实现很简单: 定义协议来封装那些需要被委托的函数和方法,使其 遵循者 拥有这些被委托的 函数和方法 。委托模式可以用来响应特定的动作或接收外部数据源提供的数据,而无需要知道外部数据源的类型信息。下面的例子是两个基于骰子游戏的协议:

protocol DiceGame {
  var dice: Dice { get }
  func play()
}
protocol DiceGameDelegate {
  func gameDidStart(game: DiceGame)
  func game(game: DiceGame, didStartNewTurnWithDiceRoll diceRoll:Int)
  func gameDidEnd(game: DiceGame)
}

DiceGame 协议可以在任意含有骰子的游戏中实现。 DiceGameDelegate 协议可以用来追踪 DiceGame 的游戏过程。如下所示, SnakesAndLadders 是 Snakes and Ladders游戏的新版本。新版本使用 Dice 作为骰子,并且实现了 DiceGame 和 DiceGameDelegate 协议,后者用来记录游戏的过程:

class SnakesAndLadders: DiceGame {
  let finalSquare = 25
  let dice = Dice(sides: 6, generator: LinearCongruentialGenerator())
  var square = 0
  var board: [Int]
  init() {
    board = [Int](count: finalSquare + 1, repeatedValue: 0)
    board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
    board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
  }
  var delegate: DiceGameDelegate?
  func play() {
    square = 0
    delegate?.gameDidStart(self)
    gameLoop: while square != finalSquare {
      let diceRoll = dice.roll()
      delegate?.game(self,didStartNewTurnWithDiceRoll: diceRoll)
      switch square + diceRoll {
        case finalSquare:
          break gameLoop
        case let newSquare where newSquare > finalSquare:
          continue gameLoop
        default:
          square += diceRoll
          square += board[square]
       }
  }
  delegate?.gameDidEnd(self)
  }
}

这个版本的游戏封装到了 SnakesAndLadders 类中,该类遵循了 DiceGame 协议,并且提供了相应的可读的 dice 属性和 play 实例方法。( dice 属性在构造之后就不再改变,且协议只要求 dice 为只读的,因此将 dice 声明为常量属性。)
游戏使用 SnakesAndLadders 类的 构造器(initializer) 初始化游戏。所有的游戏逻辑被转移到了协议中的 play 方法, play 方法使用协议规定的 dice 属性提供骰子摇出的值。
注意: delegate 并不是游戏的必备条件,因此 delegate 被定义为遵循 DiceGameDelegate 协议的可选属性。因为 delegate 是可选值,因此在初始化的时候被自动赋值为 nil 。随后,可以在游戏中为 delegate 设置适当的值。
DicegameDelegate 协议提供了三个方法用来追踪游戏过程。被放置于游戏的逻辑中,即 play() 方法内。分别在游戏开始时,新一轮开始时,游戏结束时被调用。
因为 delegate 是一个遵循 DiceGameDelegate 的可选属性,因此在 play() 方法中使用了 可选链 来调用委托方法。 若 delegate 属性为 nil , 则delegate所调用的方法失效,并不会产生错误。若 delegate 不为 nil ,则方法能够被调用
如下所示, DiceGameTracker 遵循了 DiceGameDelegate 协议:

class DiceGameTracker: DiceGameDelegate {
  var numberOfTurns = 0
  func gameDidStart(game: DiceGame) {
    numberOfTurns = 0
    if game is SnakesAndLadders {
      print("Started a new game of Snakes and Ladders")
    }
    print("The game is using a \(game.dice.sides)-sided dice")
    }
  func game(game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int) {
    ++numberOfTurns
    print("Rolled a \(diceRoll)")
  }
  func gameDidEnd(game: DiceGame) {
    print("The game lasted for \(numberOfTurns) turns")
  }
}

DiceGameTracker 实现了 DiceGameDelegate 协议规定的三个方法,用来记录游戏已经进行的轮数。 当游戏开始时, numberOfTurns 属性被赋值为0; 在每新一轮中递增; 游戏结束后,输出打印游戏的总轮数。
gameDidStart 方法从 game 参数获取游戏信息并输出。 game 在方法中被当做 DiceGame 类型而不是 SnakeAndLadders 类型,所以方法中只能访问 DiceGame 协议中的成员。当然了,这些方法也可以在类型转换之后调用。在上例代码中,通过 is 操作符检查 game 是否为 SnakesAndLadders 类型的实例,如果是,则打印出相应的内容。
无论当前进行的是何种游戏, game 都遵循 DiceGame 协议以确保 game 含有 dice 属性,因此在 gameDidStart(_:) 方法中可以通过传入的 game 参数来访问 dice 属性,进而打印出 dice 的 sides 属性的值。
DiceGameTracker 的运行情况,如下所示:

let tracker = DiceGameTracker()
let game = SnakesAndLadders()
game.delegate = trackergame.play()
// 开始一个新的Snakes and Ladders的游戏
// 游戏使用 6 面的骰子
// 翻转得到 3
// 翻转得到 5
// 翻转得到 4
// 翻转得到 5
// 游戏进行了 4 轮

文章摘自开发者手册

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

推荐阅读更多精彩内容