协议(Protocols)

694 阅读31分钟

原文

协议定义了一个方法,属性,其他要求的满足一个特殊任务或者一块功能蓝本。然后协议可以被提供那些要求的实际实现的类,结构体或者枚举采用。任何满足协议要求的类型可以说是遵守那个协议。

除了指定遵循的类型必须实现的必要条件,你可以扩展一个协议来实现一些必要条件或者实现遵循的类型可以使用的额外功能。

协议语法(Protocol Syntax)

用和类,结构体,和枚举非常相似的方式来定义协议:

protocol SomeProtocol {
    // protocol definition goes here
}

在他们的定义中,自定义类型通过在类型名称后面加协议的名字,用欧冠冒号分隔来表示他们采用一个特定的协议。可以列出多个协议,并用逗号分隔:

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

如果一个类有父类,在它采用的任何协议之前列出父类的名字,跟着一个逗号:

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

属性必要条件(Property Requirements)

一个协议需要任何一个遵循的类型用特定的名称和类型来提供实例属性和类型属性。协议没有指定属性是存储属性还是计算属性--它只指定了必要的属性名称和类型。协议也制定了每个属性是否必需是可读的或者可读可写的。

如果协议需要一个属性是可读可写的,属性的要求不能由常量存储属性或者只读计算属性来实现。如果协议只要求属性是可读的,要求可以由任何种类的属性满足,如果对你的代码有用也可写的属性也是有效的。

属性要求通常声明为可变属性,前缀用var关键字。可读和可写的属性通过在他们的类型后面写{get set}来指定,gettable苏醒通过写{get}来指定。

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

当在协议中定义它们时通常用static关键字作为类型属性需求的前缀。即使类型属性必要条件由类实现的时候可以用class或者static做前缀,这条规则也使用:

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

这里是一个只有一个实例属性要求的协议的例子:

protocol FullyNamed {
    var fullName: String { get }
}

FullyNamed协议需要遵循协议的类型提供一个完全限定的名字。协议没有指定其他关于遵循的类型的性质的东西--它只制定了这个类型必需可以给自己提供一个全名。协议声明任何FullyNamed类型必需有一个gettable名为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协议的单一的需求,并且意味着Person正确的遵循了协议。(如果一个协议的要求没有满足swift会报告一个编译时错误。)

这里是一个更复杂的类,也采用并遵循了FullNamed协议:

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。如果存在的话fullName属性使用prefix值,并且把它放在name的开始来为starship创建一个全名。

方法必要条件(Method Requirements)

协议可以要求指定的实例方法和类型方法由遵循类型实现。这些方法在协议的定义中和普通实例和类型方法一样的方式来写,但是没有花括号或方法体。允许可变参数,和普通方法一样的限制规则。默认值,无论如何,在协议的定义中不能为方法参数指定。

作为类型属性的要求,当他们在协议中定义的时候通常用static作为前缀在类型方法的要求前。当类型方法被一个类实现时就算用class或者static关键字作为类型方法要求的前缀的也可以:

protocol SomeProtocol {
    static func someTypeMethod()
}

下面的例子用一个简单的实例方法要求来定义一个协议:

protocol RandomNumberGenerator {
    func random() -> Double
}

这个协议,RandomNumberGenerator,需要任何一个遵循的类型有一个名为random的实例方法,当它被调用的时候返回一个Double值。即使没有在协议中指定,会认为这个值会是一个从0.0到(不包括)1.0的数字。

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)
            .truncatingRemainder(dividingBy:m))
        return lastRandom / m
    }
}
let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// Prints "Here's a random number: 0.3746499199817101"
print("And another one: \(generator.random())")
// Prints "And another one: 0.729023776863283"

可变方法必要条件(Mutating Method Requirements)

有时候需要方法修改它属于的实例。对于值类型中的实例方法(结构体和枚举),在方法的func关键字前放一个mutating关键字来指定允许方法修改他所属于的实例和这个实例任何的属性。这个过程描述在Modifying Value Types from Within Instance Methods

如果你定义了一个协议实例方法要求,他要修改采用协议的任何类型的实例,在协议声明的时候用mutating关键字标记方法。这使结构体和枚举可以采用协议并且满足方法要求。

