[翻译]Swift编程语言——协议

协议

一个协议可以定义实现特定任务或者功能篇的的方法、属性或者其他的蓝图。协议不会给出这些要求的具体实现——它只是描述实现后要是说明样子。协议可以被一个类、结构体或者枚举采用,后者需要提供协议要求的实现。任何满足一个协议的类型都可以被称作遵循(译者:conform)了那个协议。

协议可以要求遵循它的类型拥有特定的实例属性、实例方法、类型方法、类型操作符和类型下标。

协议语法

定义一个协议和定义一个类、结构体和枚举很像:

protocol​ ​SomeProtocol​ {
​    ​// protocol definition goes here
​}

想要定制一个遵循特定协议的类型,可以在类型的名称之后放置协议的名称,用冒号分隔(作为类型定义的一部分)。多个协议可以一并列出来,用逗号分隔:

struct​ ​SomeStructure​: ​FirstProtocol​,​AnotherProtocol​ {
​    ​// structure definition goes here
​}

如果一个类有超类,那么将超类的名称写在协议名称的前面,依然是用逗号分隔:

​class​ ​SomeClass​: ​SomeSuperclass​,​FirstProtocol​,​AnotherProtocol​ {
​    ​// class definition goes here
​}

属性的要求

一个协议可以要求任何遵循它的类型提供特定名称和类型的实例属性或者类型属性。协议不会指定属性是存储的还是计算的——它只指定需要的属性名称和类型。协议同时还规定每个属性是只读的还是可读写的。

如果一个协议指定了一个属性是可读写的,那么属性就不能是一个常量存储属性或者是一个只读计算属性。如果协议制定了一个属性是只读的,那么属性可以是任何种类的,如果需要你也可以将其设置为可写的。

(协议的)属性要求通常用变量属性,用var关键做前缀。可读写属性用类型声明后面的{get set}表示,只读属性用{get}表示。

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

协议中,通常用class前缀表示类型属性。这个规则即便是对由结构体或者枚举实现的用static关键字做前缀的类型属性也适用:

protocol​ ​AnotherProtocol​ {
​    ​class​ ​var​ ​someTypeProperty​: ​Int​ { ​get​ ​set​ }
​}

这里有一个协议,它有唯一的一个实例属性要求:

​protocol​ ​FullyNamed​ {
​    ​var​ ​fullName​: ​String​ { ​get​ }
​}

FullyNamed协议要求遵循它的类型提供一个全名。它除了要求遵循它的类型要提供一个自己的全名外,没有其他别的要求了。协议规定任何的FullNamed类型必须提供一个只读的、叫做fullName的、String类型的实例属性。

这里是一个遵循FullyNamed协议的简单结构体:

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

这个例子定义了一个叫做Person的结构体,表现了一个有特定名称的人。在它定义的第一行,它遵循了FullyNamed协议。

每个Person实例有一个存储的属性叫做fullName,它是String类型的。这些符合FullyNamed协议的要求,意味着它遵循了那个协议。(如果协议没有被完全满足Swift会报告一个编译时错误)

这里有一个更复杂的类,同样也遵循了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"

这个类用一个只读的计算属性实现了fullName。每个Starship 类实例存储了一个强制的name属性和一个可选的prefiex属性。fullName属性在prefix值存在的情况下会使用它的值,使用时会将它的值放置在name之前。

方法的要求

协议可以要求遵循它的类型有指定的实例方法或和类型方法。这些方法作为协议定义的一部分,写法和普通的实例方法和类型方法一样,区别是不需要有大括号和方法体。可变参数是被允许的,和普通的方法一样。

NOTE
协议中的方法语法和普通方法的语法一样,除了不允许指定方法参数的默认值之外。

和类型属性的要求一样,使用class关键字做前缀在一个协议内定义类型方法。即便是协议最终由结构体或者枚举实现也要这样做:

​protocol​ ​SomeProtocol​ {
​    ​class​ ​func​ ​someTypeMethod​()
​}

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

