Swift编程二十二(协议)

案例代码下载

协议

协议定义的适应特定任务或功能块的方法,属性和其他需求的方案。然后,可以通过类,结构或枚举来遵守该协议,以提供这些要求的实际实现。任何满足协议要求的类型都被认为遵守该协议。

除了指定遵守的类型必须实现的要求之外,还可以扩展协议以实现符遵守的类型可以使用的一些需求或其他功能。

协议语法

可以使用与类,结构和枚举非常类似的方式定义协议:

protocol SomeProtocol {
    // 协议在这里定义
}

声明遵守特定协议的自定义类型,作为其定义的一部分,将协议名称放在类型名称后面并用冒号隔开。可以列出多个协议,并以逗号分隔:

struct SomeStructure: FirstProtocol, AnotherProtocol {
    // 结构体在这里定义
}

如果一个类有超类,则在遵守任何协议之前列出超类名,后跟一个逗号:

class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
    // 类在这里定义
}

协议要求

协议可以要求遵守该协议的类型提供具有特定名称和类型的实例属性或类型属性。协议未指定属性是存储属性还是计算属性 - 它仅指定所需的属性名称和类型。协议还指定每个属性是否必须是可获取的,可获取的和可设置的。

如果协议要求属性可获取和可设置,则不能通过常量存储属性或只读计算属性来满足该属性要求。如果协议只需要一个可获取属性,那么任何类型的属性都可以满足要求,如果对你自己的代码有用可设置的属性也是有效的。

属性要求始终声明为变量属性,前缀为var关键字。可获取和可设置属性通过在类型声明后写入{ get set }来指示,并且可通过写入{ get }来指示可获取属性。

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

在协议中定义类型属性始终要求使用关键字static添加前缀。即使在类实现时类型属性要求可以使用class或static关键字作为前缀,此规则也适用:

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

以下是具有单实例属性要求的协议示例:

protocol FullyNamed {
    var fullName: String { get }
}

FullyNamed协议要求遵守的类型以提供完全限定的名称。该协议没有指定有关遵守的类型性质的任何其他内容 - 它只指定该类型必须能够为自己提供全名。该协议声明任何遵守FullyNamed协议的类型必须具有一个名为fullName的可获取实例属性,该属性属于String类型。

这是一个遵守FullyNamed协议的简单结构示例:

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

此示例定义一个名为Person的结构,该结构表示特定的命名人员。声明它遵守ullyNamed协议作为其定义第一行的一部分。

每个实例Person都有一个名为fullName的存储属性,属性类型String。这符合FullyNamed协议的单一要求,并且意味着Person已正确符合协议。(如果未满足协议要求,Swift会在编译时报告错误。)

这是一个更复杂的类,它也遵守FullyNamed协议:

class Starship: FullyNamed {
    var prefix: String?
    var name: String
    init(name: String, prefix: String? = nil) {
        self.name = name
        self.prefix = prefix
    }
    var fullName: String {
        return (prefix != nil ? prefix! + " " : "") + name
    }
}
var ncc1701 = Starship(name: "Enterprise", prefix: "USS")
print(ncc1701.fullName)

此类将协议要求的fullName属性实现为飞船的计算只读属性。每个Starship类实例都存储一个强制项name和一个可选项prefix。fullName属性使用prefix值(如果存在),并将其添加到name开头,以便为星舰创建全名。

方法要求

协议可能需要通过遵守的类型来实现特定的实例方法和类型方法。这些方法作为协议定义的一部分编写,与普通实例和类型方法完全相同,但没有花括号或方法体。允许使用变量参数,遵循与常规方法相同的规则。但是,无法为协议定义中的方法参数指定默认值。

与类型属性要求一样,当在协议中定义类型方法时始终要求使用关键字static添加前缀。即使在类实现时类型方法要求以classor static关键字为前缀,也是如此:

protocol SomeProtocol {
    static func someTypeMethod()
}