如果你把协议实例方法要求标记为mutating,当为一个类写这个方法的实现的时候不需要写mutating关键字。mutating关键字只用于结构体和枚举。

下面的例子定义了一个名为Togglable的协议,定义了一个名为toggle的单独的实例方法要求。像他名字表示的,toggle()方法要切换或者转换任何遵循的类型的状态,当它被调用的时候通常是修改遵循的实例的状态:

protocol Togglable {
    mutating func toggle()
}

如果你为一个结构体或者枚举实现了Togglable协议,结构体或者枚举可以通过提供一个标记了mutating的toggle()方法的实现来遵循协议。

下面的例子定义了一个名为OnOffSwitch的枚举。这个枚举在两个状态之间切换,用枚举cases的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 is now equal to .on

初始化器必要条件(Initializer Requirements)

协议可以要求让遵循的类型实现指定的初始化器。在协议的定义中用像平常初始化器一样的方式写这些初始化器,但是没有花括号和初始化器体:

protocol SomeProtocol {
    init(someParameter: Int)
}

协议初始化器必要条件的类实现(Class Implementations of Protocol Initializer Requirements)

你可以在一个遵循的类像设计初始化器或者便利初始化器一样实现一个协议的初始化器要求。两种情况中,必须用required修饰词标记初始化器的实现:

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

required修饰词的使用确保你在遵循的类的全部子类中提供一个明确的的或者继承的初始化器要求的实现,如此以致于他们也遵循了协议。

更多关于必需初始化器的信息,查看Required Initializers

你不需要在标记了final修饰词的类中用required修饰词标记协议初始化器的实现,因为final类不能子类化。更多关于final修饰词,查看Preventing Overrides

如果子类重写了父类中的设计初始化器,也实现了协议中匹配的初始化器要求,用required和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 Requirements)

协议可以为遵循的类定义可是白的初始化器要求,像在Failable Initializers中定义的。

一个可失败的初始化器要求可以由遵循的类型上的可失败或者不可失败的初始化器来满足。一个不可失败的初始化器要求可以由一个不可是白的初始化器或者一个隐式解包的可失败的初始化器满足。

协议作为类型(Protocols as Types)

协议本身不能实现任何功能。但是,你可以在代码中把协议作为一个完全成熟的类型。把协议作为类型使用有时称为existential类型,从短语“there exists a type such that conforms to the protocol”中而来。

你可以在很多允许其他类型的地方会用协议,包括:

  • 在函数,方法,或者初始化器中作为一个参数类型
  • 作为常量,变量或者属性的类型
  • 作为数组,字典,或者其他容器中的对象的类型

因为协议是类型,用一个大写字母(例如FullyNamed和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的类,表示一个在board游戏中的n边筛子。Dice实例有一个名为sides的整型属性,表示他们有多少边,和一个名为generator的属性,提供生成筛子摇动值的随机数字生成器。

generator属性是一个RandomNumberGenerator类型。所以,你可以把它设置为任何采用了RandomNumberGenerator协议的类型的实例。你分配给这个属性的实例不需要其他东西,除了这个实例必需采用RandomNumberGenerator协议。以为他的类型是RandomNumberGenerator,Dice类中的代码这可以用对遵循了这个协议的全部生成器使用的方式来和generator进行交互。这意味着你不能使用任何由生成器的底层类型定义的方法和属性。不过,你可以用和从父类向下转换为子类一样的方式来把协议类型向下转换为底层的类型,像在Downcasting中描述的。

Dice也有一个初始化器,来设置它的初始状态。这个初始化器有一个名为generator的参数,也是RandomNumberGenerator类型。当初始化一个新的Dice实例的时候你可以传任何一个遵循的类型给这个参数。

Dice提供一个实例方法,roll,他返回一个在1和筛子的面数范围之间的一个整型值。这个方法调用生成器的random()方法来创建一个新的在0.0和1.0之间的随机数。因为知道generator采用了RandomNumberGenerator,保证他有一个random()方法可以调用。

这里是Dice类如何用一个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

代理(Delegation)

Delegation是一个可以让类或者结构体将一些他们的职责交给(或者代理)其他类型的实例的设计模式。这个设计模式通过定义一个封装了代理职责的协议来实现,例如一个实现的类型(称为代理)保证提供被代理了的功能。代理可以用于响应一个特定的动作,或者不需要知道那个资源的底层类型来从外部资源中获取数据。

下面的例子定义了两个为了和在筛子的基础上的板子游戏一起使用的协议:

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

协议DiceGame是一个可以被任何包含筛子的游戏采用的协议。

DiceGameDelegate协议可以被用于跟踪一个DiceGame的进度。为了防止强引用循环,代理使用weak引用声明。关于weak引用的信息,查看Strong Reference Cycles Between Class Instances。把协议标记为class-only使本章节后面的SnakesAndLadders类声明他的代理必需使用一个weak引用。class-only的协议通过它继承自AnyObject来标记,像Class-Only Protocols中讨论的。

这里是一个原先在Control Flow中介绍的Snakes and Ladders游戏的版本。这个版本改为给他的dice-rolls使用一个Dice实例;采用DiceGame协议;通知一个DiceGameDelegate关于他的进度:

class SnakesAndLadders: DiceGame {
    let finalSquare = 25
    let dice = Dice(sides: 6, generator: LinearCongruentialGenerator())
    var square = 0
    var board: [Int]
    init() {
        board = Array(repeating: 0, count: finalSquare + 1)
        board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
        board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
    }
    weak 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)
    }
}