​protocol​ ​RandomNumberGenerator​ {
​    ​func​ ​random​() -> ​Double
​}

这个协议,RandomNumberGenerator,要求任何遵循它的类型有一个叫做random的实例方法,该方法被调用后返回一个Double类型的数值。尽管没有在协议内指定,但是已经假设这个返回值的范围是从0.0(不包含)到1.0.

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​()
​println​(​"Here's a random number: ​\(​generator​.​random​())​"​)
​// prints "Here's a random number: 0.37464991998171"
​println​(​"And another one: ​\(​generator​.​random​())​"​)
​// prints "And another one: 0.729023776863283"

变异方法的要求

有时需要一个方法修改它所在的实例。对于在值类型(就是结构体和枚举)中的实例方法可以在方法的func关键字之前放置mutating关键字表明那个方法可以修改它所在的实例、同时/或者实例的任意属性。这在 用实例方法修改值类型(Modifying Value Types from Within Instance Methods)有描述。

如果一个协议定义了实例方法的要求,要求中想要修改任意遵循该协议的类型的实例,那么用mutating关键字标记方法吧。这样使得遵循这样的协议的结构体和枚举满足方法的要求。

NOTE
如果将一个协议的实例方法要求标记为了mutating,在遵循了这个协议的类中不需要再写mutating关键字了,mutating关键字只在遵循这个协议的结构体和枚举中使用。

下面的例子定义了一个叫做Togglable的协议,它定义了唯一的一个实例方法需求,叫做toggle。就像它的名字暗示的一样,toggle方法将要切换遵循那个协议的类型的状态,通常是通过修改该类型的属性(实现)。

作为Toggleable协议定义的一部分,toggle方法被用mutating关键字标记了,这表明这个方法在调用时会修改遵循这个协议的实例:

protocol​ ​Togglable​ {
​    ​mutating​ ​func​ ​toggle​()
​}

如果用结构体或者枚举实现这个Togglable协议,结构体或者枚举要提供一个同样用mutating标记的toggle方法。

下面的例子定义了一个叫做onOffSwitch的枚举。这个枚举在两个状态之间来回切换,通过枚举的两个case:On和Off表示。枚举的toggle实现被mutating标记了,为了满足Toggleable协议的要求:

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 is now equal to .On

构造方法的要求

协议可以对遵循它的类型的构造方法进行要求。这个构造方法可以作为协议的定义的一部分,书写方法和普通的构造方法一样,除了不要大括号和构造方法体:

protocol​ ​SomeProtocol​ {
​    ​init​(​someParameter​: ​Int​)
​}

类实现协议构造方法的要求

实现一个协议中定义的构造方法要求可以作为指定构造方法也可以作为方便构造方法。每种情况下,都要将构造方法的实现用required修饰符标记:

​class​ ​SomeClass​: ​SomeProtocol​ {
​    ​required​ ​init​(​someParameter​: ​Int​) {
​        ​// initializer implementation goes here
​    }
​}

使用required修饰符可以明确的对遵循协议的类的子类进行要求,这样那些子类也要遵循协议。

更多的关于 必要构造方法的内容 参见 必要构造方法(Required Initializers)。

NOTE

不需要对实现协议构造方法的、标记为final的类的构造方法实现标记required,因为最终类不会有子类了。更多的关于final修饰符的信息,参见 阻止重写(Preventing Overrides)。

如果子类重写了一个超类的指定构造方法,同时这个构造方法实现了一个协议的构造方法要求,需要同时采用requried和override修饰符对这个构造方法实现进行标记:

protocol​ ​SomeProtocol​ {
​    ​init​()
​}
​
​class​ ​SomeSuperClass​ {
​    ​init​() {
​        ​// initializer implementation goes here
​    }
​}
​
​class​ ​SomeSubClass​: ​SomeSuperClass​,​SomeProtocol​ {
​    ​// "required" from SomeProtocol conformance; "override" from SomeSuperClass
​    ​required​ ​override​ ​init​() {
​        ​// initializer implementation goes here
​    }
​}

