美文网首页
Swift - 18.协议

Swift - 18.协议

作者: 顺手给我new一个对象 | 来源:发表于2017-03-27 13:58 被阅读45次

最近过得太放松了,打码的双手都感觉要生锈了,像我这种不虐不活(不虐待自己感觉不曾活过)的人怎么能忍受,周六在七姑娘的虐待下,受虐倾向越显严重,不能自拨,今天来一篇让我很抗拒的Swift Protocol更新,轻虐一下。

协议 定义了一个蓝图,规定了用来实现某一特定任务或者功能的方法、属性,以及其他需要的东西。类、结构体或枚举都可以遵循协议,并为协议定义的这些要求提供具体实现。某个类型能够满足某个协议的要求,就可以说该类型遵循这个协议。

除了遵循协议的类型必须实现的要求外,还可以对协议进行扩展,通过扩展来实现一部分要求或者实现一些附加功能,这样遵循协议的类型就能够使用这些功能。

一、协议语法

协议的定义方式与类、结构体和枚举的定义非常相似:

protocol SomeProtocol {
    // 这里是协议的定义部分
}

要让自定义类型遵循某个协议,在定义类型时,需要在类型名称后加上协议名称,中间以冒号(:)分隔。遵循多个协议时,各协议之间用逗号(,)分隔:

struct SomeStructure: FirstProtocol, AnotherProtocol {
    // 这里是结构体的定义部分
}

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

class SomeClass: SomeSuperClass, FirstProtocol, AnotherProtocol {
    // 这里是类的定义部分
}

二、属性要求

协议可以要求遵循协议的类型提供特定名称和类型的实例属性或类型属性。协议不指定属性是存储型属性还是计算型属性,它只指定属性的名称和类型。此外,协议还指定属性是可读的还是可读可写的。

如果协议要求属性是可读可写的,那么该属性不能是常量属性或只读的计算型属性。如果协议只要求属性是可读的,那么该属性不仅可以是可读的,如果代码需要的话,还可以是可写的。

协议总是用 var 关键字来声明变量属性,在类型声明后加上 { set get } 来表示属性是可读可写的,可读属性则用 { get } 来表示:

2.1 一个简单遵循协议的结构体

//这是一个只含有一个实例属性要求的协议:
protocol FullyNamed {
    var fullName: String { get }
}
//定义了一个叫Person的结构体,用来表示一个具体有名字的人。Person遵循了FullyNamed协议:
struct Person: FullyNamed {
    var fullName: String
}

//调用:
let john = Person(fullName: "John Appleseed")
print(john.fullName)//John Appleseed

2.2下面我们来定义一个更为复杂的类,遵循协议

//Starship类把fullName属性实现为只读的计算型属性。每个Starship类的实例都有一个名为name的非可选属性和一个名为prefix的可选属性。
//当prefix存在时,计算型属性fullName会将prefix插入到name 之前,从而为Starship构建一个全名。
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")
print(ncc1701.fullName);//USSEnterprise

三、方法要求

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

正如属性要求中所述,在协议中定义类方法的时候,总是使用 static 关键字作为前缀。当类类型遵循协议时,除了 static 关键字,还可以使用 class 关键字作为前缀:

protocol SomeProtocol {
    static func someTypeMethod()
}

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

//RandowNumberGenerator协议要求遵循协议的类型必须拥有一个名为random,返回值类型为Double的实例方法。尽管这里并未指明,但是我们假设返回值在[0.0,1.0)区间内
//RandowNumberGenerator协议并不关心每个随机数是怎样生产的,它只要求必须提供一个随机数生成器
protocol RandowNumberGenerator {
    func random() -> Double
}