以下示例使用单个实例方法的需求定义协议:

protocol RandomNumberGenerator {
    func random() -> Double
}

RandomNumberGenerator协议要求任何遵守的类型都有一个名为random的实例方法,只要调用它就会返回一个Double值。可以假设该值是0.0到1.0(但不包括)的数字,即使这没有被指定为协议的一部分。

RandomNumberGenerator协议不对如何生成每个随机数做出任何假设 - 它只需要生成器提供生成新随机数的标准方法。

这是一个遵守RandomNumberGenerator协议的类的实现。此类实现的伪随机数生成器算法称为线性同余生成器:

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())")
print("And another one: \(generator.random())")

Mutating方法要求

有时需要一种方法来修改(或改变)它所属的实例。例如,关于值类型(即结构和枚举)将关键字mutating放在方法的func关键字之前,以指示允许该方法修改它所属的实例以及该实例的任何属性。在实例方法中修改值类型中描述了此过程。

如果定义了一个协议实例方法要求改变遵守该协议的任何类型的实例,请使用mutating关键字作为协议定义的一部分来标记该方法。这使得结构和枚举能够采用协议并满足该方法要求。

注意: 如果将协议实例方法要求标记为mutating,则mutating在为类编写该方法的实现时,不需要编写关键字。该mutating关键字仅由结构和枚举使用。

下面的示例定义了一个名为Togglable的协议,它定义了一个名为toggle的实例方法。顾名思义,toggle()方法旨在通过修改该类型的属性来切换或反转任何遵守协议类型的状态。

toggle()方法使用mutating关键字作为Togglable协议定义的一部分进行标记,以指示该方法在调用时会改变实例的状态:

protocol Togglable {
    mutating func toggle()
}

如果Togglable协议被结构或枚举实现,则该结构或枚举可以通过提供标记mutating的toggle()方法来实现遵守的协议。

下面的示例定义了一个名为OnOffSwitch的枚举。这个枚举在on和off两个状态之间切换。枚举的toggle实现标记为mutating,以匹配Togglable协议的要求:

enum OnOffSwitch: Togglable {
    case on, off
    mutating func toggle() {
        switch self {
        case .on:
            self = .off
        default:
            self = .on
        }
    }
}
var lightSwitch = OnOffSwitch.off
lightSwitch.toggle()
print(lightSwitch)

初始化程序要求

协议可能需要通过遵守的类型来实现特定的初始化程序。可以将这些初始化程序作为协议定义的一部分编写,其方式与普通初始化程序完全相同,但不使用花括号或初始化程序主体:

protocol SomeProtocol {
    init(someParameter: Int)
}

协议初始化程序要求的类实现

可以将遵守协议的类初始化程序要求实现为指定初始化程序或便捷初始化程序。在这两种情况下,都必须使用required修饰符标记初始化程序实现:

class SomeClass: SomeProtocol {
    required init(someParameter: Int) {
        //在这里实现初始化
    }
}

required修饰符的使用可确保在遵守的类的所有子类上提供显式或继承的实现初始化程序的需求,以便能够遵守协议。

有关必须的初始化的更多信息,请参阅必需的初始化器。

注意: 不能在使用final修饰符标记的类上使用required修饰符标记的协议初始化程序实现,因为最终类不能进行子类化。有关final修饰符的更多信息,请参阅防止覆盖。

如果子类重写超类中的指定初始化程序,并且还实现遵守协议的初始化程序,使用required和override修饰符标记初始化程序实现:

protocol SomeProtocol {
    init()
}

class SomeSuperClass {
    init() {
        //在这里实现初始化
    }
}

class SomeSubClass: SomeSuperClass, SomeProtocol {
    // "required"来自遵守SomeProtocol协议; "override"来自继承SomeSuperClass父类
    required override init() {
        //在这里实现初始化
    }
}

Failable初始化程序要求

协议可以定义遵守类型的Failable初始化程序,如Failable Initializers中所定义。