可失败构造方法的要求

协议可以给实现类型定义一个可失败的构造方法要求,就像 可失败的构造方法(Failable Initializer)中的一样。

一个可以失败构造方法的要求可以由实现类型的可失败构造方法或者不可失败构造方法来实现。一个不可失败的构造方法的需求可以由一个可失败的构造方法或者静默拆包可失败构造方法实现。

协议作为类型

协议本身不会实现任何功能。但是,协议可以作为类型使用。

因为协议是类型,可以在一些其他类型适用的场合使用协议,包括:

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

NOTE
因为协议是类型,它们的名字首字母需要大写(比如FullNamed和RandomNumberGenerator),这样就和Swift的其他的类型一致了(比如,Int,String,Double)。

这里有一个协议被当作类型使用的例子:

​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实例有一个整型的属性叫做sides,这个属性表示色子有多少个面;还有一个叫做generator的属性,这个属性为摇色子提供了一个随机数字生成器。

generator属性是RandomNumberGenerator类型的。因此可以给它赋值任何类型,只要这个类型遵循了RandomNumberGenerator 协议。除此之外没有其他任何的要求了。

Dice用一个构造方法设置它的出事状态。这个构造方法有一个叫做generator的参数,这个参数是RandomNumberGenerator 类型的。当需要初始化一个新的Dice实例时,传递一个遵循RandomNumberGenerator 协议的值就可以了。

Dice提供了一个实例方法:roll,这个方法返回一个从1到色子面数之间的整数。这个调用了generator属性的random方法创建了一个从0.0到1.0的随机数,然后使用这个随机数创建出摇色子的数字。因为gnerator是遵循RandomNumberGenerator协议的,所以一定可以调用它的random 方法。

下面的例子是如何使用Dice类,通过采用一个LinearCongruentialGenerator 实例做随机数字产生器,创建出一个六面的色子:

​var​ ​d6​ = ​Dice​(​sides​: ​6​,​generator​: ​LinearCongruentialGenerator​())
​for​ ​_​ ​in​ ​1​...​5​ {
​    ​println​(​"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的类型遵循。

这里有一个在 控制流(Control Flow)中介绍的 蛇和梯子(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​)
​    }
​}

关于 蛇和梯子游戏的玩法,参见控制流一章的Break一节。

这个版本的游戏用一个叫做SnakeAndLadders的类包装,它遵循DiceGame协议。为了满足协议要求它提供一个只读的dice属性和一个paly方法。(dice属性被声明为常量,因为在初始化过后就不需要修改它了,而且协议只要求这个属性可读就行了)

游戏的初始化在这个类的构造方法init()中进行。所有的游戏逻辑挪到了协议的play方法中,方法中使用了协议中要求的dice属性,用来提供摇色子的值。

这里delegate属性被声明为了一个可选的DiceGameDelegate,因为一个委托代表不是必须的,所以它是一个可选类型,正因为如此,delegate属性被自动设置一个初始值nil。同时,游戏实例可以选择设置一个适合的委托代表给这个属性。

DiceGameDelegate 提供了三个方法来监控游戏的过程。三个方法在上面提到的play方法中,分别在游戏开始、游戏结束、游戏新一轮开始的时候被调用,已经和逻辑混合在一起了。

因为delegate 属性是一个可选DiceGameDelegate类型,paly方法每次调用委托代表上的方法时采用了可选类型链。如果delegate 属性是nil,这个委托代表调用会优雅的失败而不会产生错误。如果delegate 属性不是nil,委托代表的方法就被调用,SnakesAndLadders 被当成参数传递给该方法。

下面展示了一个叫做DiceGameTracker的类,这个类遵循DiceGameDelegate 协议:

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