//该类遵循并符合RandowNumberGenerator协议。该类实现了一个叫做 线性同余生成器 的伪随机数算法
class LinearCongruentialGenerator: RandowNumberGenerator {
    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 random number: \(generator.random())")//Here's random number: 0.37464991998171
print("And another one: \(generator.random())")//And another one: 0.729023776863283

四、Mutating方法要求

有时需要在方法中改变方法所属的实例。例如,在值类型(即结构体和枚举)的实例方法中,将 mutating 关键字作为方法的前缀,写在 func 关键字之前,表示可以在该方法中修改它所属的实例以及实例的任意属性的值。

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

protocol Togglable{
    //toggle() 方法将改变实例属性,从而切换遵循该协议类型的实例的状态。
    //使用 mutating 关键字标记,这表明当它被调用时,该方法将会改变遵循协议的类型的实例
    mutating func 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()
print(lightSwitch) //on
lightSwitch.toggle()
print(lightSwitch) //off

五、构造器要求

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

5.1构造器要求在类中的实现

protocol SomeProtocol {
    init(someParameter: Int)
}

//你可以在遵循协议的类中实现构造器,无论是作为指定构造器,还是作为便利构造器。无论哪种情况,你都必须为构造器实现标上required修饰符
class SomeClass: SomeProtocol{
    //使用required修饰符可以确保所有子类也必须提供此构造器实现,从而也能符合协议
    required init(someParameter: Int) {
        //这里是构造器的实现部分
    }
}

子类重写了父类的指定构造器,并且该该构造器满足了某个协议的要求

//1.2 如果一个子类重写了父类的指定构造器,并且该该构造器满足了某个协议的要求,那么该构造器的实现需要同事标注required和override修饰符
protocol SomeProtocol1 {
    init()
}

class SomeSuperClass {
    init() {
        //这里是构造器的实现部分
    }
}

class SomeSubClass: SomeSuperClass, SomeProtocol1 {
    //因为遵循协议,需要加上required
    //因为继承自父类,需要加上override
    required override init(){
        //这里是构造器的实现部分
    }
}

5.2 协议作为类型
尽管协议本身并未实现任何功能,但是协议可以被当做一个成熟的类型来使用。
协议可以像其他普通类型一样使用,使用场景如下:

  • 作为函数、方法或构造器中的参数类型或返回值类型
  • 作为常量、变量或属性的类型
  • 作为数组、字典或其他容器中的元素类型
//下面是将协议作为类型使用的例子
class Dice {
    let sides: Int
    let generator: RandowNumberGenerator
    //Dice还有一个构造器,用来设置初始状态。构造器有一个为generator,类型为RandowNumberGenerator的形参。
    //在调用构造方法创建Dice的实例时,可以传入任何遵循RandowNumberGenerator协议的实例给generator。
    init(sides: Int, generator:RandowNumberGenerator) {
        self.sides = sides
        self.generator = generator
    }
    /*
     */
    
    //Dice类提供了一个名为roll的实例方法,用来模拟骰子的面值。它先调用generator的random()方法来生成一个[0.0,1.0)区间内的随机数,然后使用这个随机数生成正确的骰子面值。因此generator遵循了RandowNumberGenerator协议,可以确保它有一个random()方法可供调用。
    func roll() -> Int {
        return Int(generator.random() * Double(sides)) + 1
    }
}

//调用:
var d6 = Dice(sides: 6, generator: LinearCongruentialGenerator())
for _ in 1...5 {
    print("Random dice roll is \(d6.roll())")
}

六、委托(代理)模式

委托是一种设计模式,它允许类或结构体将一些需要它们负责的功能委托给其他类型的实例。委托模式的实现很简单:定义协议来封装那些需要被委托的功能,这样就能确保遵循协议的类型能提供这些功能。委托模式可以用来响应特定的动作,或者接收外部数据源提供的数据,而无需关心外部数据源的类型。

一万个字不如一个代码例子:

//RandowNumberGenerator协议要求遵循协议的类型必须拥有一个名为random,返回值类型为Double的实例方法。尽管这里并未指明,但是我们假设返回值在[0.0,1.0)区间内
//RandowNumberGenerator协议并不关心每个随机数是怎样生产的,它只要求必须提供一个随机数生成器
protocol RandowNumberGenerator {
    func random() -> Double
}

//该类遵循并符合RandowNumberGenerator协议。该类实现了一个叫做 线性同余生成器 的伪随机数算法
class LinearCongruentialGenerator: RandowNumberGenerator {
    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
    }
}

//定义了一个Dice类,用来代表桌游中拥有N个面的骰子。Dice的实例含有sides和generator两个属性,前者是整型,用来表示骰子有几个面,后者为骰子提供一个随机数生成器,从而生成随机点数。
//generator属性的类型为RandowNumberGenerator,因此任何遵循了RandowNumberGenerator协议的类型的实例都可以赋值给generator,除此之外并无其他要求
class Dice {
    let sides: Int
    let generator: RandowNumberGenerator
    //Dice还有一个构造器,用来设置初始状态。构造器有一个为generator,类型为RandowNumberGenerator的形参。
    //在调用构造方法创建Dice的实例时,可以传入任何遵循RandowNumberGenerator协议的实例给generator。
    init(sides: Int, generator:RandowNumberGenerator) {
        self.sides = sides
        self.generator = generator
    }
    /*
     */
    
