Files
the-swift-programming-langu…/source/chapter2/22_Protocols.md
2015-07-09 11:07:44 +08:00

37 KiB
Raw Blame History

翻译:geek5nan 校对:dabing1022

协议


本页包含内容:

协议定义了一个蓝图,规定了用来实现某一特定工作或者功能所必需的方法和属性。类,结构体或枚举类型都可以遵循协议,并提供具体实现来完成协议定义的方法和功能。任意能够满足协议要求的类型被称为遵循(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关键字作为前缀。当协议的遵循者是类时,可以使用classstatic关键字来声明类属性,但是在协议的定义中,仍然要使用static关键字。

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

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

protocol FullyNamed {
	var fullName: String { get }
}

FullyNamed协议除了要求协议的遵循者提供fullName属性外对协议对遵循者的类型并没有特别的要求。这个协议表示任何遵循FullyNamed协议的类型,都具有一个可读的String类型实例属性fullName

下面是一个遵循FullyNamed协议的简单结构体。

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

这个例子中定义了一个叫做Person的结构体,用来表示具有名字的人。从第一行代码中可以看出,它遵循了FullyNamed协议。

Person结构体的每一个实例都有一个叫做fullNameString类型的存储型属性。这正好满足了FullyNamed协议的要求,也就意味着,Person结构体完整的遵循了协议。(如果协议要求未被完全满足,在编译时会报错)

下面是一个更为复杂的类,它采用并遵循了FullyNamed协议:

class Starship: FullyNamed {
    var prefix: String?
    var name: String
    init(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 is "USS Enterprise"

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

对方法的规定

协议可以要求其遵循者实现某些指定的实例方法或类方法。这些方法作为协议的一部分,像普通的方法一样放在协议的定义中,但是不需要大括号和方法体。可以在协议中定义具有可变参数的方法,和普通方法的定义方式相同。但是在协议的方法定义中,不支持参数默认值。

正如对属性的规定中所说的,在协议中定义类方法的时候,总是使用static关键字作为前缀。当协议的遵循者是类的时候,虽然你可以在类的实现中使用class或者static来实现类方法,但是在协议中声明类方法,仍然要使用static关键字。

protocol SomeProtocol {
	static func someTypeMethod()
}

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

protocol RandomNumberGenerator {
	func random() -> Double
}

RandomNumberGenerator协议要求其遵循者必须拥有一个名为random 返回值类型为Double的实例方法。尽管这里并未指明,但是我们假设返回值在[01)区间内。

RandomNumberGenerator协议并不在意每一个随机数是怎样生成的,它只强调这里有一个随机数生成器。

如下所示,下边的是一个遵循了RandomNumberGenerator协议的类。该类实现了一个叫做*线性同余生成器(linear congruential 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"

对Mutating方法的规定

有时需要在方法中改变它的实例。例如,值类型(结构体,枚举)的实例方法中,将mutating关键字作为函数的前缀,写在func之前,表示可以在该方法中修改它所属的实例及其实例属性的值。这一过程在Modifyting Value Types from Within Instance Methods章节中有详细描述。

如果你在协议中定义了一个方法旨在改变遵循该协议的实例,那么在协议定义时需要在方法前加mutating关键字。这使得结构和枚举遵循协议并满足此方法要求。

注意: 用类实现协议中的mutating方法时,不用写mutating关键字;用结构体,枚举实现协议中的mutating方法时,必须写mutating关键字。

如下所示,Togglable协议含有名为toggle的实例方法。根据名称推测,toggle()方法将通过改变实例属性,来切换遵循该协议的实例的状态。

toggle()方法在定义的时候,使用mutating关键字标记,这表明当它被调用时该方法将会改变协议遵循者实例的状态。

protocol Togglable {
	mutating func toggle()
}

当使用枚举结构体来实现Togglable协议时,需要提供一个带有mutating前缀的toggle方法。

下面定义了一个名为OnOffSwitch的枚举类型。这个枚举类型在两种状态之间进行切换,用枚举成员OnOff表示。枚举类型的toggle方法被标记为mutating以满足Togglable协议的要求。

enum OnOffSwitch: Togglable {
	case Off, On
	mutating func toggle() {
		switch self {
		case Off:
			self = On
		case On:
			self = Off
		}
	}
}
var lightSwitch = OnOffSwitch.Off
lightSwitch.toggle()
//lightSwitch 现在的值为 .On

对构造器的规定

协议可以要求它的遵循者实现指定的构造器。你可以像书写普通的构造器那样,在协议的定义里写下构造器的声明,但不需要写花括号和构造器的实体:

protocol SomeProtocol {
    init(someParameter: Int)
}

协议构造器规定在类中的实现

你可以在遵循该协议的类中实现构造器,并指定其为类的指定构造器(designated initializer)或者便利构造器(convenience initializer)。在这两种情况下,你都必须给构造器实现标上"required"修饰符:

class SomeClass: SomeProtocol {
    required init(someParameter: Int) {
        //构造器实现
    }
}

使用required修饰符可以保证:所有的遵循该协议的子类,同样能为构造器规定提供一个显式的实现或继承实现。

关于required构造器的更多内容,请参考Required构造器

注意 如果类已经被标记为final,那么不需要在协议构造器的实现中使用required修饰符。因为final类不能有子类。关于final修饰符的更多内容,请参见防止重写

如果一个子类重写了父类的指定构造器,并且该构造器遵循了某个协议的规定,那么该构造器的实现需要被同时标示requiredoverride修饰符

protocol SomeProtocol {
    init()
}


class SomeSuperClass {
    init() {
        // 构造器的实现
    }
}


class SomeSubClass: SomeSuperClass, SomeProtocol {
    // 因为遵循协议,需要加上"required"; 因为继承自父类,需要加上"override"
    required override init() {
        // 构造器实现
    }
}

可失败构造器的规定

可以通过给协议Protocols中添加可失败构造器来使遵循该协议的类型必须实现该可失败构造器。

如果在协议中定义一个可失败构造器,则在遵顼该协议的类型中必须添加同名同参数的可失败构造器或非可失败构造器。如果在协议中定义一个非可失败构造器,则在遵循该协议的类型中必须添加同名同参数的非可失败构造器或隐式解析类型的可失败构造器(init!)。

协议类型

尽管协议本身并不实现任何功能,但是协议可以被当做类型来使用。

协议可以像其他普通类型一样使用,使用场景:

  • 作为函数、方法或构造器中的参数类型或返回值类型
  • 作为常量、变量或属性的类型
  • 作为数组、字典或其他容器中的元素类型

注意 协议是一种类型,因此协议类型的名称应与其他类型(IntDoubleString)的写法相同,使用大写字母开头的驼峰式写法,例如(FullyNamedRandomNumberGenerator)

如下所示,这个示例中将协议当做类型来使用

class Dice {
	let sides: Int
	let generator: RandomNumberGenerator
	init(sides: Int, generator: RandomNumberGenerator) {
		self.sides = sides
		self.generator = generator
	}
	func roll() -> Int {
		return Int(generator.random() * Double(sides)) + 1
	}
}

例子中定义了一个Dice用来代表桌游中的拥有N个面的骰子。Dice的实例含有sidesgenerator两个属性,前者是整型,用来表示骰子有几个面,后者为骰子提供一个随机数生成器。

generator属性的类型为RandomNumberGenerator,因此任何遵循了RandomNumberGenerator协议的类型的实例都可以赋值给generator,除此之外,无其他要求。

Dice类中也有一个构造器(initializer),用来进行初始化操作。构造器中含有一个名为generator,类型为RandomNumberGenerator的形参。在调用构造方法时创建Dice的实例时,可以传入任何遵循RandomNumberGenerator协议的实例给generator。

Dice类也提供了一个名为roll的实例方法用来模拟骰子的面值。它先使用generatorrandom()方法来创建一个[0,1)区间内的随机数然后使用这个随机数生成正确的骰子面值。因为generator遵循了RandomNumberGenerator协议,因而保证了random方法可以被调用。

下面的例子展示了如何使用LinearCongruentialGenerator的实例作为随机数生成器创建一个六面骰子:

var d6 = Dice(sides: 6, generator: LinearCongruentialGenerator())
for _ in 1...5 {
	print("Random dice roll is \(d6.roll())")
}
//输出结果
//Random dice roll is 3
//Random dice roll is 5
//Random dice roll is 4
//Random dice roll is 5
//Random dice roll is 4

委托(代理)模式

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

下面的例子是两个基于骰子游戏的协议:

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的游戏过程

如下所示,SnakesAndLaddersSnakes and Ladders(译者注:Control Flow章节有该游戏的详细介绍)游戏的新版本。新版本使用Dice作为骰子,并且实现了DiceGameDiceGameDelegate协议,后者用来记录游戏的过程:

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属性,进而打印出dicesides属性的值。

DiceGameTracker的运行情况,如下所示:

let tracker = DiceGameTracker()
let game = SnakesAndLadders()
game.delegate = tracker
game.play()
// Started a new game of Snakes and Ladders
// The game is using a 6-sided dice
// Rolled a 3
// Rolled a 5
// Rolled a 4
// Rolled a 5
// The game lasted for 4 turns

在扩展中添加协议成员

即便无法修改源代码,依然可以通过扩展(Extension)来扩充已存在类型(译者注: 类,结构体,枚举等)。扩展可以为已存在的类型添加属性,方法,下标脚本,协议等成员。详情请在扩展章节中查看。

注意 通过扩展为已存在的类型遵循协议时,该类型的所有实例也会随之添加协议中的方法

例如TextRepresentable协议,任何想要表示一些文本内容的类型都可以遵循该协议。这些想要表示的内容可以是类型本身的描述,也可以是当前内容的版本:

protocol TextRepresentable {
	func asText() -> String
}

可以通过扩展,为上一节中提到的Dice增加类遵循TextRepresentable协议的功能

extension Dice: TextRepresentable {
	func asText() -> String {
		return "A \(sides)-sided dice"
	}
}

现在,通过扩展使得Dice类型遵循了一个新的协议,这和Dice类型在定义的时候声明为遵循TextRepresentable协议的效果相同。在扩展的时候,协议名称写在类型名之后,以冒号隔开,在大括号内写明新添加的协议内容。

现在所有Dice的实例都遵循了TextRepresentable协议:

let d12 = Dice(sides: 12,generator: LinearCongruentialGenerator())
print(d12.asText())
// 输出 "A 12-sided dice"

同样SnakesAndLadders类也可以通过扩展的方式来遵循TextRepresentable协议:

extension SnakesAndLadders: TextRepresentable {
	func asText() -> String {
		return "A game of Snakes and Ladders with \(finalSquare) squares"
	}
}
print(game.asText())
// 输出 "A game of Snakes and Ladders with 25 squares"

通过扩展补充协议声明

当一个类型已经实现了协议中的所有要求,却没有声明为遵循该协议时,可以通过扩展(空的扩展体)来补充协议声明:

struct Hamster {
	var name: String
	func asText() -> String {
		return "A hamster named \(name)"
	}
}
extension Hamster: TextRepresentable {}

从现在起,Hamster的实例可以作为TextRepresentable类型使用

let simonTheHamster = Hamster(name: "Simon")
let somethingTextRepresentable: TextRepresentable = simonTheHamster
print(somethingTextRepresentable.asText())
// 输出 "A hamster named Simon"

注意 即使满足了协议的所有要求,类型也不会自动转变,因此你必须为它做出显式的协议声明

集合中的协议类型

协议类型可以在集合使用,表示集合中的元素均为协议类型,下面的例子创建了一个类型为TextRepresentable的数组:

let things: [TextRepresentable] = [game,d12,simonTheHamster]

如下所示,things数组可以被直接遍历,并打印每个元素的文本表示:

for thing in things {
	print(thing.asText())
}
// A game of Snakes and Ladders with 25 squares
// A 12-sided dice
// A hamster named Simon

thing被当做是TextRepresentable类型而不是DiceDiceGameHamster等类型。因此能且仅能调用asText方法

协议的继承

协议能够继承一个或多个其他协议,可以在继承的协议基础上增加新的内容要求。协议的继承语法与类的继承相似,多个被继承的协议间用逗号分隔:

protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
	// 协议定义
}

如下所示,PrettyTextRepresentable协议继承了TextRepresentable协议

protocol PrettyTextRepresentable: TextRepresentable {
	func asPrettyText() -> String
}

例子中定义了一个新的协议PrettyTextRepresentable,它继承自TextRepresentable协议。任何遵循PrettyTextRepresentable协议的类型在满足该协议的要求时,也必须满足TextRepresentable协议的要求。在这个例子中,PrettyTextRepresentable协议要求其遵循者提供一个返回值为String类型的asPrettyText方法。

如下所示,扩展SnakesAndLadders,让其遵循PrettyTextRepresentable协议:

extension SnakesAndLadders: PrettyTextRepresentable {
    func asPrettyText() -> String {
        var output = asText() + ":\n"
        for index in 1...finalSquare {
            switch board[index] {
            	case let ladder where ladder > 0:
                output += "▲ "
            case let snake where snake < 0:
                output += "▼ "
            default:
                output += "○ "
            }
        }
        return output
    }
}

上述扩展使得SnakesAndLadders遵循了PrettyTextRepresentable协议,并为每个SnakesAndLadders类型提供了了协议要求的asPrettyText()方法。每个PrettyTextRepresentable类型同时也是TextRepresentable类型,所以在asPrettyText的实现中,可以调用asText()方法。之后在每一行加上换行符,作为输出的开始。然后遍历数组中的元素,输出一个几何图形来表示遍历的结果:

  • 当从数组中取出的元素的值大于0时表示
  • 当从数组中取出的元素的值小于0时表示
  • 当从数组中取出的元素的值等于0时表示

任意SankesAndLadders的实例都可以使用asPrettyText()方法。

print(game.asPrettyText())
// A game of Snakes and Ladders with 25 squares:
// ○ ○ ▲ ○ ○ ▲ ○ ○ ▲ ▲ ○ ○ ○ ▼ ○ ○ ○ ○ ▼ ○ ○ ▼ ○ ▼ ○

类专属协议

你可以在协议的继承列表中,通过添加class关键字,限制协议只能适配到类class类型。结构体或枚举不能遵循该协议。该class关键字必须是第一个出现在协议的继承列表中,其后,才是其他继承协议。

protocol SomeClassOnlyProtocol: class, SomeInheritedProtocol {
    // class-only protocol definition goes here
}

在以上例子中,协议SomeClassOnlyProtocol只能被类class类型适配。如果尝试让结构体或枚举类型适配该协议则会出现编译错误。

注意 当协议想要定义的行为,要求(或假设)它的遵循类型必须是引用语义而非值语义时,应该采用类专属协议。关于引用语义,值语义的更多内容,请查看结构体和枚举是值类型类是引用类型

协议合成

有时候需要同时遵循多个协议。你可以将多个协议采用protocol<SomeProtocol AnotherProtocol>这样的格式进行组合,称为协议合成(protocol composition)。你可以在<>中罗列任意多个你想要遵循的协议,以逗号分隔。

下面的例子中,将NamedAged两个协议按照上述的语法组合成一个协议:

protocol Named {
    var name: String { get }
}
protocol Aged {
    var age: Int { get }
}
struct Person: Named, Aged {
    var name: String
    var age: Int
}
func wishHappyBirthday(celebrator: protocol<Named, Aged>) {
    print("Happy birthday \(celebrator.name) - you're \(celebrator.age)!")
}
let birthdayPerson = Person(name: "Malcolm", age: 21)
wishHappyBirthday(birthdayPerson)
// 输出 "Happy birthday Malcolm - you're 21!

Named协议包含String类型的name属性;Aged协议包含Int类型的age属性。Person结构体遵循了这两个协议。

wishHappyBirthday函数的形参celebrator的类型为protocol<NamedAged>。可以传入任意遵循这两个协议的类型的实例。

上面的例子创建了一个名为birthdayPersonPerson实例,作为参数传递给了wishHappyBirthday(_:)函数。因为Person同时遵循这两个协议,所以这个参数合法,函数将输出生日问候语。

注意 协议合成并不会生成一个新协议类型,而是将多个协议合成为一个临时的协议,超出范围后立即失效。

检验协议的一致性

使用isas操作符来检查协议的一致性或转化协议类型。检查和转化的语法和之前相同(详情查看Typy Casting章节):

  • is操作符用来检查实例是否遵循了某个协议
  • as?返回一个可选值,当实例遵循协议时,返回该协议类型;否则返回nil
  • as用以强制向下转型。
@objc protocol HasArea {
    var area: Double { get }
}

注意: @objc用来表示协议是可选的,也可以用来表示暴露给Objective-C的代码,此外,@objc型协议只对有效,因此只能在中检查协议的一致性。详情查看*Using Siwft with Cocoa and Objectivei-c*。

如下所示,定义了CircleCountry类,它们都遵循了HasArea协议

class Circle: HasArea {
    let pi = 3.1415927
    var radius: Double
    var area: Double { return pi * radius * radius }
    init(radius: Double) { self.radius = radius }
}
class Country: HasArea {
    var area: Double
    init(area: Double) { self.area = area }
}

Circle类把area实现为基于存储型属性radius的计算型属性Country类则把area实现为存储型属性。这两个类都遵循HasArea协议。

如下所示Animal是一个没有实现HasArea协议的类

class Animal {
	var legs: Int
	init(legs: Int) { self.legs = legs }
}

CircleCountryAnimal并没有一个相同的基类,因而采用AnyObject类型的数组来装载在他们的实例,如下所示:

let objects: [AnyObject] = [
	Circle(radius: 2.0),
	Country(area: 243_610),
	Animal(legs: 4)
]

objects数组使用字面量初始化,数组包含一个radius为2。0的Circle的实例,一个保存了英国面积的Country实例和一个legs为4的Animal实例。

如下所示,objects数组可以被迭代,对迭代出的每一个元素进行检查,看它是否遵循了HasArea协议:

for object in objects {
    if let objectWithArea = object as? HasArea {
        print("Area is \(objectWithArea.area)")
    } else {
        print("Something that doesn't have an area")
    }
}
// Area is 12.5663708
// Area is 243610.0
// Something that doesn't have an area

当迭代出的元素遵循HasArea协议时,通过as?操作符将其可选绑定(optional binding)objectWithArea常量上。objectWithAreaHasArea协议类型的实例,因此area属性是可以被访问和打印的。

objects数组中元素的类型并不会因为向下转型而改变,它们仍然是CircleCountryAnimal类型。然而,当它们被赋值给objectWithArea常量时,则只被视为HasArea类型,因此只有area属性能够被访问。

对可选协议的规定

可选协议含有可选成员,其遵循者可以选择是否实现这些成员。在协议中使用@optional关键字作为前缀来定义可选成员。

可选协议在调用时使用可选链,详细内容在Optional Chaning章节中查看。

someOptionalMethod?(someArgument)这样,你可以在可选方法名称后加上?来检查该方法是否被实现。可选方法可选属性都会返回一个可选值(optional value),当其不可访问时,?之后语句不会执行,并整体返回nil

注意: 可选协议只能在含有@objc前缀的协议中生效。且@objc的协议只能被遵循

如下所示,Counter类使用含有两个可选成员的CounterDataSource协议类型的外部数据源来提供增量值(increment amount)

@objc protocol CounterDataSource {
    optional func incrementForCount(count: Int) -> Int
    optional var fixedIncrement: Int { get }
}

CounterDataSource含有incrementForCount可选方法fiexdIncrement可选属性,它们使用了不同的方法来从数据源中获取合适的增量值。

注意: CounterDataSource中的属性和方法都是可选的,因此可以在类中声明但不实现这些成员,尽管技术上允许这样做,不过最好不要这样写。

Counter类含有CounterDataSource?类型的可选属性dataSource,如下所示:

@objc class Counter {
    var count = 0
    var dataSource: CounterDataSource?
    func increment() {
        if let amount = dataSource?.incrementForCount?(count) {
            count += amount
        } else if let amount = dataSource?.fixedIncrement? {
            count += amount
        }
    }
}

count属性用于存储当前的值,increment方法用来为count赋值。

increment方法通过可选链,尝试从两种可选成员中获取count

  1. 由于dataSource可能为nil,因此在dataSource后边加上了?标记来表明只在dataSource非空时才去调用incrementForCount方法。

  2. 即使dataSource存在,但是也无法保证其是否实现了incrementForCount方法,因此在incrementForCount方法后边也加有?标记

在调用incrementForCount方法后,Int可选值通过可选绑定(optional binding)自动拆包并赋值给常量amount

incrementForCount不能被调用时,尝试使用可选属性fixedIncrement来代替。

ThreeSource实现了CounterDataSource协议,如下所示:

class ThreeSource: CounterDataSource {
	let fixedIncrement = 3
}

使用ThreeSource作为数据源开实例化一个Counter:

var counter = Counter()
counter.dataSource = ThreeSource()
for _ in 1...4 {
    counter.increment()
    print(counter.count)
}
// 3
// 6
// 9
// 12

TowardsZeroSource实现了CounterDataSource协议中的incrementForCount方法,如下所示:

class TowardsZeroSource: CounterDataSource {
func incrementForCount(count: Int) -> Int {
        if count == 0 {
            return 0
        } else if count < 0 {
            return 1
        } else {
            return -1
        }
    }
}

下边是执行的代码:

counter.count = -4
counter.dataSource = TowardsZeroSource()
for _ in 1...5 {
    counter.increment()
    print(counter.count)
}
// -3
// -2
// -1
// 0
// 0

协议扩展

Protocols can be extended to provide method and property implementations to conforming types. This allows you to define behavior on protocols themselves, rather than in each types individual conformance or in a global function.

For example, the RandomNumberGenerator protocol can be extended to provide a randomBool() method, which uses the result of the required random() method to return a random Bool value:

extension RandomNumberGenerator {
    func randomBool() -> Bool {
        return random() > 0.5
    }
}

By creating an extension on the protocol, all conforming types automatically gain this method implementation without any additional modification.

let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// prints "Here's a random number: 0.37464991998171"
print("And here's a random Boolean: \(generator.randomBool())")
// prints "And here's a random Boolean: true"

Providing Default Implementations

You can use protocol extensions to provide a default implementation to any method or property requirement of that protocol. If a conforming type provides its own implementation of a required method or property, that implementation will be used instead of the one provided by the extension.

NOTE Protocol requirements with default implementations provided by extensions are distinct from optional protocol requirements. Although conforming types dont have to provide their own implementation of either, requirements with default implementations can be called without optional chaining.

For example, the PrettyTextRepresentable protocol, which inherits the TextRepresentable protocol can provide a default implementation of its required asPrettyText() method to simply return the result of the asText() method:

extension PrettyTextRepresentable  {
    func asPrettyText() -> String {
        return asText()
    }
}

Adding Constraints to Protocol Extensions

When you define a protocol extension, you can specify constraints that conforming types must satisfy before the methods and properties of the extension are available. You write these constraints after the name of the protocol youre extending using a where clause, as described in (Where 子句).:

For instance, you can define an extension to the CollectionType protocol that applies to any collection whose elements conform to the TextRepresentable protocol from the example above.

extension CollectionType where Generator.Element : TextRepresentable {
    func asList() -> String {
        return "(" + ", ".join(map({$0.asText()})) + ")"
    }
}

The asList() method takes the textual representation of each element in the collection and concatenates them into a comma-separated list.

Consider the Hamster structure from before, which conforms to the TextRepresentable protocol, and an array of Hamster values:

let murrayTheHamster = Hamster(name: "Murray")
let morganTheHamster = Hamster(name: "Morgan")
let mauriceTheHamster = Hamster(name: "Maurice")
let hamsters = [murrayTheHamster, morganTheHamster, mauriceTheHamster]

Because Array conforms to CollectionType, and the arrays elements conform to the TextRepresentable protocol, the array can use the asList() method to get a textual representation of its contents:

print(hamsters.asList())
// prints "(A hamster named Murray, A hamster named Morgan, A hamster named Maurice)"

NOTE If a conforming type satisfies the requirements for multiple constrained extensions that provide implementations for the same method or property, Swift will use the implementation corresponding to the most specialized constraints.