DiceGameTracker 实现了DiceGameDelegate协议要求的全部三个方法。用这些方法,这个类监控了游戏进行了多少轮(译者:存储在​numberOfTurns变量中)。当游戏开始时,将​numberOfTurns的数字清零;每轮开始时对​numberOfTurns做自加操作;在游戏结束时,将​numberOfTurns的数字打印出来。

如上所示的gameDidStart 实现,使用game参数打印了关于将要进行的游戏的引导信息。game参数是​DiceGame​类型的而不是SnakesAndLadders,所以gameDidStart方法只能访问和使用作为DiceGame协议实现的那部分属性和方法。不管怎样,gameDidStart 方法都可以利用造型查询一个实例的具体类型。这个例子中,它就检查了game是不是一个SnakesAndLadders 类的实例,如果属实,还打印了对应的信息。

gameDidStart方法还访问了传递来的game参数的dice属性。因为game是遵循DiceGame协议的,所以它肯定会有一个dice属性,所以gameDidStart 方法能够方法和打印色子的sides属性,不必理会game具体的类型。

下面是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

用扩展实现协议

尽管不能访问已经存在类型的源代码,但可以扩展一个已经存在的类型,使其遵循一个新的协议。扩展可以给已经存在的类添加新的属性、方法和下标,因此可以添加协议的任何要求。关于扩展更多的信息,参见 扩展(Extensions)。

NOTE
当用扩展给类型添加了协议的要求后,这些类型的已经存在的实例就都遵循那个协议了。

举例说明,这个叫做TextRepresentable的协议,可以被任何可以用文本表示的类型实现。这个文本可能是自我描述,或者是当前状态的文本:

protocol​ ​TextRepresentable​ {
​    ​func​ ​asText​() -> ​String
​}

前面提到的Dice类可以通过扩展遵循TextRepresentable协议:

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

这个扩展使Dice遵循了一个新的协议就像在其原始实现中提供了一样。协议的名字在类型之后提供,用冒号分割,在扩展内容的大括号中提供协议所有要求的实现。

现在,任何一个Dice实例都可以被当作TextRepresentable对待了:

let​ ​d12​ = ​Dice​(​sides​: ​12​,​generator​: ​LinearCongruentialGenerator​())
​println​(​d12​.​asText​())
​// prints "A 12-sided dice"

类似的,SnakesAndLadders 也可以通过扩展遵循TextRepresentable 协议:

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

通过扩展宣布遵循一个协议(Declaring Protocol Adoption with an Extension)

如果一个类型已经满足一个协议的全部要求,但是没有规定遵循那个协议,可以通过一个空的扩展使类型遵循协议:

​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
​println​(​somethingTextRepresentable​.​asText​())
​// prints "A hamster named Simon"

NOTE
类型并不会通过满足协议的要求自动就遵循协议。需要明确的声明才可以。

协议类型的集合(Collections of Protocol Types)

就像 协议作为类型(Protocols as Types)提到的,协议可以作为集合(比如数组、字典)的类型使用。这个例子创建了一个TextRepresentable 类型的数组:

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

现在迭代数组中的每一项,打印每一项的文本表示:

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

tings的内容是TextRepresentable。而不是Dice、DiceGame或者Hamster类型的,尽管实际上它们是这些类型的。因为是TextRepresentable类型的,而TextRepresentable类型已知只有asText方法,所以在循环中调用thing.asText是安全的。

协议的继承(Protocol Inheritance)

一个协议可以继承一个或多个其他的协议,同时添加进一步的要求。协议继承的语法和类继承的语法类似,不同的是可以继承多个协议,用逗号分隔他们:

protocol​ ​InheritingProtocol​: ​SomeProtocol​,​AnotherProtocol​ {
​    ​// protocol definition goes here
​}

这里是一个继承了前文TextRepresentable 协议的一个协议:

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