关于Snakes and Ladders游戏玩法的描述,查看Break

游戏的这个版本封装为一个名为SnakesAndLadders的类,它采用了DiceGame协议。为了遵守协议,他提供了一个gettable的属性dice和一个play()方法。(dice属性声明为一个常量属性,因为他在初始化后不需要改变,协议只需要它是gettable。)

Snakes and Ladders 游戏板子的配置在类的init()初始化器中。所有的游戏逻辑移动到了协议play方法中,它用了协议必要条件dice属性来提供dice摇晃的值。

注意代理属性定义为一个可选的DiceGameDelegate,因为为了这个游戏,代理不是必需的。因为它的可选类型,代理属性自动设置为初始化值nil。从此之后,game初始化器可以选择把属性设置为合适的代理。因为DiceGameDelegate协议是class-only的,可以把代理声明为weak来防止循环引用。

DiceGameDelegate为追踪游戏的进度提供了三个方法。这三个方法已经合并到了上面play()方法中的游戏逻辑里,当一个新游戏开始,新的切换开始,或者游戏结束的的时候调用。

因为delegate属性是一个可选DiceGameDelegate,每次在代理上调用方法的时候play()方法使用可选链。如果delegate属性是nil,这些代理调用没有错误地优雅的结束。如果代理属性是nin-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 += 1
        print("Rolled a \(diceRoll)")
    }
    func gameDidEnd(_ game: DiceGame) {
        print("The game lasted for \(numberOfTurns) turns")
    }
}

DiceGameTracker实现了三个全部的DiceGameDelegate要求的方法。使用这三个方法类保持追踪游戏切换发生的次数。当游戏开始的时候将numberOfTurns属性重设为0,每次新一轮开始的时候给他增加,并且一旦游戏结束打印出切换的总数。

上面展示的gameDidStart(_:)的实现使用game参数来打印一些将要玩的游戏的介绍信息。game参数有一个DiceGame类型,不是SnakesAndLadders,所以gameDidStart(_:)可以访问和使用在DiceGame协议中实现的方法和属性。不过,方法仍然可以使用类型转换来查询底层实例的类型。在这个例子中,他检查game背后是否实际上是一个SnakeAndLadders的实例,如果是的话打印一个合适的信息。

gameDidStart(_:)方法也访问传进来的game参数的dice属性。因为game已经知道了遵循DiceGame协议,保证有一个dice属性,所以gameDidStart(_:)方法可以访问和打印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

用扩展增加遵循协议(Adding Protocol Conformance with an Extension)

可以扩展一个已存在的类型来采用和遵循一个新协议,即使你没有对已存在类型源码的访问。扩展可以给已存在的类型增加新的属性,方法,和下标,所以可以增加任何协议需要的要求。更多关于扩展,查看Extensions