    //Dice类提供了一个名为roll的实例方法,用来模拟骰子的面值。它先调用generator的random()方法来生成一个[0.0,1.0)区间内的随机数,然后使用这个随机数生成正确的骰子面值。因此generator遵循了RandowNumberGenerator协议,可以确保它有一个random()方法可供调用。
    func roll() -> Int {
        return Int(generator.random() * Double(sides)) + 1
    }
}


//下面的例子定义了两个基于骰子游戏的协议:

//DiceGaem 协议可以被任意涉及骰子的游戏遵循。
protocol DiceGaem{
    var dice: Dice { get }
    func play()
}

//DiceGameDelegate 协议可以被任意类型遵循,用来追踪DiceGaem的游戏过程
//DicegameDelegate 协议提供了三个方法用来追踪游戏过程。这三个方法被放置于游戏的逻辑中,即 play() 方法内。分别在游戏开始时,新一轮开始时,以及游戏结束时被调用。
protocol DiceGameDelegate {
    func gameDidStart(_ game: DiceGaem)
    func game(_ game: DiceGaem, didStartNewTurnWithDiceRoll diceRoll:Int)
    func gameDidEnd(_ game: DiceGaem)
}

//SnakesAndLadders 是 控制流 章节引入的蛇梯棋游戏的新版本。新版本使用 Dice 实例作为骰子,并且实现了 DiceGame 和 DiceGameDelegate 协议,后者用来记录游戏的过程
//该类遵循了 DiceGame 协议,并且提供了相应的可读的 dice 属性和 play() 方法。
class SnakesAndLadders: DiceGaem {
    let finalSquare = 25
    //dice 属性在构造之后就不再改变,且协议只要求 dice 为可读的,因此将 dice 声明为常量属性。
    let dice = Dice(sides: 6, generator: LinearCongruentialGenerator())
    var square = 0
    var board: [Int]
    //初始化游戏
    init() {
        board = [Int](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
    }
    //delegate并不是游戏必备条件,因此delegate被定义为DiceGameDelegate类型的可选属性
    var delegate:DiceGameDelegate?
    
    //play()方法实现游戏逻辑,play()方法使用协议要求的dice属性提供骰子摇出的值
    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)
    }
}


//DiceGameTracker实现了DiceGameDelegate协议要求的三个方法,用来记录游戏已经进行的轮数
class DiceGameTracker: DiceGameDelegate {
    //当游戏开始时,numberOfTurns被赋值为0,然后在每新一轮中递增,游戏结束后,打印游戏的总轮数
    var numberOfTurns = 0
    
    //该方法从game参数获取游戏信息并且打印
    //game 参数是 DiceGame 类型而不是 SnakeAndLadders 类型,所以在gameDidStart(_:) 方法中只能访问 DiceGame 协议中的内容。
    //无论当前进行的是何种游戏,由于 game 符合 DiceGame 协议,可以确保 game 含有 dice 属性。因此在 gameDidStart(_:) 方法中可以通过传入的 game 参数来访问 dice 属性,进而打印出 dice 的 sides 属性的值。
    func gameDidStart(_ game: DiceGaem) {
        numberOfTurns = 0
        //当然了,SnakeAndLadders 的方法也可以在类型转换之后调用。在上例代码中,通过 is 操作符检查 game 是否为 SnakesAndLadders 类型的实例,如果是,则打印出相应的消息。
        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: DiceGaem, didStartNewTurnWithDiceRoll diceRoll: Int) {
        numberOfTurns += 1
        print("Rolled a \(diceRoll)")
    }
    
    func gameDidEnd(_ game: DiceGaem) {
        print("The game lasted for \(numberOfTurns) turns")
    }
}

//调用
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
 */

七、通过扩展添加协议一致性

即便无法修改源代码,依然可以通过扩展令已有类型遵循并符合协议。扩展可以为已有类型添加属性、方法、下标以及构造器,因此可以符合协议中的相应要求。