这个例子定义了一个新的协议:PrettyTextRepresentable,它继承了TextRepresentable协议。任何遵循PrettyTextRepresentable 协议的类型必须满足TextRepresentable协议的所有要求,另外加上PrettyTextRepresentable协议的。在这个例子中,PrettyTextRepresentable 协议添加了要求,需要提供一个叫做asPrettyText的实例方法,该方法返回一个String。

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
​    }
​}

这个扩展规定要遵循PrettyTextRepresentable 协议,而且提供了SnakesAndLadders 类型的asPrettyText 方法实现。任何是PrettyTextRepresentable 类型的同时也是TextRepresentable类型的,所以asPrettyText 实现中以调用来自于TextRepresentable 协议的asText 方法开始,给output一个初始值。初始值后之后添加一个冒号和一个换行符,这些一起作为漂亮文字表述(pretty text representation)的开始。接下来遍历游戏格子的数组,用一个几何图形表示每个格子:

如果格子的值大于0,那么它是一个梯子,用▲表示。
如果格子的值小于0,那么它是条蛇,用▼表示。
不是以上情况,那么它是空格子,用○表示。

这个方法就可以被用来打印任何SnakesAndLadders 类型的实例的漂亮文字表述了:

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

只对类开放的协议(Class-Only Protocols)

在协议的继承列表中,可以用class关键字限制协议只对类开放。class关键字通常放置在协议继承列表的最开始,在任何继承的协议名字之前:

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

上面的例子中,SomeClassOnlyProtocol 只能被类实现。尝试让结构体或者枚举遵循SomeClassOnlyProtocol,会导致编译时错误。

NOTE
当协议定义的行为和引用相关而不是和值相关时,考虑采用只对类开放的协议(Class-Only Protocols)。关于引用和值的相关内容参见 结构体和枚举是值类型的 (Structures and Enumerations Are Value Type)和 类是引用类型的(Class Are Reference Type)

协议组合(Protocol Composition)

需要让一个类型同时遵循多个协议,这是很正常的。采用一个协议组合,可以将多个协议组合为一个(协议)需求。协议组合以这样的形式出现:protocol

是否满足协议的检查(Checking for Protocol Conformance)

可以使用造型 (Type Casting)中描述的is 和 as操作符检查是否满足协议和造型为特定协议。下面的对协议的检查和造型的语法和对类型的检查和造型的语法一样:

如果实例遵循一个协议,is操作法返回ture,否则返回false;
as?向下造型操作符,返回一个协议类型的可选类型,如果实例不遵循协议,可选类型的值是nil。
as!向下造型操作符,强制向下造型为一个协议,如果造型失败会触发一个运行时错误。

下面定义了一个叫做HasArea的协议,要求有一个只读的Double类型的属性area:

​protocol​ ​HasArea​ {
​    ​var​ ​area​: ​Double​ { ​get​ }
​}

这里有两个类,Circle和Country,它们两个都遵循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​ }
​}

Circle、Country和Animal三个类没有共同的超类。但它们都是类类型的,所以三个类型的实例可以存储在一个AnyObject类型的数组中:

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

objects数组采用了字面初始化,其中包含一个Circle实例(半径为2);一个Country实例(用英国的国土面积初始化);和一个有四条腿的Animal实例。

objects数组可以被迭代,其中的每个对象可以被检查是否遵循了HasArea协议:

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

一旦数组中的内容遵循了HasArea协议,as?操作符返回的可选值,会通过可选绑定拆包给常量objectWithArea赋值。objectWithArea 常量是HasArea类型,所以对area属性的访问和打印是类型安全的。

注意,潜在的对象没有通过造型处理被改变。它们任然是Circle、Country和Animal。但是,一旦它们被存储在objectWithArea常量中,它们就被当作HasArea类型,所以它们的area属性可以被访问。

可选协议要求(Optional Protocol Requirements)

可以给些一定义可选要求,这些要求不必一定被遵循的类型实现。协议定义中用前缀optional标记可选要求。

一个可选协议要求可以用可选类型链调用,提供了这些要求可以不被遵循协议的类型是实现的可能。更多关于可选类型链的信息,参见 可选类型链(Optional Chainning)。

