126. 协议(Protocols)
- 协议语法(Protocol Syntax)
- 属性要求(Property Requirements)
- 方法要求(Method Requirements)
- Mutating 方法要求(Mutating Method Requirements)
- 构造器要求(Initializer Requirements)
- 协议作为类型(Protocols as Types)
- 委托(代理)模式(Delegation)
- 通过扩展添加协议一致性(Adding Protocol Conformance with an Extension)
- 通过扩展采纳协议(Declaring Protocol Adoption with an Extension)
- 协议类型的集合(Collections of Protocol Types)
- 协议的继承(Protocol Inheritance)
- 类类型专属协议(Class-Only Protocol)
- 协议合成(Protocol Composition)
- 检查协议一致性(Checking for Protocol Conformance)
- 可选的协议要求(Optional Protocol Requirements)
- 协议扩展(Protocol Extensions)
协议定义了一个蓝图,规定了用来实现某一特定任务或者功能的方法、属性,以及其他需要的东西。类、结构体或枚举都可以采纳协议,并为协议定义的这些要求提供具体实现。某个类型能够满足某个协议的要求,就可以说该类型“符合”这个协议。
除了采纳协议的类型必须实现的要求外,还可以对协议进行扩展,通过扩展来实现一部分要求或者实现一些附加功能,这样采纳协议的类型就能够使用这些功能。
协议语法
协议的定义方式与类、结构体和枚举的定义非常相似:
protocol SomeProtocol {
// 这里是协议的定义部分
}
要让自定义类型采纳某个协议,在定义类型时,需要在类型名称后加上协议名称,中间以冒号(:)分隔。采纳多个协议时,各协议之间用逗号(,)分隔:
struct SomeStructure: FirstProtocol, AnotherProtocol {
// 这里是结构体的定义部分
}
拥有父类的类在采纳协议时,应该将父类名放在协议名之前,以逗号分隔:
class SomeClass: SomeSuperClass, FirstProtocol, AnotherProtocol {
// 这里是类的定义部分
}
属性要求
协议可以要求采纳协议的类型提供特定名称和类型的实例属性或类型属性。协议不指定属性是存储型属性还是计算型属性,它只指定属性的名称和类型。此外,协议还指定属性是可读的还是可读可写的。
如果协议要求属性是可读可写的,那么该属性不能是常量属性或只读的计算型属性。如果协议只要求属性是可读的,那么该属性不仅可以是可读的,如果代码需要的话,还可以是可写的。
协议总是用 var 关键字来声明变量属性,在类型声明后加上 { set get } 来表示属性是可读可写的,可读属性则用 { get } 来表示:
protocol SomeProtocol {
var mustBeSettable: Int { get set }
var doesNotNeedToBeSettable: Int { get }
}
在协议中定义类型属性时,总是使用 static 关键字作为前缀。当类类型采纳协议时,除了 static 关键字,还可以使用 class 关键字来声明类型属性:
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 结构体的每一个实例都有一个 String 类型的存储型属性 fullName。这正好满足了 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 是 "USS Enterprise"
Starship 类把 fullName 属性实现为只读的计算型属性。每一个 Starship 类的实例都有一个名为 name 的非可选属性和一个名为 prefix 的可选属性。 当 prefix 存在时,计算型属性 fullName 会将 prefix 插入到 name 之前,从而为星际飞船构建一个全名。
方法要求
协议可以要求采纳协议的类型实现某些指定的实例方法或类方法。这些方法作为协议的一部分,像普通方法一样放在协议的定义中,但是不需要大括号和方法体。可以在协议中定义具有可变参数的方法,和普通方法的定义方式相同。但是,不支持为协议中的方法的参数提供默认值。
正如属性要求中所述,在协议中定义类方法的时候,总是使用 static 关键字作为前缀。当类类型采纳协议时,除了static 关键字,还可以使用 class 关键字作为前缀:
protocol SomeProtocol {
static 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()
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 关键字之前,表示可以在该方法中修改它所属的实例以及实例的任意属性的值。这一过程在在实例方法中修改值类型章节中有详细描述。
如果你在协议中定义了一个实例方法,该方法会改变采纳该协议的类型的实例,那么在定义协议时需要在方法前加mutating 关键字。这使得结构体和枚举能够采纳此协议并满足此方法要求。
注意
实现协议中的 mutating 方法时,若是类类型,则不用写 mutating 关键字。而对于结构体和枚举,则必须写mutating 关键字。
如下所示,Togglable 协议只要求实现一个名为 toggle 的实例方法。根据名称的暗示,toggle() 方法将改变实例属性,从而切换采纳该协议类型的实例的状态。
toggle() 方法在定义的时候,使用 mutating 关键字标记,这表明当它被调用时,该方法将会改变采纳协议的类型的实例:
protocol Togglable {
mutating func toggle()
}
当使用枚举或结构体来实现 Togglable 协议时,需要提供一个带有 mutating 前缀的 toggle() 方法。
下面定义了一个名为 OnOffSwitch 的枚举。这个枚举在两种状态之间进行切换,用枚举成员 On 和 Off 表示。枚举的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)
}
构造器要求在类中的实现
你可以在采纳协议的类中实现构造器,无论是作为指定构造器,还是作为便利构造器。无论哪种情况,你都必须为构造器实现标上 required 修饰符:
class SomeClass: SomeProtocol {
required init(someParameter: Int) {
// 这里是构造器的实现部分
}
}
使用 required 修饰符可以确保所有子类也必须提供此构造器实现,从而也能符合协议。
关于 required 构造器的更多内容,请参考必要构造器。
注意
如果类已经被标记为 final,那么不需要在协议构造器的实现中使用 required 修饰符,因为 final 类不能有子类。关于 final 修饰符的更多内容,请参见防止重写。
如果一个子类重写了父类的指定构造器,并且该构造器满足了某个协议的要求,那么该构造器的实现需要同时标注required 和 override 修饰符:
protocol SomeProtocol {
init()
}
class SomeSuperClass {
init() {
// 这里是构造器的实现部分
}
}
class SomeSubClass: SomeSuperClass, SomeProtocol {
// 因为采纳协议,需要加上 required
// 因为继承自父类,需要加上 override
required override init() {
// 这里是构造器的实现部分
}
}
可失败构造器要求
协议还可以为采纳协议的类型定义可失败构造器要求,详见可失败构造器。
采纳协议的类型可以通过可失败构造器(init?)或非可失败构造器(init)来满足协议中定义的可失败构造器要求。协议中定义的非可失败构造器要求可以通过非可失败构造器(init)或隐式解包可失败构造器(init!)来满足。
协议作为类型
尽管协议本身并未实现任何功能,但是协议可以被当做一个成熟的类型来使用。
协议可以像其他普通类型一样使用,使用场景如下:
- 作为函数、方法或构造器中的参数类型或返回值类型
- 作为常量、变量或属性的类型
- 作为数组、字典或其他容器中的元素类型
注意
协议是一种类型,因此协议类型的名称应与其他类型(例如 Int,Double,String)的写法相同,使用大写字母开头的驼峰式写法,例如(FullyNamed 和 RandomNumberGenerator)。
下面是将协议作为类型使用的例子:
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 协议的类型的实例都可以赋值给 generator,除此之外并无其他要求。
Dice 类还有一个构造器,用来设置初始状态。构造器有一个名为 generator,类型为 RandomNumberGenerator 的形参。在调用构造方法创建 Dice 的实例时,可以传入任何采纳 RandomNumberGenerator 协议的实例给 generator。
Dice 类提供了一个名为 roll 的实例方法,用来模拟骰子的面值。它先调用 generator 的 random() 方法来生成一个 [0.0,1.0) 区间内的随机数,然后使用这个随机数生成正确的骰子面值。因为 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的游戏过程。
如下所示,SnakesAndLadders 是 Control Flow 章节引入的蛇梯棋游戏的新版本。新版本使用 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)
}
}
关于这个蛇梯棋游戏的详细描述请参阅 Control Flow 章节中的 Break 部分。
这个版本的游戏封装到了 SnakesAndLadders 类中,该类采纳了 DiceGame 协议,并且提供了相应的可读的 dice 属性和 play() 方法。( dice 属性在构造之后就不再改变,且协议只要求 dice 为可读的,因此将 dice 声明为常量属性。)
游戏使用 SnakesAndLadders 类的 init() 构造器来初始化游戏。所有的游戏逻辑被转移到了协议中的 play() 方法,play() 方法使用协议要求的 dice 属性提供骰子摇出的值。
注意,delegate 并不是游戏的必备条件,因此 delegate 被定义为 DiceGameDelegate 类型的可选属性。因为delegate 是可选值,因此会被自动赋予初始值 nil。随后,可以在游戏中为 delegate 设置适当的值。
DicegameDelegate 协议提供了三个方法用来追踪游戏过程。这三个方法被放置于游戏的逻辑中,即 play() 方法内。分别在游戏开始时,新一轮开始时,以及游戏结束时被调用。
因为 delegate 是一个 DiceGameDelegate 类型的可选属性,因此在 play() 方法中通过可选链式调用来调用它的方法。若 delegate 属性为 nil,则调用方法会优雅地失败,并不会产生错误。若 delegate 不为 nil,则方法能够被调用,并传递 SnakesAndLadders 实例作为参数。
如下示例定义了 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 协议中的内容。当然了,SnakeAndLadders 的方法也可以在类型转换之后调用。在上例代码中,通过 is 操作符检查 game 是否为 SnakesAndLadders 类型的实例,如果是,则打印出相应的消息。
无论当前进行的是何种游戏,由于 game 符合 DiceGame 协议,可以确保 game 含有 dice 属性。因此在gameDidStart(_:) 方法中可以通过传入的 game 参数来访问 dice 属性,进而打印出 dice 的 sides 属性的值。
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
通过扩展添加协议一致性
即便无法修改源代码,依然可以通过扩展令已有类型采纳并符合协议。扩展可以为已有类型添加属性、方法、下标以及构造器,因此可以符合协议中的相应要求。详情请在扩展章节中查看。
注意
通过扩展令已有类型采纳并符合协议时,该类型的所有实例也会随之获得协议中定义的各项功能。
例如下面这个 TextRepresentable 协议,任何想要通过文本表示一些内容的类型都可以实现该协议。这些想要表示的内容可以是实例本身的描述,也可以是实例当前状态的文本描述:
protocol TextRepresentable {
var textualDescription: String { get }
}
可以通过扩展,令先前提到的 Dice 类采纳并符合 TextRepresentable 协议:
extension Dice: TextRepresentable {
var textualDescription: String {
return "A (sides)-sided dice"
}
}
通过扩展采纳并符合协议,和在原始定义中采纳并符合协议的效果完全相同。协议名称写在类型名之后,以冒号隔开,然后在扩展的大括号内实现协议要求的内容。
现在所有 Dice 的实例都可以看做 TextRepresentable 类型:
let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())
print(d12.textualDescription)
// 打印 “A 12-sided dice”
同样,SnakesAndLadders 类也可以通过扩展采纳并符合 TextRepresentable 协议:
extension SnakesAndLadders: TextRepresentable {
var textualDescription: String {
return "A game of Snakes and Ladders with (finalSquare) squares"
}
}
print(game.textualDescription)
// 打印 “A game of Snakes and Ladders with 25 squares”
通过扩展采纳协议
当一个类型已经符合了某个协议中的所有要求,却还没有声明采纳该协议时,可以通过空扩展体的扩展来采纳该协议:
struct Hamster {
var name: String
var textualDescription: String {
return "A hamster named (name)"
}
}
extension Hamster: TextRepresentable {}
从现在起,Hamster 的实例可以作为 TextRepresentable 类型使用:
let simonTheHamster = Hamster(name: "Simon")
let somethingTextRepresentable: TextRepresentable = simonTheHamster
print(somethingTextRepresentable.textualDescription)
// 打印 “A hamster named Simon”
注意
即使满足了协议的所有要求,类型也不会自动采纳协议,必须显式地采纳协议。
协议类型的集合
协议类型可以在数组或者字典这样的集合中使用,在协议类型提到了这样的用法。下面的例子创建了一个元素类型为TextRepresentable 的数组:
let things: [TextRepresentable] = [game, d12, simonTheHamster]
如下所示,可以遍历 things 数组,并打印每个元素的文本表示:
for thing in things {
print(thing.textualDescription)
}
// A game of Snakes and Ladders with 25 squares
// A 12-sided dice
// A hamster named Simon
thing 是 TextRepresentable 类型而不是 Dice,DiceGame,Hamster 等类型,即使实例在幕后确实是这些类型中的一种。由于 thing 是 TextRepresentable 类型,任何 TextRepresentable 的实例都有一个textualDescription 属性,所以在每次循环中可以安全地访问 thing.textualDescription。
协议的继承
协议能够继承一个或多个其他协议,可以在继承的协议的基础上增加新的要求。协议的继承语法与类的继承相似,多个被继承的协议间用逗号分隔:
protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
// 这里是协议的定义部分
}
如下所示,PrettyTextRepresentable 协议继承了 TextRepresentable 协议:
protocol PrettyTextRepresentable: TextRepresentable {
var prettyTextualDescription: String { get }
}
例子中定义了一个新的协议 PrettyTextRepresentable,它继承自 TextRepresentable 协议。任何采纳PrettyTextRepresentable 协议的类型在满足该协议的要求时,也必须满足 TextRepresentable 协议的要求。在这个例子中,PrettyTextRepresentable 协议额外要求采纳协议的类型提供一个返回值为 String 类型的prettyTextualDescription 属性。
如下所示,扩展 SnakesAndLadders,使其采纳并符合 PrettyTextRepresentable 协议:
extension SnakesAndLadders: PrettyTextRepresentable {
var prettyTextualDescription: String {
var output = textualDescription + ": "
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 协议,并提供了协议要求的prettyTextualDescription 属性。每个 PrettyTextRepresentable 类型同时也是 TextRepresentable 类型,所以在 prettyTextualDescription 的实现中,可以访问 textualDescription 属性。然后,拼接上了冒号和换行符。接着,遍历数组中的元素,拼接一个几何图形来表示每个棋盘方格的内容:
- 当从数组中取出的元素的值大于 0 时,用 ▲ 表示。
- 当从数组中取出的元素的值小于 0 时,用 ▼ 表示。
- 当从数组中取出的元素的值等于 0 时,用 ○ 表示。
任意 SankesAndLadders 的实例都可以使用 prettyTextualDescription 属性来打印一个漂亮的文本描述:
print(game.prettyTextualDescription)
// A game of Snakes and Ladders with 25 squares:
// ○ ○ ▲ ○ ○ ▲ ○ ○ ▲ ▲ ○ ○ ○ ▼ ○ ○ ○ ○ ▼ ○ ○ ▼ ○ ▼ ○
类类型专属协议
你可以在协议的继承列表中,通过添加 class 关键字来限制协议只能被类类型采纳,而结构体或枚举不能采纳该协议。class 关键字必须第一个出现在协议的继承列表中,在其他继承的协议之前:
protocol SomeClassOnlyProtocol: class, SomeInheritedProtocol {
// 这里是类类型专属协议的定义部分
}
在以上例子中,协议 SomeClassOnlyProtocol 只能被类类型采纳。如果尝试让结构体或枚举类型采纳该协议,则会导致编译错误。
注意
当协议定义的要求需要采纳协议的类型必须是引用语义而非值语义时,应该采用类类型专属协议。关于引用语义和值语义的更多内容,请查看结构体和枚举是值类型和类是引用类型。
协议合成
有时候需要同时采纳多个协议,你可以将多个协议采用 protocol<SomeProtocol, AnotherProtocol> 这样的格式进行组合,称为 协议合成(protocol composition)。你可以在 <> 中罗列任意多个你想要采纳的协议,以逗号分隔。
下面的例子中,将 Named 和 Aged 两个协议按照上述语法组合成一个协议,作为函数参数的类型:
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<Named,Aged>。这意味着它不关心参数的具体类型,只要参数符合这两个协议即可。
上面的例子创建了一个名为 birthdayPerson 的 Person 的实例,作为参数传递给了 wishHappyBirthday(_:) 函数。因为 Person 同时符合这两个协议,所以这个参数合法,函数将打印生日问候语。
注意
协议合成并不会生成新的、永久的协议类型,而是将多个协议中的要求合成到一个只在局部作用域有效的临时协议中。
检查协议一致性
你可以使用类型转换中描述的 is 和 as 操作符来检查协议一致性,即是否符合某协议,并且可以转换到指定的协议类型。检查和转换到某个协议类型在语法上和类型的检查和转换完全相同:
- is 用来检查实例是否符合某个协议,若符合则返回 true,否则返回 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 数组使用字面量初始化,数组包含一个 radius 为 2 的 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? 操作符返回的可选值通过可选绑定,绑定到 objectWithArea 常量上。objectWithArea 是 HasArea 协议类型的实例,因此 area 属性可以被访问和打印。
objects 数组中的元素的类型并不会因为强转而丢失类型信息,它们仍然是 Circle,Country,Animal 类型。然而,当它们被赋值给 objectWithArea 常量时,只被视为 HasArea 类型,因此只有 area 属性能够被访问。
可选的协议要求
协议可以定义可选要求,采纳协议的类型可以选择是否实现这些要求。在协议中使用 optional 关键字作为前缀来定义可选要求。使用可选要求时(例如,可选的方法或者属性),它们的类型会自动变成可选的。比如,一个类型为 (Int) -> String 的方法会变成 ((Int) -> String)?。需要注意的是整个函数类型是可选的,而不是函数的返回值。
协议中的可选要求可通过可选链式调用来使用,因为采纳协议的类型可能没有实现这些可选要求。类似someOptionalMethod?(someArgument) 这样,你可以在可选方法名称后加上 ? 来调用可选方法。详细内容可在可选链式调用章节中查看。
注意
可选的协议要求只能用在标记 @objc 特性的协议中。
该特性表示协议将暴露给 Objective-C 代码,详情参见Using Swift with Cocoa and Objective-C(Swift 2.1)。即使你不打算和 Objective-C 有什么交互,如果你想要指定可选的协议要求,那么还是要为协议加上@obj 特性。
还需要注意的是,标记 @objc 特性的协议只能被继承自 Objective-C 类的类或者 @objc 类采纳,其他类以及结构体和枚举均不能采纳这种协议。
下面的例子定义了一个名为 Counter 的用于整数计数的类,它使用外部的数据源来提供每次的增量。数据源由CounterDataSource 协议定义,包含两个可选要求:
@objc protocol CounterDataSource {
optional func incrementForCount(count: Int) -> Int
optional var fixedIncrement: Int { get }
}
CounterDataSource 协议定义了一个可选方法 incrementForCount(_:) 和一个可选属性 fiexdIncrement,它们使用了不同的方法来从数据源中获取适当的增量值。
注意
严格来讲,CounterDataSource 协议中的方法和属性都是可选的,因此采纳协议的类可以不实现这些要求,尽管技术上允许这样做,不过最好不要这样写。
Counter 类含有 CounterDataSource? 类型的可选属性 dataSource,如下所示:
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 值作为参数传入。
这里使用了两层可选链式调用。首先,由于 dataSource 可能为 nil,因此在 dataSource 后边加上了 ?,以此表明只在 dataSource 非空时才去调用 incrementForCount(_:) 方法。其次,即使 dataSource 存在,也无法保证其是否实现了 incrementForCount(_:) 方法,因为这个方法是可选的。因此,incrementForCount(_:) 方法同样使用可选链式调用进行调用,只有在该方法被实现的情况下才能调用它,所以在 incrementForCount(_:) 方法后边也加上了?。
调用 incrementForCount(_:) 方法在上述两种情形下都有可能失败,所以返回值为 Int? 类型。虽然在CounterDataSource 协议中,incrementForCount(_:) 的返回值类型是非可选 Int。另外,即使这里使用了两层可选链式调用,最后的返回结果依旧是单层的可选类型,即 Int? 而不是 Int??。关于这一点的更多信息,请查阅连接多层可选链式调用
在调用 incrementForCount(_:) 方法后,Int? 型的返回值通过可选绑定解包并赋值给常量 amount。如果可选值确实包含一个数值,也就是说,数据源和方法都存在,数据源方法返回了一个有效值。之后便将解包后的 amount 加到count 上,增量操作完成。
如果没有从 incrementForCount(_:) 方法获取到值,可能由于 dataSource 为 nil,或者它并没有实现incrementForCount(_:) 方法,那么 increment() 方法将试图从数据源的 fixedIncrement 属性中获取增量。fixedIncrement 是一个可选属性,因此属性值是一个 Int? 值,即使该属性在 CounterDataSource 协议中的类型是非可选的 Int。
下面的例子展示了 CounterDataSource 的简单实现。ThreeSource 类采纳了 CounterDataSource 协议,它实现了可选属性 fixedIncrement,每次会返回 3:
class ThreeSource: NSObject, 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
上述代码新建了一个 Counter 实例,并将它的数据源设置为一个 TreeSource 的实例,然后调用 increment() 方法四次。和预期一样,每次调用都会将 count 的值增加 3.
下面是一个更为复杂的数据源 TowardsZeroSource,它将使得最后的值变为 0:
@objc class TowardsZeroSource: NSObject, 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,以此表明之后不应再有增量操作发生。
你可以使用 TowardsZeroSource 实例将 Counter 实例来从 -4 增加到 0。一旦增加到 0,数值便不会再有变动:
counter.count = -4
counter.dataSource = TowardsZeroSource()
for _ in 1...5 {
counter.increment()
print(counter.count)
}
// -3
// -2
// -1
// 0
// 0
协议扩展
协议可以通过扩展来为采纳协议的类型提供属性、方法以及下标的实现。通过这种方式,你可以基于协议本身来实现这些功能,而无需在每个采纳协议的类型中都重复同样的实现,也无需使用全局函数。
例如,可以扩展 RandomNumberGenerator 协议来提供 randomBool() 方法。该方法使用协议中定义的 random() 方法来返回一个随机的 Bool 值:
extension RandomNumberGenerator {
func randomBool() -> Bool {
return random() > 0.5
}
}
通过协议扩展,所有采纳协议的类型,都能自动获得这个扩展所增加的方法实现,无需任何额外修改:
let generator = LinearCongruentialGenerator()
print("Here's a random number: (generator.random())")
// 打印 “Here's a random number: 0.37464991998171”
print("And here's a random Boolean: (generator.randomBool())")
// 打印 “And here's a random Boolean: true”
提供默认实现
可以通过协议扩展来为协议要求的属性、方法以及下标提供默认的实现。如果采纳协议的类型为这些要求提供了自己的实现,那么这些自定义实现将会替代扩展中的默认实现被使用。
注意
通过协议扩展为协议要求提供的默认实现和可选的协议要求不同。虽然在这两种情况下,采纳协议的类型都无需自己实现这些要求,但是通过扩展提供的默认实现可以直接调用,而无需使用可选链式调用。
例如,PrettyTextRepresentable 协议继承自 TextRepresentable 协议,可以为其提供一个默认的prettyTextualDescription 属性,只是简单地返回 textualDescription 属性的值:
extension PrettyTextRepresentable {
var prettyTextualDescription: String {
return textualDescription
}
}
为协议扩展添加限制条件
在扩展协议的时候,可以指定一些限制条件,只有采纳协议的类型满足这些限制条件时,才能获得协议扩展提供的默认实现。这些限制条件写在协议名之后,使用 where 子句来描述,正如Where子句中所描述的。
例如,你可以扩展 CollectionType 协议,但是只适用于集合中的元素采纳了 TextRepresentable 协议的情况:
extension CollectionType where Generator.Element: TextRepresentable {
var textualDescription: String {
let itemsAsText = self.map { $0.textualDescription }
return "[" + itemsAsText.joinWithSeparator(", ") + "]"
}
}
textualDescription 属性返回整个集合的文本描述,它将集合中的每个元素的文本描述以逗号分隔的方式连接起来,包在一对方括号中。
现在我们来看看先前的 Hamster 结构体,它符合 TextRepresentable 协议,同时这里还有个装有 Hamster 的实例的数组:
let murrayTheHamster = Hamster(name: "Murray")
let morganTheHamster = Hamster(name: "Morgan")
let mauriceTheHamster = Hamster(name: "Maurice")
let hamsters = [murrayTheHamster, morganTheHamster, mauriceTheHamster]
因为 Array 符合 CollectionType 协议,而数组中的元素又符合 TextRepresentable 协议,所以数组可以使用textualDescription 属性得到数组内容的文本表示:
print(hamsters.textualDescription)
// 打印 “[A hamster named Murray, A hamster named Morgan, A hamster named Maurice]”
注意
如果多个协议扩展都为同一个协议要求提供了默认实现,而采纳协议的类型又同时满足这些协议扩展的限制条件,那么将会使用限制条件最多的那个协议扩展提供的默认实现。
127.泛型(Generics)
泛型代码让你能够根据自定义的需求,编写出适用于任意类型、灵活可重用的函数及类型。它能让你避免代码的重复,用一种清晰和抽象的方式来表达代码的意图。
泛型是 Swift 最强大的特性之一,许多 Swift 标准库是通过泛型代码构建的。事实上,泛型的使用贯穿了整本语言手册,只是你可能没有发现而已。例如,Swift 的 Array 和 Dictionary 都是泛型集合。你可以创建一个 Int 数组,也可创建一个 String 数组,甚至可以是任意其他 Swift 类型的数组。同样的,你也可以创建存储任意指定类型的字典。
泛型所解决的问题
下面是一个标准的非泛型函数 swapTwoInts(_:_:),用来交换两个 Int 值:
func swapTwoInts(inout a: Int, inout _ b: Int) {
let temporaryA = a
a = b
b = temporaryA
}
这个函数使用输入输出参数(inout)来交换 a 和 b 的值,请参考输入输出参数。
swapTwoInts(_:_:) 函数交换 b 的原始值到 a,并交换 a 的原始值到 b。你可以调用这个函数交换两个 Int 变量的值:
var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print("someInt is now (someInt), and anotherInt is now (anotherInt)")
// 打印 “someInt is now 107, and anotherInt is now 3”
诚然,swapTwoInts(_:_:) 函数挺有用,但是它只能交换 Int 值,如果你想要交换两个 String 值或者 Double值,就不得不写更多的函数,例如 swapTwoStrings(_:_:) 和 swapTwoDoubles(_:_:),如下所示:
func swapTwoStrings(inout a: String, inout _ b: String) {
let temporaryA = a
a = b
b = temporaryA
}
func swapTwoDoubles(inout a: Double, inout _ b: Double) {
let temporaryA = a
a = b
b = temporaryA
}
你可能注意到 swapTwoInts(_:_:)、swapTwoStrings(_:_:) 和 swapTwoDoubles(_:_:) 的函数功能都是相同的,唯一不同之处就在于传入的变量类型不同,分别是 Int、String 和 Double。
在实际应用中,通常需要一个更实用更灵活的函数来交换两个任意类型的值,幸运的是,泛型代码帮你解决了这种问题。(这些函数的泛型版本已经在下面定义好了。)
注意
在上面三个函数中,a 和 b 类型相同。如果 a 和 b 类型不同,那它们俩就不能互换值。Swift 是类型安全的语言,所以它不允许一个 String 类型的变量和一个 Double 类型的变量互换值。试图这样做将导致编译错误。
泛型函数
泛型函数可以适用于任何类型,下面的 swapTwoValues(_:_:) 函数是上面三个函数的泛型版本:
func swapTwoValues<T>(inout a: T, inout _ b: T) {
let temporaryA = a
a = b
b = temporaryA
}
swapTwoValues(_:_:) 的函数主体和 swapTwoInts(_:_:) 函数是一样的,它们只在第一行有点不同,如下所示:
func swapTwoInts(inout a: Int, inout _ b: Int)
func swapTwoValues<T>(inout a: T, inout _ b: T)
这个函数的泛型版本使用了占位类型名(在这里用字母 T 来表示)来代替实际类型名(例如 Int、String 或Double)。占位类型名没有指明 T 必须是什么类型,但是它指明了 a 和 b 必须是同一类型 T,无论 T 代表什么类型。只有 swapTwoValues(_:_:) 函数在调用时,才能根据所传入的实际类型决定 T 所代表的类型。
另外一个不同之处在于这个泛型函数名(swapTwoValues(_:_:))后面跟着占位类型名(T),并用尖括号括起来(<T>)。这个尖括号告诉 Swift 那个 T 是 swapTwoValues(_:_:) 函数定义内的一个占位类型名,因此 Swift 不会去查找名为 T 的实际类型。
swapTwoValues(_:_:) 函数现在可以像 swapTwoInts(_:_:) 那样调用,不同的是它能接受两个任意类型的值,条件是这两个值有着相同的类型。swapTwoValues(_:_:) 函数被调用时,T 所代表的类型都会由传入的值的类型推断出来。
在下面的两个例子中,T 分别代表 Int 和 String:
var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
// someInt is now 107, and anotherInt is now 3
var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
// someString is now "world", and anotherString is now "hello"
注意
上面定义的 swapTwoValues(_:_:) 函数是受 swap(_:_:) 函数启发而实现的。后者存在于 Swift 标准库,你可以在你的应用程序中使用它。如果你在代码中需要类似 swapTwoValues(_:_:) 函数的功能,你可以使用已存在的 swap(_:_:) 函数。
类型参数
在上面的 swapTwoValues(_:_:) 例子中,占位类型 T 是类型参数的一个例子。类型参数指定并命名一个占位类型,并且紧随在函数名后面,使用一对尖括号括起来(例如 <T>)。
一旦一个类型参数被指定,你可以用它来定义一个函数的参数类型(例如 swapTwoValues(_:_:) 函数中的参数 a 和b),或者作为函数的返回类型,还可以用作函数主体中的注释类型。在这些情况下,类型参数会在函数调用时被实际类型所替换。(在上面的 swapTwoValues(_:_:) 例子中,当函数第一次被调用时,T 被 Int 替换,第二次调用时,被String 替换。)
你可提供多个类型参数,将它们都写在尖括号中,用逗号分开。
命名类型参数
在大多数情况下,类型参数具有一个描述性名字,例如 Dictionary<Key, Value> 中的 Key 和 Value,以及Array<Element> 中的 Element,这可以告诉阅读代码的人这些类型参数和泛型函数之间的关系。然而,当它们之间没有有意义的关系时,通常使用单个字母来命名,例如 T、U、V,正如上面演示的 swapTwoValues(_:_:) 函数中的T 一样。
注意
请始终使用大写字母开头的驼峰命名法(例如 T 和 MyTypeParameter)来为类型参数命名,以表明它们是占位类型,而不是一个值。
泛型类型
除了泛型函数,Swift 还允许你定义泛型类型。这些自定义类、结构体和枚举可以适用于任何类型,类似于 Array 和Dictionary。
这部分内容将向你展示如何编写一个名为 Stack (栈)的泛型集合类型。栈是一系列值的有序集合,和 Array 类似,但它相比 Swift 的 Array 类型有更多的操作限制。数组允许在数组的任意位置插入新元素或是删除其中任意位置的元素。而栈只允许在集合的末端添加新的元素(称之为入栈)。类似的,栈也只能从末端移除元素(称之为出栈)。
注意
栈的概念已被 UINavigationController 类用来构造视图控制器的导航结构。你通过调用UINavigationController 的 pushViewController(_:animated:) 方法来添加新的视图控制器到导航栈,通过 popViewControllerAnimated(_:) 方法来从导航栈中移除视图控制器。每当你需要一个严格的“后进先出”方式来管理集合,栈都是最实用的模型。
下图展示了一个栈的入栈(push)和出栈(pop)的行为:
- 现在有三个值在栈中。
- 第四个值被压入到栈的顶部。
- 现在有四个值在栈中,最近入栈的那个值在顶部。
- 栈中最顶部的那个值被移除,或称之为出栈。
- 移除掉一个值后,现在栈又只有三个值了。
下面展示了如何编写一个非泛型版本的栈,以 Int 型的栈为例:
struct IntStack {
var items = [Int]()
mutating func push(item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
}
这个结构体在栈中使用一个名为 items 的 Array 属性来存储值。Stack 提供了两个方法:push(_:) 和 pop(),用来向栈中压入值以及从栈中移除值。这些方法被标记为 mutating,因为它们需要修改结构体的 items 数组。
上面的 IntStack 结构体只能用于 Int 类型。不过,可以定义一个泛型 Stack 结构体,从而能够处理任意类型的值。
下面是相同代码的泛型版本:
struct Stack<Element> {
var items = [Element]()
mutating func push(item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
}
注意,Stack 基本上和 IntStack 相同,只是用占位类型参数 Element 代替了实际的 Int 类型。这个类型参数包裹在紧随结构体名的一对尖括号里(<Element>)。
Element 为待提供的类型定义了一个占位名。这种待提供的类型可以在结构体的定义中通过 Element 来引用。在这个例子中,Element 在如下三个地方被用作占位符:
- 创建 items 属性,使用 Element 类型的空数组对其进行初始化。
- 指定 push(_:) 方法的唯一参数 item 的类型必须是 Element 类型。
- 指定 pop() 方法的返回值类型必须是 Element 类型。
由于 Stack 是泛型类型,因此可以用来创建 Swift 中任意有效类型的栈,就像 Array 和 Dictionary 那样。
你可以通过在尖括号中写出栈中需要存储的数据类型来创建并初始化一个 Stack 实例。例如,要创建一个 String 类型的栈,可以写成 Stack<String>():
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
// 栈中现在有 4 个字符串
下图展示了 stackOfStrings 如何将这四个值入栈:
移除并返回栈顶部的值 "cuatro",即将其出栈:
let fromTheTop = stackOfStrings.pop()
// fromTheTop 的值为 "cuatro",现在栈中还有 3 个字符串
下图展示了 stackOfStrings 如何将顶部的值出栈:
扩展一个泛型类型
当你扩展一个泛型类型的时候,你并不需要在扩展的定义中提供类型参数列表。原始类型定义中声明的类型参数列表在扩展中可以直接使用,并且这些来自原始类型中的参数名称会被用作原始定义中类型参数的引用。
下面的例子扩展了泛型类型 Stack,为其添加了一个名为 topItem 的只读计算型属性,它将会返回当前栈顶端的元素而不会将其从栈中移除:
extension Stack {
var topItem: Element? {
return items.isEmpty ? nil : items[items.count - 1]
}
}
topItem 属性会返回一个 Element 类型的可选值。当栈为空的时候,topItem 会返回 nil;当栈不为空的时候,topItem 会返回 items 数组中的最后一个元素。
注意,这个扩展并没有定义一个类型参数列表。相反的,Stack 类型已有的类型参数名称 Element,被用在扩展中来表示计算型属性 topItem 的可选类型。
计算型属性 topItem 现在可以用来访问任意 Stack 实例的顶端元素且不移除它:
if let topItem = stackOfStrings.topItem {
print("The top item on the stack is (topItem).")
}
// 打印 “The top item on the stack is tres.”
类型约束
swapTwoValues(_:_:) 函数和 Stack 类型可以作用于任何类型。不过,有的时候如果能将使用在泛型函数和泛型类型中的类型添加一个特定的类型约束,将会是非常有用的。类型约束可以指定一个类型参数必须继承自指定类,或者符合一个特定的协议或协议组合。
例如,Swift 的 Dictionary 类型对字典的键的类型做了些限制。在字典的描述中,字典的键的类型必须是可哈希(hashable)的。也就是说,必须有一种方法能够唯一地表示它。Dictionary 的键之所以要是可哈希的,是为了便于检查字典是否已经包含某个特定键的值。若没有这个要求,Dictionary 将无法判断是否可以插入或者替换某个指定键的值,也不能查找到已经存储在字典中的指定键的值。
为了实现这个要求,一个类型约束被强制加到 Dictionary 的键类型上,要求其键类型必须符合 Hashable 协议,这是 Swift 标准库中定义的一个特定协议。所有的 Swift 基本类型(例如 String、Int、Double 和 Bool)默认都是可哈希的。
当你创建自定义泛型类型时,你可以定义你自己的类型约束,这些约束将提供更为强大的泛型编程能力。抽象概念,例如可哈希的,描述的是类型在概念上的特征,而不是它们的显式类型。
类型约束语法
你可以在一个类型参数名后面放置一个类名或者协议名,并用冒号进行分隔,来定义类型约束,它们将成为类型参数列表的一部分。对泛型函数添加类型约束的基本语法如下所示(作用于泛型类型时的语法与之相同):
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
// 这里是泛型函数的函数体部分
}
上面这个函数有两个类型参数。第一个类型参数 T,有一个要求 T 必须是 SomeClass 子类的类型约束;第二个类型参数 U,有一个要求 U 必须符合 SomeProtocol 协议的类型约束。
类型约束实践
这里有个名为 findStringIndex 的非泛型函数,该函数的功能是在一个 String 数组中查找给定 String 值的索引。若查找到匹配的字符串,findStringIndex(_:_:) 函数返回该字符串在数组中的索引值,否则返回 nil:
func findStringIndex(array: [String], _ valueToFind: String) -> Int? {
for (index, value) in array.enumerate() {
if value == valueToFind {
return index
}
}
return nil
}
findStringIndex(_:_:) 函数可以用于查找字符串数组中的某个字符串:
let strings = ["cat", "dog", "llama", "parakeet", "terrapin"]
if let foundIndex = findStringIndex(strings, "llama") {
print("The index of llama is (foundIndex)")
}
// 打印 “The index of llama is 2”
如果只能查找字符串在数组中的索引,用处不是很大。不过,你可以用占位类型 T 替换 String 类型来写出具有相同功能的泛型函数 findIndex(_:_:)。
下面展示了 findStringIndex(_:_:) 函数的泛型版本 findIndex(_:_:)。请注意这个函数返回值的类型仍然是Int?,这是因为函数返回的是一个可选的索引数,而不是从数组中得到的一个可选值。需要提醒的是,这个函数无法通过编译,原因会在例子后面说明:
func findIndex<T>(array: [T], _ valueToFind: T) -> Int? {
for (index, value) in array.enumerate() {
if value == valueToFind {
return index
}
}
return nil
}
上面所写的函数无法通过编译。问题出在相等性检查上,即 "if value == valueToFind"。不是所有的 Swift 类型都可以用等式符(==)进行比较。比如说,如果你创建一个自定义的类或结构体来表示一个复杂的数据模型,那么 Swift 无法猜到对于这个类或结构体而言“相等”意味着什么。正因如此,这部分代码无法保证适用于每个可能的类型 T,当你试图编译这部分代码时会出现相应的错误。
不过,所有的这些并不会让我们无从下手。Swift 标准库中定义了一个 Equatable 协议,该协议要求任何遵循该协议的类型必须实现等式符(==)及不等符(!=),从而能对该类型的任意两个值进行比较。所有的 Swift 标准类型自动支持Equatable 协议。
任何 Equatable 类型都可以安全地使用在 findIndex(_:_:) 函数中,因为其保证支持等式操作符。为了说明这个事实,当你定义一个函数时,你可以定义一个 Equatable 类型约束作为类型参数定义的一部分:
func findIndex<T: Equatable>(array: [T], _ valueToFind: T) -> Int? {
for (index, value) in array.enumerate() {
if value == valueToFind {
return index
}
}
return nil
}
findIndex(_:_:) 唯一的类型参数写做 T: Equatable,也就意味着“任何符合 Equatable 协议的类型 T ”。
findIndex(_:_:) 函数现在可以成功编译了,并且可以作用于任何符合 Equatable 的类型,如 Double 或String:
let doubleIndex = findIndex([3.14159, 0.1, 0.25], 9.3)
// doubleIndex 类型为 Int?,其值为 nil,因为 9.3 不在数组中
let stringIndex = findIndex(["Mike", "Malcolm", "Andrea"], "Andrea")
// stringIndex 类型为 Int?,其值为 2
关联类型
定义一个协议时,有的时候声明一个或多个关联类型作为协议定义的一部分将会非常有用。关联类型为协议中的某个类型提供了一个占位名(或者说别名),其代表的实际类型在协议被采纳时才会被指定。你可以通过 associatedtype 关键字来指定关联类型。
关联类型实践
下面例子定义了一个 Container 协议,该协议定义了一个关联类型 ItemType:
protocol Container {
associatedtype ItemType
mutating func append(item: ItemType)
var count: Int { get }
subscript(i: Int) -> ItemType { get }
}
Container 协议定义了三个任何采纳了该协议的类型(即容器)必须提供的功能:
- 必须可以通过 append(_:) 方法添加一个新元素到容器里。
- 必须可以通过 count 属性获取容器中元素的数量,并返回一个 Int 值。
- 必须可以通过索引值类型为 Int 的下标检索到容器中的每一个元素。
这个协议没有指定容器中元素该如何存储,以及元素必须是何种类型。这个协议只指定了三个任何遵从 Container 协议的类型必须提供的功能。遵从协议的类型在满足这三个条件的情况下也可以提供其他额外的功能。
任何遵从 Container 协议的类型必须能够指定其存储的元素的类型,必须保证只有正确类型的元素可以加进容器中,必须明确通过其下标返回的元素的类型。
为了定义这三个条件,Container 协议需要在不知道容器中元素的具体类型的情况下引用这种类型。Container 协议需要指定任何通过 append(_:) 方法添加到容器中的元素和容器中的元素是相同类型,并且通过容器下标返回的元素的类型也是这种类型。
为了达到这个目的,Container 协议声明了一个关联类型 ItemType,写作 associatedtype ItemType。这个协议无法定义 ItemType 是什么类型的别名,这个信息将留给遵从协议的类型来提供。尽管如此,ItemType 别名提供了一种方式来引用 Container 中元素的类型,并将之用于 append(_:) 方法和下标,从而保证任何 Container 的行为都能够正如预期地被执行。
下面是先前的非泛型的 IntStack 类型,这一版本采纳并符合了 Container 协议:
struct IntStack: Container {
// IntStack 的原始实现部分
var items = [Int]()
mutating func push(item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
// Container 协议的实现部分
typealias ItemType = Int
mutating func append(item: Int) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Int {
return items[i]
}
}
IntStack 结构体实现了 Container 协议的三个要求,其原有功能也不会和这些要求相冲突。
此外,IntStack 在实现 Container 的要求时,指定 ItemType 为 Int 类型,即 typealias ItemType = Int,从而将 Container 协议中抽象的 ItemType 类型转换为具体的 Int 类型。
由于 Swift 的类型推断,你实际上不用在 IntStack 的定义中声明 ItemType 为 Int。因为 IntStack 符合Container 协议的所有要求,Swift 只需通过 append(_:) 方法的 item 参数类型和下标返回值的类型,就可以推断出 ItemType 的具体类型。事实上,如果你在上面的代码中删除了 typealias ItemType = Int 这一行,一切仍旧可以正常工作,因为 Swift 清楚地知道 ItemType 应该是哪种类型。
你也可以让泛型 Stack 结构体遵从 Container 协议:
struct Stack<Element>: Container {
// Stack<Element> 的原始实现部分
var items = [Element]()
mutating func push(item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
// Container 协议的实现部分
mutating func append(item: Element) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Element {
return items[i]
}
}
这一次,占位类型参数 Element 被用作 append(_:) 方法的 item 参数和下标的返回类型。Swift 可以据此推断出Element 的类型即是 ItemType 的类型。
通过扩展一个存在的类型来指定关联类型
通过扩展添加协议一致性中描述了如何利用扩展让一个已存在的类型符合一个协议,这包括使用了关联类型的协议。
Swift 的 Array 类型已经提供 append(_:) 方法,一个 count 属性,以及一个接受 Int 类型索引值的下标用以检索其元素。这三个功能都符合 Container 协议的要求,也就意味着你只需简单地声明 Array 采纳该协议就可以扩展Array,使其遵从 Container 协议。你可以通过一个空扩展来实现这点,正如通过扩展采纳协议中的描述:
extension Array: Container {}
如同上面的泛型 Stack 结构体一样,Array 的 append(_:) 方法和下标确保了 Swift 可以推断出 ItemType 的类型。定义了这个扩展后,你可以将任意 Array 当作 Container 来使用。
Where 子句
类型约束让你能够为泛型函数或泛型类型的类型参数定义一些强制要求。
为关联类型定义约束也是非常有用的。你可以在参数列表中通过 where 子句为关联类型定义约束。你能通过 where 子句要求一个关联类型遵从某个特定的协议,以及某个特定的类型参数和关联类型必须类型相同。你可以通过将 where 关键字紧跟在类型参数列表后面来定义 where 子句,where 子句后跟一个或者多个针对关联类型的约束,以及一个或多个类型参数和关联类型间的相等关系。
下面的例子定义了一个名为 allItemsMatch 的泛型函数,用来检查两个 Container 实例是否包含相同顺序的相同元素。如果所有的元素能够匹配,那么返回 true,否则返回 false。
被检查的两个 Container 可以不是相同类型的容器(虽然它们可以相同),但它们必须拥有相同类型的元素。这个要求通过一个类型约束以及一个 where 子句来表示:
func allItemsMatch<
C1: Container, C2: Container
where C1.ItemType == C2.ItemType, C1.ItemType: Equatable>
(someContainer: C1, _ anotherContainer: C2) -> Bool {
// 检查两个容器含有相同数量的元素
if someContainer.count != anotherContainer.count {
return false
}
// 检查每一对元素是否相等
for i in 0..<someContainer.count {
if someContainer[i] != anotherContainer[i] {
return false
}
}
// 所有元素都匹配,返回 true
return true
}
这个函数接受 someContainer 和 anotherContainer 两个参数。参数 someContainer 的类型为 C1,参数anotherContainer 的类型为 C2。C1 和 C2 是容器的两个占位类型参数,函数被调用时才能确定它们的具体类型。
这个函数的类型参数列表还定义了对两个类型参数的要求:
- C1 必须符合 Container 协议(写作 C1: Container)。
- C2 必须符合 Container 协议(写作 C2: Container)。
- C1 的 ItemType 必须和 C2 的 ItemType类型相同(写作 C1.ItemType == C2.ItemType)。
- C1 的 ItemType 必须符合 Equatable 协议(写作 C1.ItemType: Equatable)。
第三个和第四个要求被定义为一个 where 子句,写在关键字 where 后面,它们也是泛型函数类型参数列表的一部分。
这些要求意味着:
- someContainer 是一个 C1 类型的容器。
- anotherContainer 是一个 C2 类型的容器。
- someContainer 和 anotherContainer 包含相同类型的元素。
- someContainer 中的元素可以通过不等于操作符(!=)来检查它们是否彼此不同。
第三个和第四个要求结合起来意味着 anotherContainer 中的元素也可以通过 != 操作符来比较,因为它们和someContainer 中的元素类型相同。
这些要求让 allItemsMatch(_:_:) 函数能够比较两个容器,即使它们的容器类型不同。
allItemsMatch(_:_:) 函数首先检查两个容器是否拥有相同数量的元素,如果它们的元素数量不同,那么一定不匹配,函数就会返回 false。
进行这项检查之后,通过 for-in 循环和半闭区间操作符(..<)来迭代每个元素,检查 someContainer 中的元素是否不等于 anotherContainer 中的对应元素。如果两个元素不相等,那么两个容器不匹配,函数返回 false。
如果循环体结束后未发现任何不匹配的情况,表明两个容器匹配,函数返回 true。
下面演示了 allItemsMatch(_:_:) 函数的使用:
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
var arrayOfStrings = ["uno", "dos", "tres"]
if allItemsMatch(stackOfStrings, arrayOfStrings) {
print("All items match.")
} else {
print("Not all items match.")
}
// 打印 “All items match.”
上面的例子创建了一个 Stack 实例来存储一些 String 值,然后将三个字符串压入栈中。这个例子还通过数组字面量创建了一个 Array 实例,数组中包含同栈中一样的三个字符串。即使栈和数组是不同的类型,但它们都遵从 Container协议,而且它们都包含相同类型的值。因此你可以用这两个容器作为参数来调用 allItemsMatch(_:_:) 函数。在上面的例子中,allItemsMatch(_:_:) 函数正确地显示了这两个容器中的所有元素都是相互匹配的。
128访问控制(Access Control)
访问控制可以限定其他源文件或模块中的代码对你的代码的访问级别。这个特性可以让我们隐藏代码的一些实现细节,并且可以为其他人可以访问和使用的代码提供接口。
你可以明确地给单个类型(类、结构体、枚举)设置访问级别,也可以给这些类型的属性、方法、构造器、下标等设置访问级别。协议也可以被限定在一定的范围内使用,包括协议里的全局常量、变量和函数。
Swift 不仅提供了多种不同的访问级别,还为某些典型场景提供了默认的访问级别,这样就不需要我们在每段代码中都申明显式访问级别。其实,如果只是开发一个单一 target 的应用程序,我们完全可以不用显式申明代码的访问级别。
注意
为了简单起见,对于代码中可以设置访问级别的特性(属性、基本类型、函数等),在下面的章节中我们会称之为“实体”。
模块和源文件
Swift 中的访问控制模型基于模块和源文件这两个概念。
模块指的是独立的代码单元,框架或应用程序会作为一个独立的模块来构建和发布。在 Swift 中,一个模块可以使用import 关键字导入另外一个模块。
在 Swift 中,Xcode 的每个 target(例如框架或应用程序)都被当作独立的模块处理。如果你是为了实现某个通用的功能,或者是为了封装一些常用方法而将代码打包成独立的框架,这个框架就是 Swift 中的一个模块。当它被导入到某个应用程序或者其他框架时,框架内容都将属于这个独立的模块。
源文件就是 Swift 中的源代码文件,它通常属于一个模块,即一个应用程序或者框架。尽管我们一般会将不同的类型分别定义在不同的源文件中,但是同一个源文件也可以包含多个类型、函数之类的定义。
访问级别
Swift 为代码中的实体提供了三种不同的访问级别。这些访问级别不仅与源文件中定义的实体相关,同时也与源文件所属的模块相关。
- public:可以访问同一模块源文件中的任何实体,在模块外也可以通过导入该模块来访问源文件里的所有实体。通常情况下,框架中的某个接口可以被任何人使用时,你可以将其设置为 public 级别。
- internal:可以访问同一模块源文件中的任何实体,但是不能从模块外访问该模块源文件中的实体。通常情况下,某个接口只在应用程序或框架内部使用时,你可以将其设置为 internal 级别。
- private:限制实体只能在所在的源文件内部使用。使用 private 级别可以隐藏某些功能的实现细节。
public 为最高(限制最少)访问级别,private 为最低(限制最多)访问级别。
注意
Swift 中的 private 访问级别不同于其他语言,它的范围限于源文件,而不是声明范围内。这就意味着,一个类型可以访问其所在源文件中的所有 private 实体,但是如果它的扩展定义在其他源文件中,那么它的扩展就不能访问它在这个源文件中定义的 private 实体。
访问级别基本原则
Swift 中的访问级别遵循一个基本原则:不可以在某个实体中定义访问级别更高的实体。
例如:
- 一个 public 访问级别的变量,其类型的访问级别不能是 internal 或 private。因为无法保证变量的类型在使用变量的地方也具有访问权限。
- 函数的访问级别不能高于它的参数类型和返回类型的访问级别。因为如果函数定义为 public 而参数类型或者返回类型定义为 internal 或 private,就会出现函数可以在任何地方被访问,但是它的参数类型和返回类型却不可以。
默认访问级别
如果你不为代码中的实体显式指定访问级别,那么它们默认为 internal 级别(有一些例外情况,稍后会进行说明)。因此,在大多数情况下,我们不需要显式指定实体的访问级别。
单 target 应用程序的访问级别
当你编写一个单 target 应用程序时,应用的所有功能都是为该应用服务,而不需要提供给其他应用或者模块使用,所以我们不需要明确设置访问级别,使用默认的访问级别 internal 即可。但是,你也可以使用 private 级别,用于隐藏一些功能的实现细节。
框架的访问级别
当你开发框架时,就需要把一些对外的接口定义为 public 级别,以便使用者导入该框架后可以正常使用其功能。这些被你定义为 public 的接口,就是这个框架的 API。
注意
框架依然会使用默认的 internal 级别,也可以指定为 private 级别。当你想把某个实体作为框架的 API 的时候,需显式为其指定 public 级别。
单元测试 target 的访问级别
当你的应用程序包含单元测试 target 时,为了测试,测试模块需要访问应用程序模块中的代码。默认情况下只有public 级别的实体才可以被其他模块访问。然而,如果在导入应用程序模块的语句前使用 @testable 特性,然后在允许测试的编译设置(Build Options -> Enable Testability)下编译这个应用程序模块,单元测试 target 就可以访问应用程序模块中所有 internal 级别的实体。
访问控制语法
通过修饰符 public、internal、private 来声明实体的访问级别:
public class SomePublicClass {}
internal class SomeInternalClass {}
private class SomePrivateClass {}
public var somePublicVariable = 0
internal let someInternalConstant = 0
private func somePrivateFunction() {}
除非专门指定,否则实体默认的访问级别为 internal,可以查阅默认访问级别这一节。这意味着在不使用修饰符显式声明访问级别的情况下,SomeInternalClass 和 someInternalConstant 仍然拥有隐式的访问级别 internal:
class SomeInternalClass {} // 隐式访问级别 internal
var someInternalConstant = 0 // 隐式访问级别 internal
自定义类型
如果想为一个自定义类型指定访问级别,在定义类型时进行指定即可。新类型只能在它的访问级别限制范围内使用。例如,你定义了一个 private 级别的类,那这个类就只能在定义它的源文件中使用,可以作为属性类型、函数参数类型或者返回类型,等等。
一个类型的访问级别也会影响到类型成员(属性、方法、构造器、下标)的默认访问级别。如果你将类型指定为private 级别,那么该类型的所有成员的默认访问级别也会变成 private。如果你将类型指定为 public 或者internal 级别(或者不明确指定访问级别,而使用默认的 internal 访问级别),那么该类型的所有成员的默认访问级别将是 internal。
注意
上面提到,一个 public 类型的所有成员的访问级别默认为 internal 级别,而不是 public 级别。如果你想将某个成员指定为 public 级别,那么你必须显式指定。这样做的好处是,在你定义公共接口的时候,可以明确地选择哪些接口是需要公开的,哪些是内部使用的,避免不小心将内部使用的接口公开。
public class SomePublicClass { // 显式的 public 类
public var somePublicProperty = 0 // 显式的 public 类成员
var someInternalProperty = 0 // 隐式的 internal 类成员
private func somePrivateMethod() {} // 显式的 private 类成员
}
class SomeInternalClass { // 隐式的 internal 类
var someInternalProperty = 0 // 隐式的 internal 类成员
private func somePrivateMethod() {} // 显式的 private 类成员
}
private class SomePrivateClass { // 显式的 private 类
var somePrivateProperty = 0 // 隐式的 private 类成员
func somePrivateMethod() {} // 隐式的 private 类成员
}
元组类型
元组的访问级别将由元组中访问级别最严格的类型来决定。例如,如果你构建了一个包含两种不同类型的元组,其中一个类型为 internal 级别,另一个类型为 private 级别,那么这个元组的访问级别为 private。
注意
元组不同于类、结构体、枚举、函数那样有单独的定义。元组的访问级别是在它被使用时自动推断出的,而无法明确指定。
函数类型
函数的访问级别根据访问级别最严格的参数类型或返回类型的访问级别来决定。但是,如果这种访问级别不符合函数定义所在环境的默认访问级别,那么就需要明确地指定该函数的访问级别。
下面的例子定义了一个名为 someFunction 的全局函数,并且没有明确地指定其访问级别。也许你会认为该函数应该拥有默认的访问级别 internal,但事实并非如此。事实上,如果按下面这种写法,代码将无法通过编译:
func someFunction() -> (SomeInternalClass, SomePrivateClass) {
// 此处是函数实现部分
}
我们可以看到,这个函数的返回类型是一个元组,该元组中包含两个自定义的类(可查阅自定义类型)。其中一个类的访问级别是 internal,另一个的访问级别是 private,所以根据元组访问级别的原则,该元组的访问级别是private(元组的访问级别与元组中访问级别最低的类型一致)。
因为该函数返回类型的访问级别是 private,所以你必须使用 private 修饰符,明确指定该函数的访问级别:
private func someFunction() -> (SomeInternalClass, SomePrivateClass) {
// 此处是函数实现部分
}
将该函数指定为 public 或 internal,或者使用默认的访问级别 internal 都是错误的,因为如果把该函数当做public 或 internal 级别来使用的话,可能会无法访问 private 级别的返回值。
枚举类型
枚举成员的访问级别和该枚举类型相同,你不能为枚举成员单独指定不同的访问级别。
比如下面的例子,枚举 CompassPoint 被明确指定为 public 级别,那么它的成员 North、South、East、West的访问级别同样也是 public:
public enum CompassPoint {
case North
case South
case East
case West
}
原始值和关联值
枚举定义中的任何原始值或关联值的类型的访问级别至少不能低于枚举类型的访问级别。例如,你不能在一个 internal访问级别的枚举中定义 private 级别的原始值类型。
嵌套类型
如果在 private 级别的类型中定义嵌套类型,那么该嵌套类型就自动拥有 private 访问级别。如果在 public 或者internal 级别的类型中定义嵌套类型,那么该嵌套类型自动拥有 internal 访问级别。如果想让嵌套类型拥有public 访问级别,那么需要明确指定该嵌套类型的访问级别。
子类
子类的访问级别不得高于父类的访问级别。例如,父类的访问级别是 internal,子类的访问级别就不能是 public。
此外,你可以在符合当前访问级别的条件下重写任意类成员(方法、属性、构造器、下标等)。
可以通过重写为继承来的类成员提供更高的访问级别。下面的例子中,类 A 的访问级别是 public,它包含一个方法someMethod(),访问级别为 private。类 B 继承自类 A,访问级别为 internal,但是在类 B 中重写了类 A 中访问级别为 private 的方法 someMethod(),并重新指定为 internal 级别。通过这种方式,我们就可以将某类中private 级别的类成员重新指定为更高的访问级别,以便其他人使用:
public class A {
private func someMethod() {}
}
internal class B: A {
override internal func someMethod() {}
}
我们甚至可以在子类中,用子类成员去访问访问级别更低的父类成员,只要这一操作在相应访问级别的限制范围内(也就是说,在同一源文件中访问父类 private 级别的成员,在同一模块内访问父类 internal 级别的成员):
public class A {
private func someMethod() {}
}
internal class B: A {
override internal func someMethod() {
super.someMethod()
}
}
因为父类 A 和子类 B 定义在同一个源文件中,所以在子类 B 可以在重写的 someMethod() 方法中调用super.someMethod()。
常量、变量、属性、下标
常量、变量、属性不能拥有比它们的类型更高的访问级别。例如,你不能定义一个 public 级别的属性,但是它的类型却是 private 级别的。同样,下标也不能拥有比索引类型或返回类型更高的访问级别。
如果常量、变量、属性、下标的类型是 private 级别的,那么它们必须明确指定访问级别为 private:
private var privateInstance = SomePrivateClass()
Getter 和 Setter
常量、变量、属性、下标的 Getters 和 Setters 的访问级别和它们所属类型的访问级别相同。
Setter 的访问级别可以低于对应的 Getter 的访问级别,这样就可以控制变量、属性或下标的读写权限。在 var 或subscript 关键字之前,你可以通过 private(set) 或 internal(set) 为它们的写入权限指定更低的访问级别。
注意
这个规则同时适用于存储型属性和计算型属性。即使你不明确指定存储型属性的 Getter 和 Setter,Swift 也会隐式地为其创建 Getter 和 Setter,用于访问该属性的后备存储。使用 private(set) 和internal(set) 可以改变 Setter 的访问级别,这对计算型属性也同样适用。
下面的例子中定义了一个名为 TrackedString 的结构体,它记录了 value 属性被修改的次数:
struct TrackedString {
private(set) var numberOfEdits = 0
var value: String = "" {
didSet {
numberOfEdits += 1
}
}
}
TrackedString 结构体定义了一个用于存储 String 值的属性 value,并将初始值设为 ""(一个空字符串)。该结构体还定义了另一个用于存储 Int 值的属性 numberOfEdits,它用于记录属性 value 被修改的次数。这个功能通过属性 value 的 didSet 观察器实现,每当给 value 赋新值时就会调用 didSet 方法,然后将 numberOfEdits 的值加一。
结构体 TrackedString 和它的属性 value 均没有显式指定访问级别,所以它们都拥有默认的访问级别 internal。但是该结构体的 numberOfEdits 属性使用了 private(set) 修饰符,这意味着 numberOfEdits 属性只能在定义该结构体的源文件中赋值。numberOfEdits 属性的 Getter 依然是默认的访问级别 internal,但是 Setter 的访问级别是private,这表示该属性只有在当前的源文件中是可读写的,而在当前源文件所属的模块中只是一个可读的属性。
如果你实例化 TrackedString 结构体,并多次对 value 属性的值进行修改,你就会看到 numberOfEdits 的值会随着修改次数而变化:
var stringToEdit = TrackedString()
stringToEdit.value = "This string will be tracked."
stringToEdit.value += " This edit will increment numberOfEdits."
stringToEdit.value += " So will this one."
print("The number of edits is (stringToEdit.numberOfEdits)")
// 打印 “The number of edits is 3”
虽然你可以在其他的源文件中实例化该结构体并且获取到 numberOfEdits 属性的值,但是你不能对其进行赋值。这一限制保护了该记录功能的实现细节,同时还提供了方便的访问方式。
你可以在必要时为 Getter 和 Setter 显式指定访问级别。下面的例子将 TrackedString 结构体明确指定为了public 访问级别。结构体的成员(包括 numberOfEdits 属性)拥有默认的访问级别 internal。你可以结合public 和 private(set) 修饰符把结构体中的 numberOfEdits 属性的 Getter 的访问级别设置为 public,而Setter 的访问级别设置为 private:
public struct TrackedString {
public private(set) var numberOfEdits = 0
public var value: String = "" {
didSet {
numberOfEdits += 1
}
}
public init() {}
}
构造器
自定义构造器的访问级别可以低于或等于其所属类型的访问级别。唯一的例外是必要构造器,它的访问级别必须和所属类型的访问级别相同。
如同函数或方法的参数,构造器参数的访问级别也不能低于构造器本身的访问级别。
默认构造器
如默认构造器所述,Swift 会为结构体和类提供一个默认的无参数的构造器,只要它们为所有存储型属性设置了默认初始值,并且未提供自定义的构造器。
默认构造器的访问级别与所属类型的访问级别相同,除非类型的访问级别是 public。如果一个类型被指定为 public级别,那么默认构造器的访问级别将为 internal。如果你希望一个 public 级别的类型也能在其他模块中使用这种无参数的默认构造器,你只能自己提供一个 public 访问级别的无参数构造器。
结构体默认的成员逐一构造器
如果结构体中任意存储型属性的访问级别为 private,那么该结构体默认的成员逐一构造器的访问级别就是 private。否则,这种构造器的访问级别依然是 internal。
如同前面提到的默认构造器,如果你希望一个 public 级别的结构体也能在其他模块中使用其默认的成员逐一构造器,你依然只能自己提供一个 public 访问级别的成员逐一构造器。
协议
如果想为一个协议类型明确地指定访问级别,在定义协议时指定即可。这将限制该协议只能在适当的访问级别范围内被采纳。
协议中的每一个要求都具有和该协议相同的访问级别。你不能将协议中的要求设置为其他访问级别。这样才能确保该协议的所有要求对于任意采纳者都将可用。
注意
如果你定义了一个 public 访问级别的协议,那么该协议的所有实现也会是 public 访问级别。这一点不同于其他类型,例如,当类型是 public 访问级别时,其成员的访问级别却只是 internal。
协议继承
如果定义了一个继承自其他协议的新协议,那么新协议拥有的访问级别最高也只能和被继承协议的访问级别相同。例如,你不能将继承自 internal 协议的新协议定义为 public 协议。
协议一致性
一个类型可以采纳比自身访问级别低的协议。例如,你可以定义一个 public 级别的类型,它可以在其他模块中使用,同时它也可以采纳一个 internal 级别的协议,但是只能在该协议所在的模块中作为符合该协议的类型使用。
采纳了协议的类型的访问级别取它本身和所采纳协议两者间最低的访问级别。也就是说如果一个类型是 public 级别,采纳的协议是 internal 级别,那么采纳了这个协议后,该类型作为符合协议的类型时,其访问级别也是 internal。
如果你采纳了协议,那么实现了协议的所有要求后,你必须确保这些实现的访问级别不能低于协议的访问级别。例如,一个 public 级别的类型,采纳了 internal 级别的协议,那么协议的实现至少也得是 internal 级别。
注意
Swift 和 Objective-C 一样,协议的一致性是全局的,也就是说,在同一程序中,一个类型不可能用两种不同的方式实现同一个协议。
扩展
你可以在访问级别允许的情况下对类、结构体、枚举进行扩展。扩展成员具有和原始类型成员一致的访问级别。例如,你扩展了一个 public 或者 internal 类型,扩展中的成员具有默认的 internal 访问级别,和原始类型中的成员一致 。如果你扩展了一个 private 类型,扩展成员则拥有默认的 private 访问级别。
或者,你可以明确指定扩展的访问级别(例如,private extension),从而给该扩展中的所有成员指定一个新的默认访问级别。这个新的默认访问级别仍然可以被单独指定的访问级别所覆盖。
通过扩展添加协议一致性
如果你通过扩展来采纳协议,那么你就不能显式指定该扩展的访问级别了。协议拥有相应的访问级别,并会为该扩展中所有协议要求的实现提供默认的访问级别。
泛型
泛型类型或泛型函数的访问级别取决于泛型类型或泛型函数本身的访问级别,还需结合类型参数的类型约束的访问级别,根据这些访问级别中的最低访问级别来确定。
类型别名
你定义的任何类型别名都会被当作不同的类型,以便于进行访问控制。类型别名的访问级别不可高于其表示的类型的访问级别。例如,private 级别的类型别名可以作为 public、internal、private 类型的别名,但是 public 级别的类型别名只能作为 public 类型的别名,不能作为 internal 或 private 类型的别名。
注意
这条规则也适用于为满足协议一致性而将类型别名用于关联类型的情况。
129.高级运算符(Advanced Operators)
除了在之前介绍过的基本运算符,Swift 中还有许多可以对数值进行复杂运算的高级运算符。这些高级运算符包含了在 C 和 Objective-C 中已经被大家所熟知的位运算符和移位运算符。
与 C 语言中的算术运算符不同,Swift 中的算术运算符默认是不会溢出的。所有溢出行为都会被捕获并报告为错误。如果想让系统允许溢出行为,可以选择使用 Swift 中另一套默认支持溢出的运算符,比如溢出加法运算符(&+)。所有的这些溢出运算符都是以 & 开头的。
自定义结构体、类和枚举时,如果也为它们提供标准 Swift 运算符的实现,将会非常有用。在 Swift 中自定义运算符非常简单,运算符也会针对不同类型使用对应实现。
我们不用被预定义的运算符所限制。在 Swift 中可以自由地定义中缀、前缀、后缀和赋值运算符,以及相应的优先级与结合性。这些运算符在代码中可以像预定义的运算符一样使用,我们甚至可以扩展已有的类型以支持自定义的运算符。
位运算符
位运算符可以操作数据结构中每个独立的比特位。它们通常被用在底层开发中,比如图形编程和创建设备驱动。位运算符在处理外部资源的原始数据时也十分有用,比如对自定义通信协议传输的数据进行编码和解码。
Swift 支持 C 语言中的全部位运算符,接下来会一一介绍。
按位取反运算符
按位取反运算符(~)可以对一个数值的全部比特位进行取反:
Art/bitwiseNOT_2x.png
按位取反运算符是一个前缀运算符,需要直接放在运算的数之前,并且它们之间不能添加任何空格:
let initialBits: UInt8 = 0b00001111
let invertedBits = ~initialBits // 等于 0b11110000
UInt8 类型的整数有 8 个比特位,可以存储 0 ~ 255 之间的任意整数。这个例子初始化了一个 UInt8 类型的整数,并赋值为二进制的 00001111,它的前 4 位都为 0,后 4 位都为 1。这个值等价于十进制的 15。
接着使用按位取反运算符创建了一个名为 invertedBits 的常量,这个常量的值与全部位取反后的 initialBits 相等。即所有的 0 都变成了 1,同时所有的 1 都变成 0。invertedBits 的二进制值为 11110000,等价于无符号十进制数的 240。
按位与运算符
按位与运算符(&)可以对两个数的比特位进行合并。它返回一个新的数,只有当两个数的对应位都为 1 的时候,新数的对应位才为 1:
Art/bitwiseAND_2x.png
在下面的示例当中,firstSixBits 和 lastSixBits 中间 4 个位的值都为 1。按位与运算符对它们进行了运算,得到二进制数值 00111100,等价于无符号十进制数的 60:
let firstSixBits: UInt8 = 0b11111100
let lastSixBits: UInt8 = 0b00111111
let middleFourBits = firstSixBits & lastSixBits // 等于 00111100
按位或运算符
按位或运算符(|)可以对两个数的比特位进行比较。它返回一个新的数,只要两个数的对应位中有任意一个为 1 时,新数的对应位就为 1:
Art/bitwiseOR_2x.png
在下面的示例中,someBits 和 moreBits 不同的位会被设置为 1。接位或运算符对它们进行了运算,得到二进制数值 11111110,等价于无符号十进制数的 254:
let someBits: UInt8 = 0b10110010
let moreBits: UInt8 = 0b01011110
let combinedbits = someBits | moreBits // 等于 11111110
按位异或运算符
按位异或运算符(^)可以对两个数的比特位进行比较。它返回一个新的数,当两个数的对应位不相同时,新数的对应位就为 1:
Art/bitwiseXOR_2x.png
在下面的示例当中,firstBits 和 otherBits 都有一个自己的位为 1 而对方的对应位为 0 的位。 按位异或运算符将新数的这两个位都设置为 1,同时将其它位都设置为 0:
let firstBits: UInt8 = 0b00010100
let otherBits: UInt8 = 0b00000101
let outputBits = firstBits ^ otherBits // 等于 00010001
按位左移、右移运算符
按位左移运算符(<<)和按位右移运算符(>>)可以对一个数的所有位进行指定位数的左移和右移,但是需要遵守下面定义的规则。
对一个数进行按位左移或按位右移,相当于对这个数进行乘以 2 或除以 2 的运算。将一个整数左移一位,等价于将这个数乘以 2,同样地,将一个整数右移一位,等价于将这个数除以 2。
无符号整数的移位运算
对无符号整数进行移位的规则如下:
已经存在的位按指定的位数进行左移和右移。
任何因移动而超出整型存储范围的位都会被丢弃。
用 0 来填充移位后产生的空白位。
这种方法称为逻辑移位。
以下这张图展示了 11111111 << 1(即把 11111111 向左移动 1 位),和 11111111 >> 1(即把 11111111 向右移动 1 位)的结果。蓝色的部分是被移位的,灰色的部分是被抛弃的,橙色的部分则是被填充进来的:
Art/bitshiftUnsigned_2x.png
下面的代码演示了 Swift 中的移位运算:
let shiftBits: UInt8 = 4 // 即二进制的 00000100
shiftBits << 1 // 00001000
shiftBits << 2 // 00010000
shiftBits << 5 // 10000000
shiftBits << 6 // 00000000
shiftBits >> 2 // 00000001
可以使用移位运算对其他的数据类型进行编码和解码:
let pink: UInt32 = 0xCC6699
let redComponent = (pink & 0xFF0000) >> 16 // redComponent 是 0xCC,即 204
let greenComponent = (pink & 0x00FF00) >> 8 // greenComponent 是 0x66, 即 102
let blueComponent = pink & 0x0000FF // blueComponent 是 0x99,即 153
这个示例使用了一个命名为 pink 的 UInt32 型常量来存储 CSS 中粉色的颜色值。该 CSS 的十六进制颜色值 #CC6699,在 Swift 中表示为 0xCC6699。然后利用按位与运算符(&)和按位右移运算符(>>)从这个颜色值中分解出红(CC)、绿(66)以及蓝(99)三个部分。
红色部分是通过对 0xCC6699 和 0xFF0000 进行按位与运算后得到的。0xFF0000 中的 0 部分“掩盖”了 OxCC6699 中的第二、第三个字节,使得数值中的 6699 被忽略,只留下 0xCC0000。
然后,再将这个数按向右移动 16 位(>> 16)。十六进制中每两个字符表示 8 个比特位,所以移动 16 位后 0xCC0000 就变为 0x0000CC。这个数和0xCC是等同的,也就是十进制数值的 204。
同样的,绿色部分通过对 0xCC6699 和 0x00FF00 进行按位与运算得到 0x006600。然后将这个数向右移动 8 位,得到 0x66,也就是十进制数值的 102。
最后,蓝色部分通过对 0xCC6699 和 0x0000FF 进行按位与运算得到 0x000099。这里不需要再向右移位,所以结果为 0x99 ,也就是十进制数值的 153。
有符号整数的移位运算
对比无符号整数,有符号整数的移位运算相对复杂得多,这种复杂性源于有符号整数的二进制表现形式。(为了简单起见,以下的示例都是基于 8 比特位的有符号整数的,但是其中的原理对任何位数的有符号整数都是通用的。)
有符号整数使用第 1 个比特位(通常被称为符号位)来表示这个数的正负。符号位为 0 代表正数,为 1 代表负数。
其余的比特位(通常被称为数值位)存储了实际的值。有符号正整数和无符号数的存储方式是一样的,都是从 0 开始算起。这是值为 4 的 Int8 型整数的二进制位表现形式:
Art/bitshiftSignedFour_2x.png
符号位为 0,说明这是一个正数,另外 7 位则代表了十进制数值 4 的二进制表示。
负数的存储方式略有不同。它存储的值的绝对值等于 2 的 n 次方减去它的实际值(也就是数值位表示的值),这里的 n 为数值位的比特位数。一个 8 比特位的数有 7 个比特位是数值位,所以是 2 的 7 次方,即 128。
这是值为 -4 的 Int8 型整数的二进制位表现形式:
Art/bitshiftSignedMinusFour_2x.png
这次的符号位为 1,说明这是一个负数,另外 7 个位则代表了数值 124(即 128 - 4)的二进制表示:
Art/bitshiftSignedMinusFourValue_2x.png
负数的表示通常被称为二进制补码表示。用这种方法来表示负数乍看起来有点奇怪,但它有几个优点。
首先,如果想对 -1 和 -4 进行加法运算,我们只需要将这两个数的全部 8 个比特位进行相加,并且将计算结果中超出 8 位的数值丢弃:
Art/bitshiftSignedAddition_2x.png
其次,使用二进制补码可以使负数的按位左移和右移运算得到跟正数同样的效果,即每向左移一位就将自身的数值乘以 2,每向右一位就将自身的数值除以 2。要达到此目的,对有符号整数的右移有一个额外的规则:
当对整数进行按位右移运算时,遵循与无符号整数相同的规则,但是对于移位产生的空白位使用符号位进行填充,而不是用 0。
Art/bitshiftSigned_2x.png
这个行为可以确保有符号整数的符号位不会因为右移运算而改变,这通常被称为算术移位。
由于正数和负数的特殊存储方式,在对它们进行右移的时候,会使它们越来越接近 0。在移位的过程中保持符号位不变,意味着负整数在接近 0 的过程中会一直保持为负。
溢出运算符
在默认情况下,当向一个整数赋予超过它容量的值时,Swift 默认会报错,而不是生成一个无效的数。这个行为为我们在运算过大或着过小的数的时候提供了额外的安全性。
例如,Int16 型整数能容纳的有符号整数范围是 -32768 到 32767,当为一个 Int16 型变量赋的值超过这个范围时,系统就会报错:
var potentialOverflow = Int16.max
// potentialOverflow 的值是 32767,这是 Int16 能容纳的最大整数
potentialOverflow += 1
// 这里会报错
为过大或者过小的数值提供错误处理,能让我们在处理边界值时更加灵活。
然而,也可以选择让系统在数值溢出的时候采取截断处理,而非报错。可以使用 Swift 提供的三个溢出运算符来让系统支持整数溢出运算。这些运算符都是以 & 开头的:
溢出加法 &+
溢出减法 &-
溢出乘法 &*
数值溢出
数值有可能出现上溢或者下溢。
这个示例演示了当我们对一个无符号整数使用溢出加法(&+)进行上溢运算时会发生什么:
var unsignedOverflow = UInt8.max
// unsignedOverflow 等于 UInt8 所能容纳的最大整数 255
unsignedOverflow = unsignedOverflow &+ 1
// 此时 unsignedOverflow 等于 0
unsignedOverflow 被初始化为 UInt8 所能容纳的最大整数(255,以二进制表示即 11111111)。然后使用了溢出加法运算符(&+)对其进行加 1 运算。这使得它的二进制表示正好超出 UInt8 所能容纳的位数,也就导致了数值的溢出,如下图所示。数值溢出后,留在 UInt8 边界内的值是 00000000,也就是十进制数值的 0。
Art/overflowAddition_2x.png
同样地,当我们对一个无符号整数使用溢出减法(&-)进行下溢运算时也会产生类似的现象:
var unsignedOverflow = UInt8.min
// unsignedOverflow 等于 UInt8 所能容纳的最小整数 0
unsignedOverflow = unsignedOverflow &- 1
// 此时 unsignedOverflow 等于 255
UInt8 型整数能容纳的最小值是 0,以二进制表示即 00000000。当使用溢出减法运算符对其进行减 1 运算时,数值会产生下溢并被截断为 11111111, 也就是十进制数值的 255。
Art/overflowUnsignedSubtraction_2x.png
溢出也会发生在有符号整型数值上。在对有符号整型数值进行溢出加法或溢出减法运算时,符号位也需要参与计算,正如按位左移、右移运算符所描述的。
var signedOverflow = Int8.min
// signedOverflow 等于 Int8 所能容纳的最小整数 -128
signedOverflow = signedOverflow &- 1
// 此时 signedOverflow 等于 127
Int8 型整数能容纳的最小值是 -128,以二进制表示即 10000000。当使用溢出减法运算符对其进行减 1 运算时,符号位被翻转,得到二进制数值 01111111,也就是十进制数值的 127,这个值也是 Int8 型整数所能容纳的最大值。
Art/overflowSignedSubtraction_2x.png
对于无符号与有符号整型数值来说,当出现上溢时,它们会从数值所能容纳的最大数变成最小的数。同样地,当发生下溢时,它们会从所能容纳的最小数变成最大的数。
优先级和结合性
运算符的优先级使得一些运算符优先于其他运算符,高优先级的运算符会先被计算。
结合性定义了相同优先级的运算符是如何结合的,也就是说,是与左边结合为一组,还是与右边结合为一组。可以将这意思理解为“它们是与左边的表达式结合的”或者“它们是与右边的表达式结合的”。
在复合表达式的运算顺序中,运算符的优先级和结合性是非常重要的。举例来说,运算符优先级解释了为什么下面这个表达式的运算结果会是 17。
2 + 3 % 4 * 5
// 结果是 17
如果完全从左到右进行运算,则运算的过程是这样的:
2 + 3 = 5
5 % 4 = 1
1 * 5 = 5
但是正确答案是 17 而不是 5。优先级高的运算符要先于优先级低的运算符进行计算。与 C 语言类似,在 Swift 中,乘法运算符(*)与取余运算符(%)的优先级高于加法运算符(+)。因此,它们的计算顺序要先于加法运算。
而乘法与取余的优先级相同。这时为了得到正确的运算顺序,还需要考虑结合性。乘法与取余运算都是左结合的。可以将这考虑成为这两部分表达式都隐式地加上了括号:
2 + ((3 % 4) * 5)
(3 % 4) 等于 3,所以表达式相当于:
2 + (3 * 5)
3 * 5 等于 15,所以表达式相当于:
2 + 15
因此计算结果为 17。
如果想查看完整的 Swift 运算符优先级和结合性规则,请参考表达式。如果想查看 Swift 标准库提供所有的运算符,请查看 Swift Standard Library Operators Reference。
注意
相对 C 语言和 Objective-C 来说,Swift 的运算符优先级和结合性规则更加简洁和可预测。但是,这也意味着它们相较于 C 语言及其衍生语言并不是完全一致的。在对现有的代码进行移植的时候,要注意确保运算符的行为仍然符合你的预期。
运算符函数
类和结构体可以为现有的运算符提供自定义的实现,这通常被称为运算符重载。
下面的例子展示了如何为自定义的结构体实现加法运算符(+)。算术加法运算符是一个双目运算符,因为它可以对两个值进行运算,同时它还是中缀运算符,因为它出现在两个值中间。
例子中定义了一个名为 Vector2D 的结构体用来表示二维坐标向量 (x, y),紧接着定义了一个可以对两个 Vector2D 结构体进行相加的运算符函数:
struct Vector2D {
var x = 0.0, y = 0.0
}
func + (left: Vector2D, right: Vector2D) -> Vector2D {
return Vector2D(x: left.x + right.x, y: left.y + right.y)
}
该运算符函数被定义为一个全局函数,并且函数的名字与它要进行重载的 + 名字一致。因为算术加法运算符是双目运算符,所以这个运算符函数接收两个类型为 Vector2D 的参数,同时有一个 Vector2D 类型的返回值。
在这个实现中,输入参数分别被命名为 left 和 right,代表在 + 运算符左边和右边的两个 Vector2D 实例。函数返回了一个新的 Vector2D 实例,这个实例的 x 和 y 分别等于作为参数的两个实例的 x 和 y 的值之和。
这个函数被定义成全局的,而不是 Vector2D 结构体的成员方法,所以任意两个 Vector2D 实例都可以使用这个中缀运算符:
let vector = Vector2D(x: 3.0, y: 1.0)
let anotherVector = Vector2D(x: 2.0, y: 4.0)
let combinedVector = vector + anotherVector
// combinedVector 是一个新的 Vector2D 实例,值为 (5.0, 5.0)
这个例子实现两个向量 (3.0,1.0) 和 (2.0,4.0) 的相加,并得到新的向量 (5.0,5.0)。这个过程如下图示:
Art/vectorAddition_2x.png
前缀和后缀运算符
上个例子演示了一个双目中缀运算符的自定义实现。类与结构体也能提供标准单目运算符的实现。单目运算符只运算一个值。当运算符出现在值之前时,它就是前缀的(例如 -a),而当它出现在值之后时,它就是后缀的(例如 b!)。
要实现前缀或者后缀运算符,需要在声明运算符函数的时候在 func 关键字之前指定 prefix 或者 postfix 修饰符:
prefix func - (vector: Vector2D) -> Vector2D {
return Vector2D(x: -vector.x, y: -vector.y)
}
这段代码为 Vector2D 类型实现了单目负号运算符。由于该运算符是前缀运算符,所以这个函数需要加上 prefix 修饰符。
对于简单数值,单目负号运算符可以对它们的正负性进行改变。对于 Vector2D 来说,该运算将其 x 和 y 属性的正负性都进行了改变:
let positive = Vector2D(x: 3.0, y: 4.0)
let negative = -positive
// negative 是一个值为 (-3.0, -4.0) 的 Vector2D 实例
let alsoPositive = -negative
// alsoPositive 是一个值为 (3.0, 4.0) 的 Vector2D 实例
复合赋值运算符
复合赋值运算符将赋值运算符(=)与其它运算符进行结合。例如,将加法与赋值结合成加法赋值运算符(+=)。在实现的时候,需要把运算符的左参数设置成 inout 类型,因为这个参数的值会在运算符函数内直接被修改。
func += (inout left: Vector2D, right: Vector2D) {
left = left + right
}
因为加法运算在之前已经定义过了,所以在这里无需重新定义。在这里可以直接利用现有的加法运算符函数,用它来对左值和右值进行相加,并再次赋值给左值:
var original = Vector2D(x: 1.0, y: 2.0)
let vectorToAdd = Vector2D(x: 3.0, y: 4.0)
original += vectorToAdd
// original 的值现在为 (4.0, 6.0)
注意
不能对默认的赋值运算符(=)进行重载。只有组合赋值运算符可以被重载。同样地,也无法对三目条件运算符 (a ? b : c) 进行重载。
等价运算符
自定义的类和结构体没有对等价运算符进行默认实现,等价运算符通常被称为“相等”运算符(==)与“不等”运算符(!=)。对于自定义类型,Swift 无法判断其是否“相等”,因为“相等”的含义取决于这些自定义类型在你的代码中所扮演的角色。
为了使用等价运算符能对自定义的类型进行判等运算,需要为其提供自定义实现,实现的方法与其它中缀运算符一样:
func == (left: Vector2D, right: Vector2D) -> Bool {
return (left.x == right.x) && (left.y == right.y)
}
func != (left: Vector2D, right: Vector2D) -> Bool {
return !(left == right)
}
上述代码实现了“相等”运算符(==)来判断两个 Vector2D 实例是否相等。对于 Vector2D 类型来说,“相等”意味着“两个实例的 x 属性和 y 属性都相等”,这也是代码中用来进行判等的逻辑。示例里同时也实现了“不等”运算符(!=),它简单地将“相等”运算符的结果进行取反后返回。
现在我们可以使用这两个运算符来判断两个 Vector2D 实例是否相等:
let twoThree = Vector2D(x: 2.0, y: 3.0)
let anotherTwoThree = Vector2D(x: 2.0, y: 3.0)
if twoThree == anotherTwoThree {
print("These two vectors are equivalent.")
}
// 打印 “These two vectors are equivalent.”
自定义运算符
除了实现标准运算符,在 Swift 中还可以声明和实现自定义运算符。可以用来自定义运算符的字符列表请参考运算符。
新的运算符要使用 operator 关键字在全局作用域内进行定义,同时还要指定 prefix、infix 或者 postfix 修饰符:
prefix operator +++ {}
上面的代码定义了一个新的名为 +++ 的前缀运算符。对于这个运算符,在 Swift 中并没有意义,因此我们针对 Vector2D 的实例来定义它的意义。对这个示例来讲,+++ 被实现为“前缀双自增”运算符。它使用了前面定义的复合加法运算符来让矩阵对自身进行相加,从而让 Vector2D 实例的 x 属性和 y 属性的值翻倍:
prefix func +++ (inout vector: Vector2D) -> Vector2D {
vector += vector
return vector
}
Vector2D 的 +++ 的实现和 ++ 的实现很相似,唯一不同的是前者对自身进行相加,而后者是与另一个值为 (1.0, 1.0) 的向量相加。
var toBeDoubled = Vector2D(x: 1.0, y: 4.0)
let afterDoubling = +++toBeDoubled
// toBeDoubled 现在的值为 (2.0, 8.0)
// afterDoubling 现在的值也为 (2.0, 8.0)
自定义中缀运算符的优先级和结合性
自定义的中缀运算符也可以指定优先级和结合性。优先级和结合性中详细阐述了这两个特性是如何对中缀运算符的运算产生影响的。
结合性可取的值有left,right 和 none。当左结合运算符跟其他相同优先级的左结合运算符写在一起时,会跟左边的值进行结合。同理,当右结合运算符跟其他相同优先级的右结合运算符写在一起时,会跟右边的值进行结合。而非结合运算符不能跟其他相同优先级的运算符写在一起。
结合性的默认值是 none,优先级的默认值 100。
以下例子定义了一个新的中缀运算符 +-,此运算符的结合性为 left,并且它的优先级为 140:
infix operator +- { associativity left precedence 140 }
func +- (left: Vector2D, right: Vector2D) -> Vector2D {
return Vector2D(x: left.x + right.x, y: left.y - right.y)
}
let firstVector = Vector2D(x: 1.0, y: 2.0)
let secondVector = Vector2D(x: 3.0, y: 4.0)
let plusMinusVector = firstVector +- secondVector
// plusMinusVector 是一个 Vector2D 实例,并且它的值为 (4.0, -2.0)
这个运算符把两个向量的 x 值相加,同时用第一个向量的 y 值减去第二个向量的 y 值。因为它本质上是属于“相加型”运算符,所以将它的结合性和优先级被分别设置为 left 和 140,这与 + 和 - 等默认的中缀“相加型”运算符是相同的。关于 Swift 标准库提供的运算符的结合性与优先级,请参考 Swift Standard Library Operators Reference。
注意
当定义前缀与后缀运算符的时候,我们并没有指定优先级。然而,如果对同一个值同时使用前缀与后缀运算符,则后缀运算符会先参与运算。