遵守的类型中failable或nonfailable的初始化程序可以满足failable的初始化程序要求。nonfailable初始化程序或隐式解包的failable初始化程序可以满足nonfailable初始化程序要求。

作为类型的协议

协议本身并不实现任何功能。尽管如此,创建的任何协议都将成为完全成熟的类型。

因为它是一种类型,所以可以在其他类型允许的许多地方使用协议,包括:

  • 作为函数,方法或初始化程序中的参数类型或返回类型
  • 作为常量,变量或属性的类型
  • 作为数组,字典或其他容器中的项类型

注意: 由于协议是类型,他们的名称以大写字母(如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的新类,它表示用于棋盘游戏的n面骰子。Dice实例有一个名为sides的整数属性,它表示它们有多少面,以及一个名为generator的属性,它提供了一个随机数生成器,用于创建骰子滚动值。

generator属性是RandomNumberGenerator类型。因此,可以将其设置为遵守RandomNumberGenerator协议的任何类型的实例。除了实例必须遵守RandomNumberGenerator协议之外,分配给此属性的实例不需要任何其他内容。

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())")
}

代理

代理是一种设计模式,它使类或结构能够将其部分职责交给另一种类型(或代理)的实例。通过定义封装代理职责的协议来实现此设计模式,从而保证遵守协议的类型(称为代理)提供已委派的功能。代理可用于响应特定操作,或从外部源检索数据,而无需知道该源的基础类型。

以下示例定义了两种用于基于骰子的棋盘游戏的协议:

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类的代理必须使用弱引用,所以将协议标记为class-only协议。class-only协议由继承AnyObject标记,如“class-only协议”中所述。

这是最初在控制流中引入的蛇梯棋游戏的一个版本。该版本适用于掷骰子实例; 遵守DiceDice协议; 并通知GameDiceGameDelegate其进展情况:

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

有关蛇梯棋游戏的描述,请参阅Break。

这个版本的游戏被包装成一个名为SnakesAndLadders的类,它遵守DiceGame协议。它提供可获取的dice属性和play()方法以符合协议。(dice属性被声明为常量属性,因为它在初始化后不需要更改,并且协议只要求它必须是可获取的。)

蛇梯棋游戏板的设置在类的init()初始化内进行。所有游戏逻辑都被移入协议的play方法,该方法使用协议的必需dice属性来提供其骰子滚动值。

请注意,delegate属性被定义为DiceGameDelegate可选值,因为玩游戏代理不是必须的。因为它是可选类型,所以该delegate属性会自动设置为初始值nil。此后,游戏实例化器可以选择将属性设置为合适的代理。由于DiceGameDelegate协议仅支持类,因此可以声明代理为weak以防止引用循环。

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 += 1
        print("Rolled a \(diceRoll)")
    }
    func gameDidEnd(_ game: DiceGame) {
        print("The game lasted for \(numberOfTurns) turns")
    }
}

DiceGameTracker实现DiceGameDelegate协议所需的所有三种方法。它使用这些方法来跟踪游戏的掷骰次数。numberOfTurns在游戏开始时将属性重置为零,每次新掷骰时将其增加,并在游戏结束后打印出总掷骰数。

上面所示的gameDidStart(:)实现使用该game参数来打印关于即将开始的游戏的一些介绍性信息。该game参数的类型为DiceGame,而不是SnakesAndLadders,因此gameDidStart(:)只能访问和使用作为DiceGame协议一部分实现的方法和属性。但是,该方法仍然可以使用类型转换来查询基础实例的类型。在此示例中,它检查是否game实际上是SnakesAndLadders实例,如果是,则打印相应的消息。

gameDidStart(:)方法还访问传递的game参数的dice属性。因为game已知它符合DiceGame协议,所以它保证具有dice属性,因此该gameDidStart(:)方法能够访问和打印骰子的sides属性,无论正在进行的什么类型的游戏。