当在扩展中给实例的类型增加遵循的时候一个类型的已存在的实例自动采用和遵循一个协议。

例如,这个协议,名为TextRepresentable,可以被任何类型实现,有一种用文本表示的方式。这可能是让自己的描述,或者他当前状态的文本版本:

protocol TextRepresentable {
    var textualDescription: String { get }
}

上面的Dice类可以扩展来采用和遵循TextRepresentable:

extension Dice: TextRepresentable {
    var textualDescription: String {
        return "A \(sides)-sided dice"
    }
}

扩展采用新协议的方式和Dice在原始实现中提供的一样。协议名称在类型名字后面,用分号分隔,并且协议全部去求得实现在扩展花括号之中提供。

现在任何Dice实例可以按TextRepresentable一样对待:

let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())
print(d12.textualDescription)
// Prints "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)
// Prints "A game of Snakes and Ladders with 25 squares"

有条件遵循协议(Conditionally Conforming to a Protocol)

泛型类型可能只在特定的情况下可以满足协议的要求,例如当类型的泛型参数遵循协议的时候。当扩展类型的时候你可以通过列出限制条件指定泛型类型有条件的遵循协议。通过写一个泛型的where从句在你要采用的协议名称后面写出这些约束。更多关于泛型where从句,查看Generic Where Clauses

下面的扩展当他们存储遵循TextRepresentable类型的元素的时候使Array实例遵循TextRepresentable协议。

extension Array: TextRepresentable where Element: TextRepresentable {
    var textualDescription: String {
        let itemsAsText = self.map { $0.textualDescription }
        return "[" + itemsAsText.joined(separator: ", ") + "]"
    }
}
let myDice = [d6, d12]
print(myDice.textualDescription)
// Prints "[A 6-sided dice, A 12-sided dice]"

用扩展声明协议使用(Decclaring  Protocol Adoption with an Extension)

如果一个类型已经遵循了协议的全部要求,但是还没有声明它采用了协议,你可以用一个空扩展让他采用协议:

struct Hamster {
    var name: String
    var textualDescription: String {
        return "A hamster named \(name)"
    }
}
extension Hamster: TextRepresentable {}

在任何需要TextRepresentable类型的地方Hamster的实例现在可以使用:

let simonTheHamster = Hamster(name: "Simon")
let somethingTextRepresentable: TextRepresentable = simonTheHamster
print(somethingTextRepresentable.textualDescription)
// Prints "A hamster named Simon"

类型不会通过满足他的需求自动采用一个协议。他们必须明确声明协议的采用。

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

像在Protocols as Types中提到的,一个协议可以像一个数组或者字典一样作为类型存储在一个序列中。例子创建了一个TextRepresentable的数组:

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

他现在可能遍历数组中的对象,打印每个对象的文本描述:

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,即使背后实际的实例时这些类型之一。但是,因为它是TextRepresentable类型,并且任何是TextRepresentable的东西都认为有一个textualDescription属性,每次通过循环访问thing.textualDescription是安全的.

协议继承(Protocol Inheritance )

一个协议可以继承一个或者多个其他协议并且在他继承的最上层的需求增加更多的需求。协议继承的语法和类继承的语法很像,但是可以选择列出多个继承的协议,用逗号分隔:

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

这里是一个继承了上面的TextRepresentable协议的协议的例子:

protocol PrettyTextRepresentable: TextRepresentable {
    var prettyTextualDescription: String { get }
}

这个例子定义了一个新的协议,PrettyTextRepresentable,它从TextRepresentable中继承,任何采用PrettyTextRepresentable的对象必需满足TextRepresentable强制的全部要求,加上PrettyTextRepresentable强制要求的额外的要求。在这个例子中,PrettyTextRepresentable增加了一个简单的要求来提供一个gettable的名为prettyTextualDescription并返回一个String的属性。

SnakesAndLadders类可以扩展来采用和遵循PrettyTextRepresentable:

