协议扩展 2个月前

编程语言
434
协议扩展

在扩展里添加协议遵循

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

注意

通过扩展令已有类型遵循并符合协议时,该类型的所有实例也会随之获得协议中定义的各项功能。

例如下面这个 TextRepresentable 协议,任何想要通过文本表示一些内容的类型都可以实现该协议。这些想要表示的内容可以是实例本身的描述,也可以是实例当前状态的文本描述:

protocolTextRepresentable {
var textualDescription: String { get }
}

可以通过扩展,令先前提到的 Dice 类可以扩展来采纳和遵循 TextRepresentable 协议:

extensionDice: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 协议:

extensionSnakesAndLadders: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”

有条件地遵循协议

泛型类型可能只在某些情况下满足一个协议的要求,比如当类型的泛型形式参数遵循对应协议时。你可以通过在扩展类型时列出限制让泛型类型有条件地遵循某协议。在你采纳协议的名字后面写泛型 where 分句。更多关于泛型 where 分句,见 泛型 Where 分句

下面的扩展让 Array 类型只要在存储遵循 TextRepresentable 协议的元素时就遵循 TextRepresentable 协议。

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

在扩展里声明采纳协议

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

structHamster {
    var name: String
       var textualDescription: String {
        return"A hamster named \(name)"
    }
}
extensionHamster:TextRepresentable {}

从现在起,Hamster 的实例可以作为 TextRepresentable 类型使用:

let simonTheHamster =Hamster(name:"Simon")
let somethingTextRepresentable: TextRepresentable = simonTheHamster
print(somethingTextRepresentable.textualDescription)
// 打印 “A hamster named Simon”

注意

即使满足了协议的所有要求,类型也不会自动遵循协议,必须显式地遵循协议。

使用合成实现来采纳协议

Swift 可以自动提供一些简单场景下遵循 EquatableHashableComparable 协议的实现。在使用这些合成实现之后,无需再编写重复的代码来实现这些协议所要求的方法。

Swift 为以下几种自定义类型提供了 Equatable 协议的合成实现:

  • 遵循 Equatable 协议且只有存储属性的结构体。
  • 遵循 Equatable 协议且只有关联类型的枚举
  • 没有任何关联类型的枚举

在包含类型原始声明的文件中声明对 Equatable 协议的遵循,可以得到 == 操作符的合成实现,且无需自己编写任何关于 == 的实现代码。Equatable 协议同时包含 != 操作符的默认实现。

下面的例子中定义了一个 Vector3D 结构体来表示一个类似 Vector2D 的三维向量 (x, y, z)。由于 xyz 都是满足 Equatable 的类型,Vector3D 可以得到连等判断的合成实现。

structVector3D:Equatable{
var x =0.0, y =0.0, z =0.0
}

let twoThreeFour =Vector3D(x:2.0, y:3.0, z:4.0)
let anotherTwoThreeFour =Vector3D(x:2.0, y:3.0, z:4.0)
if twoThreeFour == anotherTwoThreeFour {
print("These two vectors are also equivalent.")
}
// 打印 "These two vectors are also equivalent."

Swift 为以下几种自定义类型提供了 Hashable 协议的合成实现:

  • 遵循 Hashable 协议且只有存储属性的结构体。
  • 遵循 Hashable 协议且只有关联类型的枚举
  • 没有任何关联类型的枚举

在包含类型原始声明的文件中声明对 Hashable 协议的遵循,可以得到 hash(into:) 的合成实现,且无需自己编写任何关于 hash(into:) 的实现代码。

Swift 为没有原始值的枚举类型提供了 Comparable 协议的合成实现。如果枚举类型包含关联类型,那这些关联类型也必须同时遵循 Comparable 协议。在包含原始枚举类型声明的文件中声明其对 Comparable 协议的遵循,可以得到 < 操作符的合成实现,且无需自己编写任何关于 < 的实现代码。Comparable 协议同时包含 <=>>= 操作符的默认实现。

下面的例子中定义了 SkillLevel 枚举类型,其中定义了初学者(beginner)、中级(intermediate)和专家(expert)三种等级,专家等级会由额外的星级(stars)来进行排名。

enumSkillLevel:Comparable{
case beginner
case intermediate
case expert(stars:Int)
}
var levels = [SkillLevel.intermediate, SkillLevel.beginner,
              SkillLevel.expert(stars:5), SkillLevel.expert(stars:3)]
for level in levels.sorted() {
print(level)
}
// 打印 "beginner"
// 打印 "intermediate"
// 打印 "expert(stars: 3)"
// 打印 "expert(stars: 5)"

协议类型的集合

协议类型可以在数组或者字典这样的集合中使用,在 协议类型 提到了这样的用法。下面的例子创建了一个元素类型为 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 类型而不是 DiceDiceGameHamster 等类型,即使实例在幕后确实是这些类型中的一种。由于 thingTextRepresentable 类型,任何 TextRepresentable 的实例都有一个 textualDescription 属性,所以在每次循环中可以安全地访问 thing.textualDescription