//任何想要通过文本表示一些内容的类型都可以实现该协议。这些想要表示的内容可以是实例本身的描述,也可以是实例当前状态的文本描述
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)

七、通过扩展添加协议一致性

当一个类型已经符合了某个协议中的所有要求,却还没有声明遵循该协议时,可以通过空扩展体的扩展来遵循该协议

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 数组,并打印每个元素的文本表示:

let things: [TextRepresentable] = [game,d12,simonTheHamster]
for thing in things {
    print(thing.textualDescription)
}

thing 是 TextRepresentable 类型而不是 Dice,DiceGame,Hamster 等类型,即使实例在幕后确实是这些类型中的一种。
由于 thing 是 TextRepresentable 类型,任何 TextRepresentable 的实例都有一个 textualDescription 属性,所以在每次循环中可以安全地访问 thing.textualDescription。

九、协议的继承

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

protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
    // 这里是协议的定义部分
}

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

//例子中定义了一个新的协议 PrettyTextRepresentable,它继承自 TextRepresentable 协议。
//任何遵循 PrettyTextRepresentable 协议的类型在满足该协议的要求时,也必须满足 TextRepresentable 协议的要求。
//在这个例子中,PrettyTextRepresentable 协议额外要求遵循协议的类型提供一个返回值为 String 类型的 prettyTextualDescription 属性。
protocol PrettyTextRepresentable: TextRepresentable{
    var prettyTextualDescription: String { get }
}

如下所示,扩展 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 += "o "
            }
        }
        return output
    }
}

上述扩展令 SnakesAndLadders 遵循了 PrettyTextRepresentable 协议,并提供了协议要求的 prettyTextualDescription 属性。
每个 PrettyTextRepresentable 类型同时也是 TextRepresentable 类型,所以在 prettyTextualDescription 的实现中,可以访问 textualDescription 属性。
然后,拼接上了冒号和换行符。接着,遍历数组中的元素,拼接一个几何图形来表示每个棋盘方格的内容:

  • 当从数组中取出的元素的值大于 0 时,用 ▲ 表示。
  • 当从数组中取出的元素的值小于 0 时,用 ▼ 表示。
  • 当从数组中取出的元素的值等于 0 时,用 ○ 表示。
//调用
print(game.prettyTextualDescription)//o o ▲ o o ▲ o o ▲ ▲ o o o ▼ o o o o ▼ o o ▼ o ▼ o

十、类类型专属协议

你可以在协议的继承列表中,通过添加 class 关键字来限制协议只能被类类型遵循,而结构体或枚举不能遵循该协议。class 关键字必须第一个出现在协议的继承列表中,在其他继承的协议之前:

protocol SomeClassOnlyProtocol: class, SomeInheritedProtocol {
    // 这里是类类型专属协议的定义部分
}

在以上例子中,协议 SomeClassOnlyProtocol 只能被类类型遵循。如果尝试让结构体或枚举类型遵循该协议,则会导致编译错误。

十一、协议合成

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

下面的例子中,将 Named 和 Aged 两个协议按照上述语法组合成一个协议,作为函数参数的类型:

//Named 协议包含 String 类型的 name 属性.
protocol Named{
    var name: String{ get }
}

//Aged 协议包含 Int 类型的 age 属性
protocol Aged {
    var age: Int { get }
}

//Person1 结构体遵循了Named,Aged这两个协议.
struct Person1: Named, Aged {
    var name: String
    var age: Int
}

//wishHappyBirthday(to:) 函数的参数 celebrator 的类型为 Named & Aged。这意味着它不关心参数的具体类型,只要参数符合这两个协议即可.
func wishHappyBirthday(to celebrator: Named & Aged) {
    print("Happy birthday \(celebrator.name) - you're \(celebrator.age)!")
}

//创建了一个名为 birthdayPerson 的 Person1 的实例,作为参数传递给了 wishHappyBirthday(to:) 函数。
//因为 Person1 同时符合这两个协议,所以这个参数合法,函数将打印生日问候语。

let birthdayPersong = Person1(name: "Yvan", age: 21)
wishHappyBirthday(to: birthdayPersong) //Happy birthday Yvan - you're 21!

十二、检查协议一致性