extension SnakesAndLadders: PrettyTextRepresentable {
    var prettyTextualDescription: String {
        var output = textualDescription + ":\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类型提供一个prettyTextualDescription属性的实现。任何事PrettyTextRepresentable的东西也必须是TextRepresentable,所以prettyTextualDescription的实现开始是访问TextRepresentable协议的textualDescription属性来开始一个输出字符串。然后他遍历板子方块的数组,并增加一个集合形状来表示每个方格的内容:

  • 如果方格的值大于0,它是梯子的底部,并且用△表示。
  • 如果方格的值小于0,它是蛇的头部,用▽表示。
  • 否则,方块的值时0,是自由地方格,用○表示。

prettyTextualDescription属性现在可以用来打印一个任何SnakesAndLadders实例的漂亮的文本描述

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

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

你可以通过把AnyObject协议增加到协议的继承列表中来把协议的采用限制到类类型(不是结构体和枚举)。

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

在上面的例子中,SomeClassOnlyProtocol只可以被类类型采用。写尝试采用SomeClassOnlyProtocol的结构体和枚举的定义时是一个编译期错误。

当协议的要求定义的表现建设或者需要一个遵循的类型有引用语义而不是值语义的时候使用class-only协议。更多关于引用和值语义,查看Structures and Enumerations Are Value TypesClasses Are Reference Types

协议组合(Protocol Composition)

有时候需要一个类型一次性遵循多个协议比较有用。你可以用协议组合把多个协议组合到一个单一的要求中。协议组合表现就像你临时定义了一个结合了组合中全部协议的要求的临时的本地协议。协议组合没有定义任何新的协议类型。

协议组合的形式是SomeProtocol&AnotherProtocol。你可以列出你需要的全部协议,用ampersands(&)分隔。除了他的协议列表,一个协议组合也可以包含一个类类型,你可以用来指定一个需要的父类。

这里是一个在函数参数中把两个名为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(to celebrator: Named & Aged) {
    print("Happy birthday, \(celebrator.name), you're \(celebrator.age)!")
}
let birthdayPerson = Person(name: "Malcolm", age: 21)
wishHappyBirthday(to: birthdayPerson)
// Prints "Happy birthday, Malcolm, you're 21!"

在这个例子中,Named协议有一个单一的名为name的gettable String属性的要求。Aged协议有一个单一的名为age的gettable Int属性的需求。两个协议被一个你各位Person的结构体采用。

例子也定义了一个wishHappyBirthday(to:)函数。celebrator参数类型是Named&Aged,意味着“任何遵循了两个Named和Aged协议的类型”。不管什么特殊的类型传给函数,只要他遵循两个必要的协议。

这个例子之后创建一个名为birthdayPerson的Person实例并且把这个新的实例传给wishHappyBirthday函数。因为Person遵循两个协议,所以调用有效,wishHappyBirthday(to:)函数可以打印它的生日问候。

这里是一个结合了之前例子中Named协议和一个Location类的例子:

class Location {
    var latitude: Double
    var longitude: Double
    init(latitude: Double, longitude: Double) {
        self.latitude = latitude
        self.longitude = longitude
    }
}
class City: Location, Named {
    var name: String
    init(name: String, latitude: Double, longitude: Double) {
        self.name = name
        super.init(latitude: latitude, longitude: longitude)
    }
}
func beginConcert(in location: Location & Named) {
    print("Hello, \(location.name)!")
}

let seattle = City(name: "Seattle", latitude: 47.6, longitude: -122.3)
beginConcert(in: seattle)
// Prints "Hello, Seattle!"

beginConcert(in:)函数采用了一个Location&Named类型的参数,意味着“任何是Location的子类的类型并且遵循Named协议。”这种情况下,City满足两个要求。

把birthdayPerson传递给beginConcert(in:)函数使用有效的,因为Person不是Location的子类。同样的,如果你制造了一个没有遵循Named协议的Location的子类,用哪个类型的实例调用beginConcert(in:)是无效的。

检查协议遵循(Checking for Protocol Conformance)

你可以使用在Type Casting中描述的is和as操作符来为协议遵循做检查,和转换为一个指定的协议。检查和转换为一个协议和检查转换一个类遵循完全一样的语法:

  • 如果一个实例遵循一个协议is操作符返回一个true,如果不是返回false。
  • 向下转换操作符的as?版本返回一个协议的类型的可选值,如果实例没有遵循协议这个值是nil。
  • 向下转换操作符的as!版本强制向下转换为协议类型并且如果转换没有成功的话触发运行时错误。

这个例子定义一个名为HasArea的协议,有一个单一的属性要求,gettable 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 }
}