这是DiceGameTracker看起来如何进行:

let tracker = DiceGameTracker()
let game = SnakesAndLadders()
game.delegate = tracker
game.play()

扩展中添加协议

即使无权访问现有类型的源代码,也可以扩展现有类型以采用并符合新协议。扩展可以向现有类型添加新属性,方法和下标,因此可以添加协议可能要求的任何要求。有关扩展的更多信息,请参阅扩展。

注意: 当在扩展中的添加协议时,类型的现有实例会自动遵守协议。

例如,这个被称为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)

同样,SnakesAndLadders游戏类可以扩展为遵守TextRepresentable协议:

extension SnakesAndLadders: TextRepresentable {
    var textualDescription: String {
        return "A game of Snakes and Ladders with \(finalSquare) squares"
    }
}
print(game.textualDescription)

有条件地遵守协议

泛型类型可能仅在某些条件下满足协议的要求,例如当泛型参数遵守协议时。在扩展类型时列出约束可以使泛型类型有条件地符合协议。通过编写泛型where子句,在遵守的协议名称之后写下这些约束。有关泛型where子句的更多信息,请参阅泛型Where子句。

以下扩展使Array实例TextRepresentable在存储符合的类型的元素时符合协议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)

通过扩展声明遵守协议

如果某个类型已经符合协议的所有要求,但尚未声明它遵守该协议,则可用遵守协议的空扩展:

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)

注意: 类型不会仅通过满足其要求自动遵守协议。必须始终明确声明他们遵守协议。

协议类型的集合

协议可以用作要存储在诸如数组或字典之类的集合中的类型,如作为类型的协议中所述。这个例子创建了一个TextRepresentable数组:

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

在可以迭代数组中的项,并打印每个项目的文本描述:

for thing in things {
    print(thing.textualDescription)
}

请注意,thing常量是TextRepresentable类型。它不是Dice类型或者DiceGame、Hamster即使幕后的实际实例属于这些类型之一。尽管如此,因为它是TextRepresentable类型,并且任何TextRepresentable已知具有textualDescription属性,所以thing.textualDescription每次通过循环访问是安全的。

协议继承

协议可以继承一个或多个其他协议,并可以在其继承的需求之上添加进一步的要求。协议继承的语法类似于类继承的语法,但是可以选择列出多个继承的协议,用逗号分隔:

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

以下是继承上述TextRepresentable协议的协议示例:

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

此示例定义了一个继承自TextRepresentable的新协议PrettyTextRepresentable。遵守PrettyTextRepresentable的任何对象必须满足TextRepresentable的所有要求,以及PrettyTextRepresentable的要求。在这个例子中,PrettyTextRepresentable将提供一个称为prettyTextualDescription的gettable属性返回一个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属性开始实现输出字符串。并附加上冒号和换行符作为prettyTextualDescription的开头。然后通过迭代棋盘方块的数组,并附加几何形状来表示每个方格的内容:

  • 如果方格的值大于0,则它是梯子的底部,由▲表示。
  • 如果方格的值小于0,则它是蛇的头部,由▼表示。
  • 否则方格的值是0,它是一个“自由”方格,由○表示。

prettyTextualDescription属性现在可用于打印任何SnakesAndLadders实例的文本描述:

print(game.prettyTextualDescription)

Class-Only协议

可以通过将AnyObject协议添加到协议的继承列表来将协议采用限制为类类型(而不是结构或枚举)。

protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {
    //在这里定义class-only协议
}

在上面的示例中,SomeClassOnlyProtocol只能由类类型遵守。编写试图遵守SomeClassOnlyProtocol的结构或枚举定义编译时报错误。

注意: 当该协议的要求定义的行为假定或要求符合类型具有引用语义而不是值语义时,请使用Class-Only协议。有关引用和值语义的更多信息,请参阅结构和枚举是值类型和类是引用类型。

协议组合