协议的继承

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

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

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

protocolPrettyTextRepresentable:TextRepresentable {
var prettyTextualDescription: String { get }
}

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

如下所示,扩展 SnakesAndLadders,使其遵循并符合 PrettyTextRepresentable 协议:

extensionSnakesAndLadders:PrettyTextRepresentable {
var prettyTextualDescription: String {
var output = textualDescription +":\n"
for index in1...finalSquare {
switch board[index] {
caselet ladder where ladder >0:
                output +="▲ "
caselet 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:
// ○ ○ ▲ ○ ○ ▲ ○ ○ ▲ ▲ ○ ○ ○ ▼ ○ ○ ○ ○ ▼ ○ ○ ▼ ○ ▼ ○

类专属的协议

你通过添加 AnyObject 关键字到协议的继承列表,就可以限制协议只能被类类型遵循(而不能是结构体类型或者枚举类型)。

protocolSomeClassOnlyProtocol:AnyObject, SomeInheritedProtocol {
// 这里是类专属协议的定义部分
}

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

注意

当协议定义的要求需要遵循协议的类型必须是引用语义而非值语义时,应该采用类类型专属协议。关于引用语义和值语义的更多内容,请查看 结构体和枚举是值类型类是引用类型

协议合成

要求一个类型同时遵循多个协议是很有用的。你可以使用协议组合来复合多个协议到一个要求里。协议组合行为就和你定义的临时局部协议一样拥有构成中所有协议的需求。协议组合不定义任何新的协议类型。

协议组合使用 SomeProtocol & AnotherProtocol 的形式。你可以列举任意数量的协议,用和符号(&)分开。除了协议列表,协议组合也能包含类类型,这允许你标明一个需要的父类。

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

protocolNamed {
var name: String { get }
}
protocolAged {
var age: Int { get }
}
structPerson:Named, Aged {
var name: String
var age: Int
}
funcwishHappyBirthday(tocelebrator: Named & Aged) {
print("Happy birthday, \(celebrator.name), you're \(celebrator.age)!")
}
let birthdayPerson =Person(name:"Malcolm", age:21)
wishHappyBirthday(to: birthdayPerson)
// 打印 “Happy birthday Malcolm - you're 21!”

Named 协议包含 String 类型的 name 属性。Aged 协议包含 Int 类型的 age 属性。Person 结构体采纳了这两个协议。

wishHappyBirthday(to:) 函数的参数 celebrator 的类型为 Named & Aged, 这意味着“任何同时遵循 Named 和 Aged 协议的类型”。它不关心参数的具体类型,只要参数遵循这两个协议即可。

上面的例子创建了一个名为 birthdayPersonPerson 的实例,作为参数传递给了 wishHappyBirthday(to:) 函数。因为 Person 同时遵循这两个协议,所以这个参数合法,函数将打印生日问候语。

这里有一个例子:将 Location 类和前面的 Named 协议进行组合:

classLocation {
var latitude: Double
var longitude: Double
init(latitude: Double, longitude: Double) {
        self.latitude = latitude
        self.longitude = longitude
    }
}
classCity:Location, Named {
var name: String
init(name: String, latitude: Double, longitude: Double) {
        self.name = name
        super.init(latitude: latitude, longitude: longitude)
    }
}
funcbeginConcert(inlocation: Location & Named) {
print("Hello, \(location.name)!")
}

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

beginConcert(in:) 函数接受一个类型为 Location & Named 的参数,这意味着“任何 Location 的子类,并且遵循 Named 协议”。在这个例子中,City 就满足这样的条件。

将 birthdayPerson 传入 beginConcert(in:) 函数是不合法的,因为 Person 不是 Location 的子类。同理,如果你新建一个类继承于 Location,但是没有遵循 Named 协议,而用这个类的实例去调用 beginConcert(in:) 函数也是非法的。

检查协议一致性

你可以使用 类型转换 中描述的 isas 操作符来检查协议一致性,即是否遵循某协议,并且可以转换到指定的协议类型。检查和转换协议的语法与检查和转换类型是完全一样的:

  • is 用来检查实例是否遵循某个协议,若遵循则返回 true,否则返回 false
  • as? 返回一个可选值,当实例遵循某个协议时,返回类型为协议类型的可选值,否则返回 nil
  • as! 将实例强制向下转换到某个协议类型,如果强转失败,将触发运行时错误。

下面的例子定义了一个 HasArea 协议,该协议定义了一个 Double 类型的可读属性 area

protocolHasArea {
var area: Double { get }
}

如下所示,Circle 类和 Country 类都遵循了 HasArea 协议:

classCircle:HasArea {
let pi =3.1415927
var radius: Double
var area: Double { return pi * radius * radius }
init(radius: Double) { self.radius = radius }
}
classCountry:HasArea {
var area: Double
init(area: Double) { self.area = area }
}

Circle 类把 area 属性实现为基于存储型属性 radius 的计算型属性。Country 类则把 area 属性实现为存储型属性。这两个类都正确地遵循了 HasArea 协议。

如下所示,Animal 是一个未遵循 HasArea 协议的类:

classAnimal {
var legs: Int
init(legs: Int) { self.legs = legs }
}

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

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

objects 数组使用字面量初始化,数组包含一个 radius2Circle 的实例,一个保存了英国国土面积的 Country 实例和一个 legs4Animal 实例。

如下所示,objects 数组可以被迭代,并对迭代出的每一个元素进行检查,看它是否遵循 HasArea 协议:

for object in objects {
iflet 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 常量上。objectWithAreaHasArea 协议类型的实例,因此 area 属性可以被访问和打印。

objects 数组中的元素的类型并不会因为强转而丢失类型信息,它们仍然是 CircleCountryAnimal 类型。然而,当它们被赋值给 objectWithArea 常量时,只被视为 HasArea 类型,因此只有 area 属性能够被访问。

可选的协议要求

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

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

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

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

@objcprotocolCounterDataSource {
@objcoptionalfuncincrement(forCountcount: Int) ->Int
@objcoptionalvar fixedIncrement: Int { get }
}

CounterDataSource 协议定义了一个可选方法 increment(forCount:) 和一个可选属性 fiexdIncrement,它们使用了不同的方法来从数据源中获取适当的增量值。

注意

严格来讲,CounterDataSource 协议中的方法和属性都是可选的,因此遵循协议的类可以不实现这些要求,尽管技术上允许这样做,不过最好不要这样写。

Counter 类含有 CounterDataSource? 类型的可选属性 dataSource,如下所示:

classCounter {
var count =0
var dataSource: CounterDataSource?
funcincrement() {
iflet amount = dataSource?.increment?(forCount: count) {
            count += amount
        } elseiflet amount = dataSource?.fixedIncrement {
            count += amount
        }
    }
}

Counter 类使用变量属性 count 来存储当前值。该类还定义了一个 increment 方法,每次调用该方法的时候,将会增加 count 的值。

increment() 方法首先试图使用 increment(forCount:) 方法来得到每次的增量。increment() 方法使用可选链式调用来尝试调用 increment(forCount:),并将当前的 count 值作为参数传入。

这里使用了两层可选链式调用。首先,由于 dataSource 可能为 nil,因此在 dataSource 后边加上了 ?,以此表明只在 dataSource 非空时才去调用 increment(forCount:) 方法。其次,即使 dataSource 存在,也无法保证其是否实现了 increment(forCount:) 方法,因为这个方法是可选的。因此,increment(forCount:) 方法同样使用可选链式调用进行调用,只有在该方法被实现的情况下才能调用它,所以在 increment(forCount:) 方法名称后边也加上了 ?

调用 increment(forCount:) 方法在上述两种情形下都有可能失败,所以返回值为 Int? 类型。虽然在 CounterDataSource 协议中,increment(forCount:) 的返回值类型是非可选 Int。另外,即使这里使用了两层可选链式调用,最后的返回结果依旧是单层的可选类型。关于这一点的更多信息,请查阅 连接多层可选链式调用

在调用 increment(forCount:) 方法后,Int? 型的返回值通过可选绑定解包并赋值给常量 amount。如果可选值确实包含一个数值,也就是说,数据源和方法都存在,数据源方法返回了一个有效值。之后便将解包后的 amount 加到 count 上,增量操作完成。

如果没有从 increment(forCount:) 方法获取到值,可能由于 dataSourcenil,或者它并没有实现 increment(forCount:) 方法,那么 increment() 方法将试图从数据源的 fixedIncrement 属性中获取增量。fixedIncrement 是一个可选属性,因此属性值是一个 Int? 值,即使该属性在 CounterDataSource 协议中的类型是非可选的 Int

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

classThreeSource:NSObject, CounterDataSource {
let fixedIncrement =3
}

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

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

上述代码新建了一个 Counter 实例,并将它的数据源设置为一个 ThreeSource 的实例,然后调用 increment() 方法 4 次。正如预期一样,每次调用都会将 count 的值增加 3.

下面是一个更为复杂的数据源 TowardsZeroSource,它将使得最后的值变为 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。一旦增加到 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 子句 中所描述的。

例如,你可以扩展 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 而且整数遵循 EquatableequalNumbersdifferentNumbers 都可以使用 allEqual() 方法。

print(equalNumbers.allEqual())
// 打印 "true"
print(differentNumbers.allEqual())
// 打印 "false"

注意

如果一个遵循的类型满足了为同一方法或属性提供实现的多个限制型扩展的要求, Swift 会使用最匹配限制的实现。

image
EchoEcho官方
无论前方如何,请不要后悔与我相遇。
1377
发布数
439
关注者
2223063
累计阅读

热门教程文档

Swift
54小节
MyBatis
19小节
Spring Boot
24小节
HTML
32小节
Typescript
31小节