Cirecle,Country和Animal类没有一个共享的基础类。不过,他们都是类,所以三个类型的实例可以用来初始化一个存储的值的类型为AnyObject的数组:

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

objects数组的初始化通过一个字面量的数组包括一个radius是2单位的Circle实例,一个用英国平方公里的表面积初始化的Country实例;和一个有四条腿的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协议,由ad?操作符返回的可选值用可选绑定解包到名为objectWithArea的常量中。objectWithArea常量是HasArea类型的,所以可以访问它的area属性并且以类型安全的方式打印。

注意底层对象不会由转化过程改变。他们仍然是Cirecle,Conutry和Animal。不过,在他们存储在objectWithArea常量中的时候,只认为他们是HasArea类型,所以他们的area属性可以访问到。

可选协议必需条件(Optional Protocol Requirements)

你可以为协议定义可选要求。这些要求不用要求遵循协议的类型必须实现。可选要求在协议的生命中用optional修饰词作为前缀。可选要求是可以获取到的所以你可以写和Objective-C交互的代码。协议和可选要求必须用@objc属性标记。注意@objc协议只可以有继承自Objective-C的类或者其他@objc类采用。他们不能用于结构体或者枚举。

当你在可选要求中使用一个方法或者属性的时候,它的类型自动变为一个可选值。例如,一个方法类型是(Int)->String变为((Int)->String)?。注意整个函数类型包装在可选类型中,不是方法的返回值。

一个可选协议要求可以用可选链调用,示意要求没有本遵从协议的类型实现的可能。当调用的时候通过在方法名称后面写问号来检查可选方法的实现,例如someOptionalMethod?(someArgument)。关于可选链的信息,查看Optional Chaining

下面的例子定义了一个名为Counter的integer-counting类,使用一个外部的数据源来提供他的增加的数字。这个数据源通过CounterDataSource协议来定义,它有两个可选的要求:

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

CounterDataSource协议定义一个名为increment(forCount:)的可选方法要求和一个名为fixedIncrement的可选属性要求。这些要求为数据源定义两个不同的方式来为Counter实例提供合适的增加数字。

直接调用,你可以写一个协议要求都没有实现的遵循CounterDataSource协议的自定义类。他们都是可选的,不过。即使技术上允许,这不是一个好的数据源。

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

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

Counter类把它当前的值存储在一个名为count的可选属性。Counter类也定义了一个名为increment的方法,每次方法调用的时候增加count属性。

increment()方法通过在它的data source上查找increment(forCount:)方法的实现来获取一个增加的数量。increment()方法使用可选链来尝试调用increment(forCount:),传递当前的作为方法的唯一参数的count值。

注意两层可选链在这里进行。首先,DataSource可能是nil,所以DataSource在它的名字后面有一个问号来表明只用DataSource不是nil的时候increment(forCount:)会被调用。第二,即使dataSource存在,不能保证它实现了increment(forCount:),因为它是可选的要求。这里,increment(forCount:)可能没有实现的可能也被可选链处理。只有increment(forCount:)存在的时候才会调用increment(forCount:)--U额就是,如果他不是nil。这是为什么increment(forCount:)也在他的名字后面写一个问号。

因为调用increment(forCount:)可能因为两个原因中的任何一个失败,调用返回一个可选Int值。即使在CounterDataSource的定义中increment(forCount:)是按返回非可选可行的Int值定义的也是这样。即使有两个可选链操作,一个在另一个后面,结果仍然包装在一个单一的可选值中。更多关于多可选链操作的信息,查看Linking Multiple Levels of Chaining

在调用increment(forCount:)之后,他返回的可选Int解包到一个名为amount的常量中,使用可选绑定。如果可选Int包含一个值--也就是,如果代理和方法都存在,方法返回一个值--解包的amount加到存储的count属性上,增加完成。