你可以使用类型转换中描述的 is 和 as 操作符来检查协议一致性,即是否符合某协议,并且可以转换到指定的协议类型。检查和转换到某个协议类型在语法上和类型的检查和转换完全相同:

  • is 用来检查实例是否符合某个协议,若符合则返回 true,否则返回 false。
  • as? 返回一个可选值,当实例符合某个协议时,返回类型为协议类型的可选值,否则返回 nil。
  • as! 将实例强制向下转换到某个协议类型,如果强转失败,会引发运行时错误。
//下面的例子定义了一个 HasArea 协议,该协议定义了一个 Double 类型的可读属性 area:
protocol HasArea{
    var area: Double { get }
    
}

//如下所示,Circle 类和 Country 类都遵循了 HasArea 协议:
//Circle 类把 area 属性实现为基于存储型属性 radius 的计算型属性。Country 类则把 area 属性实现为存储型属性。这两个类都正确地符合了 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
    }
}

//如下所示,Animal 是一个未遵循 HasArea 协议的类:
class Animal{
    var legs: Int
    init(legs: Int) {
        self.legs = legs
    }
}

Circle,Country,Animal 并没有一个共同的基类,尽管如此,它们都是类,它们的实例都可以作为 AnyObject 类型的值,存储在同一个数组中:

//objects 数组使用字面量初始化,数组包含一个 radius 为 2 的 Circle 的实例,一个保存了英国国土面积的 Country 实例和一个 legs 为 4 的 Animal 实例。
let objects: [AnyObject] = [Circle(radius: 2.0),Country(area: 243_610),Animal(legs: 4)]
for object in objects {
    if let objectWithArea = object as? HasArea {
        //当迭代出的元素符合 HasArea 协议时,将 as? 操作符返回的可选值通过可选绑定,绑定到 objectWithArea 常量上。
        //objectWithArea 是 HasArea 协议类型的实例,因此 area 属性可以被访问和打印。
        //objects 数组中的元素的类型并不会因为强转而丢失类型信息,它们仍然是 Circle,Country,Animal 类型。
        //然而,当它们被赋值给 objectWithArea 常量时,只被视为 HasArea 类型,因此只有 area 属性能够被访问。
        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

十三、可选的协议要求

协议可以定义可选要求,遵循协议的类型可以选择是否实现这些要求。在协议中使用 optional 关键字作为前缀来定义可选要求。可选要求用在你需要和 Objective-C 打交道的代码中。协议和可选要求都必须带上@objc属性。标记 @objc 特性的协议只能被继承自 Objective-C 类的类或者 @objc 类遵循,其他类以及结构体和枚举均不能遵循这种协议。

使用可选要求时(例如,可选的方法或者属性),它们的类型会自动变成可选的。比如,一个类型为 (Int) -> String 的方法会变成 ((Int) -> String)?。需要注意的是整个函数类型是可选的,而不是函数的返回值。

协议中的可选要求可通过可选链式调用来使用,因为遵循协议的类型可能没有实现这些可选要求。类似 someOptionalMethod?(someArgument) 这样,你可以在可选方法名称后加上 ? 来调用可选方法。

下面例子定义了一个名为 Counter 的用于整数计数的类,它使用外部的数据源来提供每次的增量。数据源由 CounterDataSource 协议定义,包含两个可选要求:

@objc protocol CounterDataSource {
    //严格来讲,CounterDataSource协议中的方法和属性都是可选的,因此遵循协议的类可以不实现这些要求,尽管技术上允许这样做,不过最好不要这样写
    //定义一个可选方法 incrementForCount(count:)
    @objc optional func incrementForCount(cout: Int) -> Int
    //定义一个可选属性
    @objc optional var fixedIncrement: Int { get }
}

class Counter{
    //Counter类使用变量属性count来存储当前值。
    var count = 0
    //Counter类含有CounterDataSource?类型的可选属性dataSource
    var dataSource: CounterDataSource?
    //每次调用该方法的时候,将会增加count的值
    func increment() {
        //increment 方法首先试图使用incrementForCount(count:)方法来得到每次的增量,该方法使用可选链式调用来尝试调用incrementForCount(count:),并将当前的count值作为参数传入。
        //这里使用了两层可选链式调用:
        //首先,由于 dataSource 可能为 nil,因此在 dataSource 后边加上了 ?,以此表明只在 dataSource 非空时才去调用 incrementForCount(count:) 方法。
        //其次,即使 dataSource 存在,也无法保证其是否实现了 incrementForCount(count:) 方法,因为这个方法是可选的。
        //因此,incrementForCount(count:) 方法同样使用可选链式调用进行调用,只有在该方法被实现的情况下才能调用它,所以在 incrementForCount(count:) 方法后边也加上了 ?。
        if let amount = dataSource?.incrementForCount?(cout: count) {
            //在调用 incrementForCount(count:) 方法后,Int? 型的返回值通过可选绑定解包并赋值给常量 amount。
            //如果可选值确实包含一个数值,也就是说,数据源和方法都存在,数据源方法返回了一个有效值。
            //之后便将解包后的 amount 加到 count 上,增量操作完成。
            count += amount
        }else if let amount = dataSource?.fixedIncrement {
            //如果没有从 incrementForCount(count:) 方法获取到值,可能由于 dataSource 为 nil,或者它并没有实现 incrementForCount(count:) 方法,那么 increment() 方法将试图从数据源的 fixedIncrement 属性中获取增量。
            count += amount
        }
    }
}

//下面的例子展示了 CounterDataSource 的简单实现。
//ThreeSource 类遵循了 CounterDataSource 协议,它实现了可选属性 fixedIncrement,每次会返回 3
class ThreeSource: NSObject, CounterDataSource {
    let  fixedIncrement = 3
}

//调用
//可以使用 ThreeSource 的实例作为 Counter 实例的数据源
        let counter = Counter()//创建Counter实例
        counter.dataSource = ThreeSource() //设置Counnter实例的数据源为ThreeSource()
        for _ in 1...4 {
            counter.increment()
            print(counter.count)
        }
//输出:
3
6
9
12

下面是一个更为复杂的数据源 TowardsZeroSource,它将使得最后的值变为 0:

@objc class TowardsZeroSource: NSObject, CounterDataSource{
    //TowardsZeroSource 实现了 CounterDataSource 协议中的 increment(forCount:) 方法,以 count 参数为依据,计算出每次的增量。
    //如果 count 已经为 0,此方法返回 0,以此表明之后不应再有增量操作发生。
    func incrementForCount(cout count: Int) -> Int {
        if count == 0 {
            return 0
        }else if count < 0 {
            return 1
        }else {
            return -1
        }
    }
}

//使用 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() 方法:

//扩展RandomNumberGenerator
extension RandomNumberGenerator{
    //该方法使用协议中定义的 random() 方法来返回一个随机的 Bool 值
    func randomBool() -> Bool {
        return random() > 0.5
    }
}

//通过协议扩展,所有遵循协议的类型,都能自动获得这个扩展所增加的方法实现,无需任何额外修改
let generator1 = LinearCongruentialGenerator()
print("Here's a random number:\(generator1.random())")
print("And here's a random Boolean:\(generator1.random())")

十五、提供默认实现

可以通过协议扩展来为协议要求的属性、方法以及下标提供默认的实现。如果遵循协议的类型为这些要求提供了自己的实现,那么这些自定义实现将会替代扩展中的默认实现被使用。

//PrettyTextRepresentable 协议继承自 TextRepresentable 协议,可以为其提供一个默认的 prettyTextualDescription 属性,只是简单地返回 textualDescription 属性的值
extension PrettyTextRepresentable{
    var prettyTextualDescription: String {
        return textualDescription
    }
}

十六、为协议扩展添加限制条件

在扩展协议的时候,可以指定一些限制条件,只有遵循协议的类型满足这些限制条件时,才能获得协议扩展提供的默认实现。这些限制条件写在协议名之后,使用 where
子句来描述,正如Where子句中所描述的。

//你可以扩展 CollectionType 协议,但是只适用于集合中的元素遵循了 TextRepresentable 协议的情况:
extension Collection where Iterator.Element: TextRepresentable{
    
    //textualDescription 属性返回整个集合的文本描述,它将集合中的每个元素的文本描述以逗号分隔的方式连接起来,包在一对方括号中。
    var textualDescription: String{
        let itemsAsText = self.map { $0.textualDescription }
        return "[" + itemsAsText.joined(separator: ",") + "]"
    }    
}

现在我们来看看先前的 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]

相关文章

网友评论

      本文标题:Swift - 18.协议

      本文链接:https://www.haomeiwen.com/subject/fqffottx.html