可以在名字后面加一个问号的方式检查一个可选要求是否被实现,比如someOptionalMethod?(someArgument)。可选属性要求、和返回一个值的可选方法要求 将会始终返回一个对应的可选类型,当它们被访问或者被调用时,这将反映可选要求是不是被实现了。

NOTE
可选类型要求只能在用@objc标记的协议中定义。尽管你可能不需要和OC交互,但你需要这个标记定义可选要求。

同时要注意@objc标记的协议只能被类实现,结构体和枚举不行。

下面的类型定义了一个给整型计数的类,叫做Counter,这个类使用外部数据源给自身增加数量。这里的外部数据源采用CounterDataSource协议定义,其中有两个可选的要求:

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

CounterDataSource 协议定义了一个可选的方法要求,叫做incrementForCount ;还有一个可选的属性要求,叫做fixedIncrement。这些要求定义了两种不同的给Counter实例添加数量的方法。

NOTE
坦白说,可以定义一个类遵循CounterDataSource 协议,但不实现任何要求。这些要求都是可选的。尽管从技术角度这是允许的,但会产生一个没有实际的意义的数据源。

下面定义的Counter类,有一个可选的dataSource属性,它是CounterDataSource?类型的:

@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
​        }
​    }
​}

Counter类存储了它的当前的值在一个变量属性count中。同时它还定义了一个叫做increment的方法,每次这个方法被调用时,count属性会自增。

increment方法开始尝试查看它的数据源的incrementForCount 的实现来增加数值。increment方法是用了可选类型链,尝试调用incrementForCount,还将当前的count值传递给了incrementForCount方法做参数。

这里用了两级可选类型链。首先dataSource可能是nil,所以datasource名字后面有一个问号表示,只有dataSource不是nil的时候才调用incrementForCount方法。接下来,如果dataSource存在,但不确定它一定实现了incrementForCount方法,因为这个方法是一个可选要求。这就是为什么incrementForCount的名字后后面为什么也会有一个问号。

上面的俩个原因之一,都回导致调用incrementForCount方法失败,所以这个调用返回一个可选Int值。在CounterDataSource中定义了incrementForCount 方法时,这个语句才会有效。

在调用了incrementForCount方法之后,通过可选绑定,可选Int拆包后会赋值给一个常量amount。如果可选Int有值,就意味着方法和方法的委托都存在,同时方法还返回一个值,拆包后的amount 常量被添加到了存储属性count上面,增加数量的操作就完成了。

如果dataSource是nil,或如果数据源没有实现incrementForCount,都会造成不能从incrementForCount 方法得到值,如果是这样的情况,increment 方法就会尝试从数据源的fixedIncrement 属性中得到值。fixedIncrement 属性同样是一个可选要求,所以它的名字后面也用可选类型链也就是一个问号,这就表明尝试访问属性的值可能会失败。同前所述,这样返回的值也是一个可选Int的值,尽管在CounterDataSource 协议的定义中,fixedIncrement 被定义为一个不可选的Int属性。

这里是一个简单的CounterDataSource 实现,每次被请求的时候数据源都回返回一个常量值3.这些都是通过实现可选的fixedIncrement属性要求实现的:

class​ ​ThreeSource​: ​CounterDataSource​ {
​    ​let​ ​fixedIncrement​ = ​3
​}

可以用ThreeSource 的一个实例作为一个新的Counter实例的数据源:
var​ ​counter​ = ​Counter​()
​counter​.​dataSource​ = ​ThreeSource​()
​for​ ​_​ ​in​ ​1​...​4​ {
​    ​counter​.​increment​()
​    ​println​(​counter​.​count​)
​}
​// 3
​// 6
​// 9
​// 12

上面的代码创建了一个新的Counter实例;将一个新的ThreeSource 实例的赋值给了这个Counter实例的数据源;然后调用了counter的increment方法四次。和预期的一样,counter的count属性在increment方法被调用时会被加上3.