要求类型同时遵守多个协议可能很有用。可以使用协议组合将多个协议组合到单个需求中。协议组合的行为就像定义了一个临时本地协议,该协议能够组合所有协议。协议组合不定义任何新的协议类型。

协议组合具有SomeProtocol & AnotherProtocol这种形式。可以根据需要列出任意数量的协议,并使用符号(&)分隔它们。除了协议列表之外,协议组合还可以包含一个类类型,可以使用它来指定所需的超类。

这是一个将两个叫做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)

在此示例中,Named协议要求有一个名为mame类型为String的gettable属性。Aged协议要求有一个名为age类型为Int的gettable属性。两种协议都被Person结构遵守。

示例还定义了一个wishHappyBirthday(to:)函数。celebrator参数的类型是Named & Aged,“任何遵守Named和Aged协议的类型。”只要符合两个必需的协议,哪个特定类型传递给函数都无关紧要。

然后,示例创建一个名为birthdayPerson的Person新实例,并将此新实例传递给该wishHappyBirthday(to:)函数。因为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)

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

传递birthdayPerson给beginConcert(in:)函数是无效的,因为Person它不是Location子类。同样,如果您创建了一个Location不符合Named协议的子类,则beginConcert(in:)使用该类型的实例调用也是无效的。

检查协议是否一致

可以使用类型转换中描述的is和as运算符来检查协议是否一致,以及转换为特定协议。检查并转换为协议遵循与检查和转换为类型完全相同的语法:

  • is运算符如果一个实例遵守协议返回true,如果它不遵守返回false,。
  • as?运算符的版本返回协议类型的可选值,如果实例不符合该协议则此值为nil。
  • as!如果向下转换不成功,则向下转换运算符的版本强制向下转换为协议类型并触发运行时错误。

此示例定义了一个名为HasArea的协议,其中要求包含单个Double类型的gettable属性的属性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)
]

使用半径为2.0的Circle实例、以英里平方公里的面积初始化的Country实例还有四条腿的Animal实例的数组文字初始化objects数组;。

现在可以迭代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")
    }
}

只要数组中的对象符合HasArea协议,as?操作符返回的可选值就会被解包,并且可选绑定到一个叫做objectWithArea的常量中。objectWithArea常量已知是HasArea类型,所以能够类型安全的访问并打印area属性。

请注意,构建过程不会更改基础对象。他们仍然是Circle,Country和Animal。然而,在它们存储在objectWithArea常量中时,它们只是已知的HasArea类型,因此只能访问它们的area属性。

可选要求的协议

可以定义可选要求的协议,遵守协议的类型不一定要实现这些要求。可选要求以optional修饰符为前缀,作为协议定义的一部分。可以编写可选要求与Objective-C互操作的代码。必须使用@objc属性标记协议和可选要求。请注意,@objc协议只能由继承自Objective-C类或其他@objc类的类遵守。结构或枚举不能遵守它们。

在可选要求中使用方法或属性时,其类型将自动变为可选。例如,方法的类型(Int) -> String变为((Int) -> String)?。请注意,整个函数类型包含在可选项中,而不是方法的返回值。

可以使用可选链接调用可选要求协议,以考虑遵守协议的类型未实现要求的可能性。通过在调用方法名称后面写一个问号来检查可选方法的实现,例如someOptionalMethod?(someArgument)。有关可选链接的信息,请参阅可选链接。

以下示例定义了一个叫做Counter的整数计数类,它使用外部数据源来提供增量。此数据源由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()方法首先尝试通过increment(forCount:)在其数据源上查找该方法的实现来检索增量。increment()方法使用可选链接来尝试调用increment(forCount:),每次调用这个方法来增加count属性。