不可能从increment(forCount:)方法中获取一个值--不管以为DataSource是nil,或者因为data source没有实现increment(forCount:)--然后increment()方法替换为尝试从data source的fixedIncrement属性中回去值。fixedIncrement属性也是一个可选的要求,所以它的值是一个可选的Int值,即使fixedincrement在CounterDataSource协议的定义中被定义为一个非可选的Int属性。

这里是一个简单的在每次查询的时候datasource返回一个值为3的常量的CounterDataSource的实现。通过实现可选的fixedIncrement属性要求实现:

class ThreeSource: NSObject, CounterDataSource {
    let fixedIncrement = 3
}

你可以为一个新的Counter实例使用一个ThreeSource的实例作为data source:

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

上面的代码创建一个新的Counter实例;把他的data source设置为一个新的ThreeSource实例,increment()方法调用了四次。按照预期,每次调用increment()的时候Counter的count属性加3.

这里是一个更复杂的名为TowardsZeroSource的data source,使Counter实例从他当前的值增加或者减到0:

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

TowardsZeroSource类实现CounterDataSource协议中的可选increment(forCount:)方法并且使用count参数值来决定哪个方向计数。如果count已经是0,方法返回0指明不要再有更多的计算发生。

可以使用一个TowardsZeroSource实例和以存在的Counter实例来从-4开始计算到0.一旦Counter达到了0,不再有计算发生:

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

协议扩展(Protocol Extensions)

协议可以扩展来给遵循的类型提供方法,初始器,下标,和计算属性的实现。这使你可以在协议上定义表现,而不是在每个类型的单独的一致性或者一个全局函数。

例如,RandomNumberGenerator协议可以扩展来提供一个randomBool()方法,使用必要的random()方法的结果来返回一个随机的Bool值:

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

通过在协议上创建一个扩展,全部的遵循类型不需要额外的修改自动获得这个方法的实现。

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

协议扩展可以给遵循的类型增加实现但是不能生成一个协议扩展或者从其他协议中继承。协议继承通常在协议自己的声明中指定。

提供默认实现(Providing Default Implementations)

你可以使用协议扩展来给协议的任何方法或者计算属性要求提供一个默认的实现。如果一个遵循的类型提供了自己的关于必需的方法或者属性的实现,那个实现会用来替代扩展提供的。

通过扩展提供了默认实现的协议要求和可选的协议要求不一样。即使遵循的类型不需要一定提供他们都实现,有默认实现的要求可以不用可选链来调用。

例如,协议PrettyTextRepresentable,继承了TextRepresentable协议可以提供一个他的必需的prettyTextualDescription属性的默认实现来简单的返回访问textualDescription属性的结果:

extension PrettyTextRepresentable  {
    var prettyTextualDescription: String {
        return textualDescription
    }
}

给协议扩展增加约束(Adding Constraints to Protocol Extensions)

当你定义一个协议扩展的时候,你可以指定限制,遵循的类型在可以获取扩展的方法和属性之前必需要满足的。通过写一个泛型where从句在你扩展的协议名称后面写这些约束。更多关于where从句的信息,查看Generic Where Clauses

例如,你可以给Collection协议定义一个扩展,应用在元素都遵循Equatable协议的任何集合。通过约束一个结合的元素遵循Equatable协议,标准库的一部分,你可以使用==和!=操作符来检查两个元素是相等的还是不相等的。

extension Collection where Element: Equatable {
    func allEqual() -> Bool {
        for element in self {
            if element != self.first {
                return false
            }
        }
        return true
    }
}

allEqual方法只有集合中全部属性都相等的时候返回true。

考虑两个整型的数组,一个全部的元素都相等,一个不是:

let equalNumbers = [100, 100, 100, 100, 100]
let differentNumbers = [100, 100, 200, 100, 200]

因为数组遵循Collection并且整型遵循Equatable,equalNumbers和differentNumbers可以使用allEqual()方法:

print(equalNumbers.allEqual())
// Prints "true"
print(differentNumbers.allEqual())
// Prints "false"

如果一个遵循的类型满足多个约束的给相同的方法或者属性提供实现的扩展,swift使用最专门的约束的相对应的实现。