这里有一个更复杂的叫做TowardsZeroSource的数据源,它会使得Counter实例的count值趋于0:

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

TowardsZeroSource 类实现了CounterDataSource 协议中的可选incrementForCount 方法,根据count参数判断应该朝哪个方向计数。如果count已经是0了,那么方法就会返回0,不需要再进行计数下去了。

可以对一个已经存在的Counter实例使用一个TowardsZeroSource 实例,从-4计数到0,到0后就不再计数了:

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

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


软件简介:蓝湖辅助工具,减少移动端开发中控件属性的复制和粘贴.待开发的功能:1.支持自动生成约束2.开发设置页面3.做一个浏览器插件,支持不需要下载整个工程,可即时操作当前蓝湖浏览页面4.支持Flutter语言模板生成5.支持更多平台,如Sketch等6.支持用户自定义语言模板
现实生活中,我们听到的声音都是时间连续的,我们称为这种信号叫模拟信号。模拟信号需要进行数字化以后才能在计算机中使用。目前我们在计算机上进行音频播放都需要依赖于音频文件。那么音频文件如何生成的呢?音频文件的生成过程是将声音信息采样、量化和编码产生的数字信号的过程,我们人耳所能听到的声音频率范围为(20Hz~20KHz),因此音频文件格式的最大带宽是20KHZ。根据奈奎斯特的理论,音频文件的采样率一般在40~50KHZ之间。奈奎斯特采样定律,又称香农采样定律。...............
前言最近在B站上看到一个漂亮的仙女姐姐跳舞视频,循环看了亿遍又亿遍,久久不能离开!看着小仙紫姐姐的蹦迪视频,除了一键三连还能做什么?突发奇想,能不能把舞蹈视频转成代码舞呢?说干就干,今天就手把手教大家如何把跳舞视频转成代码舞,跟着仙女姐姐一起蹦起来~视频来源:【紫颜】见过仙女蹦迪吗 【千盏】一、核心功能设计总体来说,我们需要分为以下几步完成:从B站上把小姐姐的视频下载下来对视频进行截取GIF,把截取的GIF通过ASCII Animator进行ASCII字符转换把转换的字符gif根据每
【Android App】实战项目之仿抖音的短视频分享App(附源码和演示视频 超详细必看)
前言这一篇博客应该是我花时间最多的一次了,从2022年1月底至2022年4月底。我已经将这篇博客的内容写为论文,上传至arxiv:https://arxiv.org/pdf/2204.10160.pdf欢迎大家指出我论文中的问题,特别是语法与用词问题在github上,我也上传了完整的项目:https://github.com/Whiffe/Custom-ava-dataset_Custom-Spatio-Temporally-Action-Video-Dataset关于自定义ava数据集,也是后台
因为我既对接过session、cookie,也对接过JWT,今年因为工作需要也对接了gtoken的2个版本,对这方面的理解还算深入。尤其是看到官方文档评论区又小伙伴表示看不懂,所以做了这期视频内容出来:视频在这里:本期内容对应B站的开源视频因为涉及的知识点比较多,视频内容比较长。如果你觉得看视频浪费时间,可以直接阅读源码:goframe v2版本集成gtokengoframe v1版本集成gtokengoframe v2版本集成jwtgoframe v2版本session登录官方调用示例文档jwt和sess
【Android App】实战项目之仿微信的私信和群聊App(附源码和演示视频 超详细必看)
用Android Studio的VideoView组件实现简单的本地视频播放器。本文将讲解如何使用Android视频播放器VideoView组件来播放本地视频和网络视频,实现起来还是比较简单的。VideoView组件的作用与ImageView类似,只是ImageView用于显示图片,VideoView用于播放视频。...
采用MATLAB对正弦信号,语音信号进行生成、采样和内插恢复,利用MATLAB工具箱对混杂噪声的音频信号进行滤波
随着移动互联网、云端存储等技术的快速发展,包含丰富信息的音频数据呈现几何级速率增长。这些海量数据在为人工分析带来困难的同时,也为音频认知、创新学习研究提供了数据基础。在本节中,我们通过构建生成模型来生成音频序列文件,从而进一步加深对序列数据处理问题的了解。
基于yolov5+deepsort+slowfast算法的视频实时行为检测。1. yolov5实现目标检测,确定目标坐标 2. deepsort实现目标跟踪,持续标注目标坐标 3. slowfast实现动作识别,并给出置信率 4. 用框持续框住目标,并将动作类别以及置信度显示在框上
数字电子钟设计本文主要完成数字电子钟的以下功能1、计时功能(24小时)2、秒表功能(一个按键实现开始暂停,另一个按键实现清零功能)3、闹钟功能(设置闹钟以及到时响10秒)4、校时功能5、其他功能(清零、加速、星期、八位数码管显示等)前排提示:前面几篇文章介绍过的内容就不详细介绍了,可以看我专栏的前几篇文章。PS.工程文件放在最后面总体设计本次设计主要是在前一篇文章 数字电子钟基本功能的实现 的基础上改编而成的,主要结构不变,分频器将50MHz分为较低的频率备用;dig_select
1.进入官网下载OBS stdioOpen Broadcaster Software | OBS (obsproject.com)2.下载一个插件,拓展OBS的虚拟摄像头功能链接:OBS 虚拟摄像头插件.zip_免费高速下载|百度网盘-分享无限制 (baidu.com)提取码:6656--来自百度网盘超级会员V1的分享**注意**该插件必须下载但OBS的根目录(应该是自动匹配了的)3.打开OBS,选中虚拟摄像头选择启用在底部添加一段视频录制选择下面,进行录制.
Meta公司在9月29日首次推出一款人工智能系统模型:Make-A-Video,可以从给定的文字提示生成短视频。基于**文本到图像生成技术的最新进展**,该技术旨在实现文本到视频的生成,可以仅用几个单词或几行文本生成异想天开、独一无二的视频,将无限的想象力带入生活
音频信号叠加噪声及滤波一、前言二、信号分析及加噪三、滤波去噪四、总结一、前言之前一直对硬件上的内容比较关注,但是可能是因为硬件方面的东西可能真的是比较杂,而且需要渗透的东西太多了,所以学习进展比较缓慢。因为也很少有单纯的硬件学习研究,总是会伴随着各种理论需要硬件做支撑,所以还是想要慢慢接触理论学习。但是之前总找不到切入点,不知道从哪里开始,就一直拖着。最近稍微接触了一点信号处理,就用这个当作切入点,开始接触理论学习。二、信号分析及加噪信号处理选用了matlab做工具,选了一个最简单的语音信号处理方
腾讯云 TRTC 实时音视频服务体验,从认识 TRTC 到 TRTC 的开发实践,Demo 演示& IM 服务搭建。
音乐音频分类技术能够基于音乐内容为音乐添加类别标签,在音乐资源的高效组织、检索和推荐等相关方面的研究和应用具有重要意义。传统的音乐分类方法大量使用了人工设计的声学特征,特征的设计需要音乐领域的知识,不同分类任务的特征往往并不通用。深度学习的出现给更好地解决音乐分类问题提供了新的思路,本文对基于深度学习的音乐音频分类方法进行了研究。首先将音乐的音频信号转换成声谱作为统一表示,避免了手工选取特征存在的问题,然后基于一维卷积构建了一种音乐分类模型。
C++知识精讲16 | 井字棋游戏(配资源+视频)【赋源码,双人对战】
本文主要讲解如何在Java中,使用FFmpeg进行视频的帧读取,并最终合并成Gif动态图。
在本篇博文中,我们谈及了 Swift 中 some、any 关键字以及主关联类型(primary associated types)的前世今生,并由浅及深用简明的示例向大家讲解了它们之间的奥秘玄机。