请注意,此处有两个级别的可选链接。首先,dataSource可能是nil,所以dataSource在其名称后面有一个问号,表示只有在dataSource不是nil时才应该调用increment(forCount:)。其次,即使dataSource不为nil,也不能保证它实现了increment(forCount:),因为它是一个可选要求。在这里,increment(forCount:)可能未实现的可能性也由可选链接处理。调用increment(forCount:)仅在increment(forCount:)存在时发生-即不为nil。这就是为什么increment(forCount:)在其名称后面还带有问号。

由于这两个原因之一increment(forCount:)的调用可能会失败,因此调用返回一个可选 Int值。即使CounterDataSource在increment(forCount:)定义中返回非可选Int值也是如此。即使有两个可选的链接操作,一个接一个,结果仍然是一个可选值。有关使用多个可选链接操作的详细信息,请参阅链接多个可选链。

在调用之后increment(forCount:)返回的Int可选项使用可选绑定被解包为名为的amount常量。如果可选Int包含一个值 - 即,如果代理和方法都存在,并且该方法返回一个值 - 则将存储属性count解包添加到amount中并完成增量。

如果无法从increment(forCount:)方法中检索值- 因为dataSource是nil,或者因为数据源没有实现increment(forCount:)- 那么该increment()方法会尝试从数据源的fixedIncrement属性中检索值。该fixedIncrement属性也是一个可选要求,因此它的值是一个可选Int值,即使作为CounterDataSource协议定义的一部分fixedIncrement被定义为非可选Int属性。

这是一个简单的CounterDataSource实现,其中数据源每次查询时返回常量值3。它通过实现可选fixedIncrement属性要求来实现:

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

可以使用ThreeSource实例作为新Counter实例的数据源:

var counter = Counter()
counter.dataSource = ThreeSource()
for _ in 1...4 {
    counter.increment()
    print(counter.count)
}

上面的代码创建了一个新Counter实例; 将其数据源设置为新ThreeSource实例; 并且四次调用计数器的方法increment()。正如预期的那样,每次调用increment()计数器方法count属性增加3 。

这是一个更复杂的数据源TowardsZeroSource,它使Counter实例从其当前count值向上或向下计数到零:

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表示没有进一步的计数应该发生。

现有可以使用TowardsZeroSourceCounter实例的实例从-4开始计数。一旦计数器达到零,就不再进行计数:

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

协议扩展

可以扩展协议以向遵守的类型提供方法,初始化器,下标和计算属性实现。这允许定义协议本身的行为,而不是每种类型的单独一致性或全局函数。

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

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

通过在协议上创建扩展,所有符合类型的类型自动获得此方法实现,而无需任何其他修改。

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

协议扩展可以为遵守的类型添加实现,但不能使协议扩展继承自其他协议。协议继承总是在协议声明本身中指定。

提供默认实现

可以使用协议扩展为协议要求的任何方法或计算属性提供默认实现。如果遵守的类型提供其自己的必需方法或属性的实现,则将使用该实现而不是扩展提供的实现。

注意: 扩展提供的协议要求的默认实现与可选协议不同。虽然遵守的类型可以不提供它们自己的实现,但是可以在没有可选链接的情况下调用具有默认实现的需求。

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

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

添加约束到协议扩展

定义协议扩展时,可以在扩展的方法和属性可用之前指定符合类型必须满足的约束。可以通过在扩展的协议名称之后编写generic where子句来编写这些约束。有关generic where子句的更多信息,请参阅Generic Where子句。

例如,可以定义Collection协议的扩展,该扩展适用于其元素遵守Equatable协议的任何集合。通过将集合的元素约束到Equatable协议(标准库的一部分),可以使用==和!=运算符来检查两个元素之间的相等性和不相等。

extension Collection where Element: Equatable {
    func allEqua() -> 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并且Int遵守Equatable,equalNumbers和differentNumbers可以使用allEqual()方法:

print(equalNumbers.allEqual())
print(differentNumbers.allEqual())

注意: 如果遵守的类型满足多个约束扩展要求对同一方法或属性提供实现,则Swift使用与最专用约束相对应的实现。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容