Swift 5.1 (20) - 协议

avatar
奇舞团移动端团队 @奇舞团

级别: ★☆☆☆☆
标签:「iOS」「Swift 5.1 」「协议」
作者: 沐灵洛
审校: QiShare团队


协议定义了适合特定任务或功能的方法,属性。协议可以由类,结构或枚举实现,任何类型实现协议的要求方法称为遵守协议。 个人理解:Swift中的协议所能实现的功能,不再局限于OC的代理委托。协议中定义的方法、属性,在遵守协议的类型的实例中可以直接调用和使用。协议这种新的能力,使得协议在Swift中的使用更加的灵活。

Protocol语法

为类,结构体,枚举定义协议的语法

protocol `protocolName` {
   //定义协议
}

自定义类型遵守协议的语法 协议名称放在类型名称之后,并用冒号分隔,多个协议时,协议之间使用逗号。当类类型有父类时则父类类型名称放在协议之前并用逗号隔开。

  • 值类型
struct SomeStructure: FirstProtocol, AnotherProtocol {
    // 结构体定义
}
  • 类类型
class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
    // class定义
}

协议中定义属性的要求

协议可以要求任何遵守此协议的类型提供一个具有特定类型和名称的实例属性或类属性。但是协议不能指定属性应该是一个存储属性还是一个计算属性。因此协议中定义的属性只能要求属性的名称与类型,并且指定属性是可get或支持getset的。

如果协议要求一个属性是可getset的,那么协议对这个属性的要求是不能被常量存储属性或者只读的计算属性满足的。 如果协议仅要求一个属性是可get的,那么这个要求可以被任何类型的属性满足,同时如果有必要,这个属性也是可set的。

属性要求:

  • 协议中定义属性必须始终声明为变量属性,并以var关键字为前缀。通过在类型声明后写{ get set }来表示属性可读写,通过写{get}来表示可读的属性。
protocol SomeProtocol {
    var mustBeSettable: Int { get set }
    var doesNotNeedToBeSettable: Int { get }
}
  • 协议中定义类属性时,必须始终在前面使用static关键字。即使该协议的类属性在被类实现时,以classstatic为前缀都是符合协议的。
protocol protocolName {
   //定义协议
    static var something : String { get}
}
class ViewController: UIViewController,protocolName {
    class var something: String {//使用static也可
        "协议中定义的类属性"
    }
}

使用举例:

定义协议如下

protocol Category {
    var kind : String {get set}
}

类实现

class Animal : Category {
    var kind: String
    init(kinds:String) {
       kind = kinds
    }
    convenience init(){
        self.init(kinds:"🐱")
    }
}
let animal = Animal()
print(animal.kind)//🐱

注意: 协议中属性为{get set}使用计算属性时必须get{}set{}不能为get{};协议中属性为{get}时则可以使用get{}或者get{}set{}

class Animal : Category {
    var houzhui : String = "类"
    var kind: String {
        get{
          houzhui
        }
        set{
           houzhui = newValue + "类"
        }
    }
}
//调用
let animal = Animal.init()
animal.kind = "🐱"
print(animal.kind) // 🐱类

结构体实现

struct AnimalStruct : Category {
    var kind: String    
}
let animal1 = AnimalStruct(kind:"🐩")
print(animal1.kind)//🐩

协议中定义方法的要求

协议中方法定义与普通实例和类方法定义方式一样,并且允许可变参数,遵守与常规方法相同的规则 区别:没有花括号和方法主体;无法为方法参数指定默认值。会要求符合此协议的类型实现协议中规定的实例方法和类方法。

回顾可变参数:形式:Type...,作用可以接受零个或多个指定类型的值,在函数体内可用作指定类型的数组。

协议中定义类方法时,必须始终在前面使用static关键字。即使该协议的类方法在被类实现时,以classstatic为前缀都是符合协议的。

定义方法协议

protocol MethodProtocol {
    func instanceMethod(para:String) -> String
    static func classMethod(para:String)->String
    func hasVariableParameter(somePara:String...) -> String
}

实现协议方法

class MethodProtocolClass : MethodProtocol {
    func hasVariableParameter(somePara: String...) -> String {
        var result : String = ""
        for item in somePara {
            result += item
        }
        return result
    }
    
    func instanceMethod(para: String) -> String {
        para + "实例方法协议实现"
    }
    class func classMethod(para: String) -> String {//!< static也可以
        para + "类方法协议实现"
    }
}
//调用
let instance = MethodProtocolClass()
print(instance.instanceMethod(para: "hello!"))//!< hello!实例方法协议实现
print(MethodProtocolClass.classMethod(para: "Hi!"))//!< Hi!类方法协议实现
print(instance.hasVariableParameter(somePara: "QiShare"," ","Come On","!"))//!< QiShare Come On!

可变方法要求

值类型实现协议方法,修改值类型实例本身,则此协议方法需要使用mutating关键字作为前缀。 若定义协议实例方法,旨在对所有遵守该协议的实例进行修改,需要将此方法标记mutating,以涵盖值类型。注:mutating标记的协议方法,由类类型实现时,无需编写mutating关键字,mutating关键字仅由结构和枚举使用。

protocol Togglable {
    mutating func switchOperate()
}

extension Bool : Togglable {
    mutating func switchOperate() {
        self = !self
    }  
}
//调用
var value = false
value.switchOperate()
print(value)//true

协议中定义初始化方法的要求

协议中定义初始化方法也和普通情况下定义初始化方法一样,唯一区别便是没有函数体和花括号。会要求遵守该协议的类型实现此初始化方法。

protocol SomeProtocol {
    init(someParameter: Int)
}

类,实现协议中初始化方法的要求

遵守协议的类类型可以实现协议中的初始化方法为指定初始化方法或者便利初始化方法,无论哪种实现,都必须使用required关键字标记。

回顾:required关键字修饰初始化方法,标记此初始化方法其所有子类必须要实现,子类实现required标记的初始化方法无需写override,但必须写required,指示该初始化方法的延续性。

//类,实现协议中初始化方法可谓是签署了魔鬼契约,要求子子孙孙都要履行,可谓之曰霸气侧漏
class SomeClass: SomeProtocol {
   required init(someParameter: Int) {
   }
    //或者实现为便利初始化方法
   required convenience init(someParameter: Int) {
       self.init()
    }
}

使用required关键字确保了遵守此协议的类类型的所有子类都能提供协议中初始化方法的实现,使其子类都遵守协议。 注意:使用final标记类类型,实现协议初始化方法时,无需使用required关键字,因为final是阻止子类化的。

特殊:若子类重写了父类的指定初始化方法,并且协议中的初始化方法与子类重写的父类的初始化方法一致,则子类实现此协议时,需同时使用requiredoverride修饰符进行标记。

protocol SomeProtocol {
    init()
}
class SomeSuperClass {
    init() {
    }
}
class SomeSubClass: SomeSuperClass, SomeProtocol {
    // "required"表示遵守协议; "override" 表示重写
    required override init() {
        //初始化方法零参数符合省略super.init()条件
    }
}

协议中定义可失败的初始化方法

协议中定义的可失败的初始化方法,可以被遵守该协议的类型实现为可失败的初始化方法或者不可失败的初始化方法。

protocol SomeProtocol {
    init?(name:String)
}
class SomeClass : SomeProtocol {
    
    required init(name:String) {

    }
    required init?(name: String) {
        if name.isEmpty {return nil}
    }
    required init!(name: String) {
        
    }
}

作为类型使用的协议

协议本身实际上并未实现任何功能。但是却可以将协议用作完整类型。 使用协议作为类型有时也称为存在类型,存在类型来自短语“存在类型T,使得T遵守协议”。类似OC中的@property (nonatomic, weak) id<protocolName> delegate

可以在允许使用其他类型的许多地方使用协议:

  • 作为函数,方法,初始化方法的参数类型或返回值类型
  • 作为常量,变量,或属性的类型
  • 作为数组,字典或其他容器的元素类型
protocol RandomNumberProtocol {
    func random() -> UInt
}
class RandomNumberSmallGenerator: RandomNumberProtocol {
    var name = "遵守协议的属性"
    func random() -> UInt {
        return UInt(arc4random() % 10)
    }
}
class RandomNumberBigGenerator: RandomNumberProtocol {
    func random() -> UInt {
        return UInt(arc4random() % 10) + 10
    }
}
class BoomTest {
    var generator : RandomNumberProtocol //!< 协议作为类型修饰属性
    var description : String
    //!< 协议作为类型修饰方法的参数,所有符合`RandomNumberProtocol`的类型都可以传入
    init(des:String,random:RandomNumberProtocol) {
        generator = random
        print("初始化时调用一下协议方法:\(generator.random())")
        description = des
    }
    //!<如何使用协议中的方法呢?需要单独定义方法
    func toRandom() -> String {
        description + "\(generator.random())"
    }  
}
//调用
let randomNum = BoomTest.init(des: "💥", random: RandomNumberSmallGenerator())
for _ in 0...2 {
    let result = randomNum.toRandom()
    print(result)
}
//输出
💥5
💥4
💥0

协议类型的集合: 协议作为集合的元素类型举例:

let protocolArray : [RandomNumberProtocol] = [RandomNumberSmallGenerator(),RandomNumberBigGenerator()]
for item in protocolArray {
    let num = item.random()
    print(num) //!< 1 17
}

参数或属性为协议类型时,当传入遵守此协议的类型时,是否可以通过此属性或参数,来访问遵守此协议类型的方法或者属性?答案是不能直接访问,通过协议类型的属性或参数只能访问到协议中的方法或属性,如何做到?需要类型转换。

for item in protocolArray {
    let num = item.random()
    if let smallGenerator = item as? RandomNumberSmallGenerator {
        print(smallGenerator.name) //log:遵守协议的属性
    }
    print(num) //!< 1 17
}

Delegation

委托是一种设计模式,使类或结构体可以将其某些职责委托给其他类型的实例。通过定义封装委托职责的协议来实现此设计模式,从而保证遵守协议的类型(或称委托)提供协议需要的的功能。委托可用于响应特定操作,或从外部源检索数据,而无需了解该源的底层类型。 这种模式OC中也是有的,示例如下:

protocol PersonActivity {
    func sleep()
    func eat()
    func play()
}
//通过将AnyObject协议添加到协议的继承列表中,可以将遵守此协议的类型限制为类类型(而不是结构或枚举)。
protocol PersonDelegate : AnyObject {
    func personNowDoSomething(name: String,SomeThing:String) -> Void
}

class Person: PersonActivity {

    var name : String
    var age : UInt
    //!< 使用了weak 因此遵守此协议的类型必须是类类型,故`PersonDelegate`继承`anyobjct`
    weak var delegate : PersonDelegate?
    
    init(name:String = "QiShare",age:UInt = 1) {
        self.name = name
        self.age = age
    }
    
    func activity() -> Void {
        let num = arc4random()%3
        switch num {
        case 0:
            sleep()
        case 1:
            eat()
        default:
            play()
        }
    }
    
    func sleep() {
        delegate?.personNowDoSomething(name:name,SomeThing: "睡觉")
    }
    
    func eat() {
        delegate?.personNowDoSomething(name:name,SomeThing: "吃饭")
    }
    
    func play() {
        delegate?.personNowDoSomething(name:name,SomeThing: "玩耍")
    }
}

class DelegationClass:PersonDelegate{
    func personNowDoSomething(name:String,SomeThing: String) {
        print(name + "正在" + SomeThing) //会输出
    }
}

注意:为了防止强引用循环,需要将委托声明为弱引用,即使用weak修饰。

仅类类型遵守的协议Class-Only Protocols

通过将AnyObject协议添加到协议的继承中,可以将遵守此协议的类型限制为类类型(而不是结构或枚举,否则会触发编译时错误)。使用weak 修饰的协议类型的属性,则传入的遵守此协议的实例只能为类类型实例。

使用扩展使某个类型遵守协议

使用扩展使得现有类型遵守新的协议。扩展能够为现有类型添加计算属性,下标,方法,因此能够添加协议要求的方法,属性。 注:当协议被遵守和实现在实例的类型扩展中,现有类型的实例会自动采用和遵守。

protocol Togglable {
    mutating func switchOperate()
}

extension Bool : Togglable {
    mutating func switchOperate() {
        self = !self
    }  
}
//调用
var value = false
value.switchOperate()
print(value)//true

有条件地遵守协议

泛型仅在特定的条件下能够满足协议的要求。通过协议名称后使用泛型的where子句来使泛型类型有条件的遵守协议。

// Array就是泛型`Array<Element>`
extension Array : RandomNumberProtocol where Element : RandomNumberProtocol {
    func random() -> UInt {
        732
    }
}
//调用
let num1 = RandomNumberSmallGenerator()
let num2 = RandomNumberSmallGenerator()
let protocolArray = [num1,num2]
print("\(protocolArray.random())")//!< 732

上述测试示例总结:

  • extension Array : RandomNumberProtocol where Element : RandomNumberProtocol表示Array中的元素都遵守某个协议时,Array的实例才能使用此协议方法。
  • let num2 = RandomNumberBigGenerator()则会报错Protocol type 'Any' cannot conform to 'RandomNumberProtocol' because only concrete types can conform to protocols。故Array必须是固定类型,不能是AnyAnyObject,才能遵守此协议。
  • let protocolArray : [RandomNumberProtocol] = [num1,num2]则会报错:Protocol type 'RandomNumberProtocol' cannot conform to 'RandomNumberProtocol' because only concrete types can conform to protocols
extension Array : RandomNumberProtocol where Element : RandomNumberProtocol

不能使用特定类型的Array,例如:Array<String>

通过扩展声明遵守协议

如果类型已经遵守协议的所有要求,但尚未声明该类型遵守协议,则可以通过一个空扩展 来声明。

protocol RandomNumberProtocol {
    func random() -> UInt
}

class RandomNumberSmallGenerator {
    var name = "遵守协议的属性"
    func random() -> UInt {
        return UInt(arc4random() % 10)
    }
}

extension RandomNumberSmallGenerator : RandomNumberProtocol {}

协议的继承

协议可以继承一个或多个其他协议,并且可以在继承的要求(方法、属性)之上添加其他要求。协议继承的语法类似于类继承的语法,多个继承的协议之间逗号分隔。基本语法:

protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
}

示例如下:

protocol TextDescription {
    func TextDescription()->String
}
protocol AdditionTextDescription : TextDescription {
    var goodEvaluation : String {get}
}
struct Evaluation : AdditionTextDescription {
    var goodEvaluation: String {
         TextDescription() + "很好!"
    }
    func TextDescription() -> String {
        "这个结构体"
    }
}
//调用
let evaluation = Evaluation.init()
print(evaluation.goodEvaluation)//!< 这个结构体很好!

协议组合

协议组合可以组合多个协议成为一个临时的本地协议,该协议具备了组合的所有协议要求,且不会任何新的协议类型。这对于要求某个类型同时遵守多个协议是很有用的。 协议组合的形式SomeProtocol & AnotherProtocol。可以使用&作为分隔符列出需要的任意数量的协议。另外,协议组合也可以包含类类型,使用此类类型指定遵守协议类类型的父类,验证得知:本类也是可以的。

protocol Color {
    var color : String {get}
}
protocol Feature {
    var feature : String {get}
}

class Dog {
   var name : String
    init(_ name : String = "阿里克") {
        self.name = name
    }
}

class Cat : Color,Feature{
    var name : String
    var color: String{
        "黑色"
    }
    var feature: String{
        "撒娇"
    }
    init(_ name : String = "小黄") {
        self.name = name
    }
}
class Husky: Dog,Color,Feature {
    var color: String
    var feature: String
    init(_ name : String,_ color : String,_ feature : String) {
        self.color = color
        self.feature = feature
    }
}

class Kid {
    static func hasCat(pet:Cat&Color&Feature)->String {
        "恭喜你获得了宠物猫:\(pet.name) 颜色:\(pet.color) 特点:\(pet.feature)"
    }
    static func hasDog(pet:Dog&Color&Feature)->String {
        "恭喜你获得了宠物狗:\(pet.name) 颜色:\(pet.color) 特点:\(pet.feature)"
    }
}
//调用
let cat = Kid.hasCat(pet: Cat.init())//!< 恭喜你获得了宠物猫:小黄 颜色:黑色 特点:撒娇
print(cat)
let dog = Kid.hasDog(pet: Husky.init("哈怂奇", "火红", "家中地雷"))//!< 恭喜你获得了宠物狗:阿里克 颜色:火红 特点:家中地雷
print(dog)

检查类型实例是否遵守特定协议

使用isas检查类型实例是否遵守特定协议并且转换为特定协议。语法与类型转换一样。

  • 如果实例遵守协议,则is运算符返回true,否则返回false
  • as?向下转换为目标协议类型的可选值,如果实例不遵守该协议,则返回nil
  • as!强制向下转换为目标协议类型,转换失败则是触发运行时错误。
protocol HasArea {
    var area: Double { get }
}
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 }
}
//调用
let objects: [AnyObject] = [
    Circle(radius: 2.0),
    Country(area: 243_610),
    Husky.init("哈士奇", "火红", "家中地雷")
]
for object in objects {
    if let objectWithArea = object as? HasArea {
        print("Area is \(objectWithArea.area)")
    } else {
        print("Something that doesn't have an area")
    }
}

可选协议要求

我们可以定义协议可选的要求,这些要求不是必须被遵守此协议的类型实现的。即:我们可以编写遵守某个协议的自定义类,而无需实现任何可选协议要求。 协议的可选要求的定义:使用optional修饰符作为前缀,定义协议要求即可。 在协议的可选要求中使用方法或属性时,其类型将自动变为可选。例如,类型(Int)->String的方法变为((Int)->String)?。注意:是整个函数类型都包装在可选内容中,而不是方法的返回值中。本质是:协议中定义的方法名称便是函数的实例,此函数类型变为了可选。

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

实际操作过程中发现不使用@objc修饰协议,是无法使用optional修饰符的,否则报错:'optional' requirements are an Objective-C compatibility feature。 总结:通过这个错误我们才知道原来,可选协议要求是Objective-C兼容性功能,optional必须与@objc联合使用:

  • optional必须在@objc修饰的协议下使用,并且必须采用@objc optional的形式。
  • 协议的可选要求不能被结构体或枚举采用。

使用举例:

@objc protocol CounterDataSource {
     @objc optional func increment(forCount count: Int) -> Int
     @objc optional var fixedIncrement: Int { get }
}
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
        }
    }
}
class ThreeSource: CounterDataSource {
    let fixedIncrement: Int = 3
}
//调用
let counter = Counter()
counter.dataSource = ThreeSource()
for _ in 1...4 {
    counter.increment()
    print(counter.count) // 3 6 9 12
}

协议扩展

协议通过扩展可以为遵守协议的类型提供方法,初始化,下标和计算属性的实现。这一点允许我们为协议本身定义行为,而不是基于遵守协议的每个类型。 协议扩展可以将实现添加到遵守协议的类型中,但不能使协议要求进行扩展或从另一个协议继承。协议继承始终在协议声明本身中指定。

protocol Color {
    var color : String {get}
}
protocol Feature {
    var feature : String {get}
}
//扩展协议Feature
extension Feature {
    var like_feature : String {
        return "喜欢" + feature
    }
}
class Kid {
    static func hasCat(pet:Cat&Color&Feature)->String {
        "恭喜你获得了宠物猫,特点:\(pet.like_feature)"
    }
    static func hasDog(pet:Dog&Color&Feature)->String {
        "恭喜你获得了宠物狗,特点:\(pet.like_feature)"
    }
}
//调用结果
恭喜你获得了宠物猫,特点:喜欢撒娇
恭喜你获得了宠物狗, 特点:喜欢捣蛋

协议要求提供默认实现

我们可以使用协议扩展为当前协议要求定义的任何方法或计算属性提供默认的实现。如果一个遵守此协议的类型提供了属于它自己关于某个协议要求的实现,那么将会代替协议扩展中提供的那一个。 通过扩展提供协议的默认实现,也可以使得遵守该协议的类型不必提供它们自己的实现,这点和可选协议要求一样。但是采用这种方式为可选协议要求增加默认实现后,则无需使用可选链接。

protocol Color {
    var color : String {get}
}
protocol Feature {
    var feature : String {get}
}
//为这两个协议提供默认实现
extension Color {
    var color : String {
        "彩虹色"
    }
//    var feature : String {
//        "动物能有啥子特点!"
//    }
}
extension Feature {
    var feature : String {
        "动物能有啥子特点?"
    }
}
class Cat : Color,Feature{
    var name : String
    var color: String{
        "黑色"
    }
//    var feature: String{
//        "撒娇"
//    }
    init(_ name : String = "小黄") {
        self.name = name
    }
}
class Kid {
    static func hasCat(pet:Cat&Color&Feature)->String {
        "恭喜你获得了宠物猫:\(pet.name) 颜色:\(pet.color) 特点:\(pet.like_feature)"
    }
}
//log:恭喜你获得了宠物猫:小黄 颜色:黑色 特点:喜欢动物能有啥子特点?

添加约束到协议扩展

定义协议扩展时,在协议扩展中定义的方法与属性可用之前,我们可以指定遵守此协议的类型必须满足的约束条件,否则将不可用。 方式:在要扩展的协议名称使用泛型where子句添加约束。

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

let equalNumbers = [100, 100, 100, 100, 100]
let differentNumbers = [100, 100, 200, 100, 200]
//let differentNumbers : [Cat] = [Cat.init(), Cat.init()]
//触发错误:Referencing instance method 'allEqual()' on 'Collection' requires that 'Cat' conform to 'Equatable'
print(equalNumbers.allEqual())//true
print(differentNumbers.allEqual()) //false

参考资料: swift 5.1官方编程指南


可添加如下小编微信,并备注加入QiShare技术交流群,小编会邀请你加入《QiShare技术交流群》。

小编微信

了解更多iOS及相关新技术,请关注我们的公众号:

image

关注我们的途径有:
QiShare(简书)
QiShare(掘金)
QiShare(知乎)
QiShare(GitHub)
QiShare(CocoaChina)
QiShare(StackOverflow)
QiShare(微信公众号)

推荐文章:
Swift 5.1 (19) - 扩展
Swift 5.1 (18) - 嵌套类型
Swift 5.1 (17) - 类型转换与模式匹配 浅谈编译过程
深入理解HTTPS 浅谈 GPU 及 “App渲染流程”
iOS 查看及导出项目运行日志
Flutter Platform Channel 使用与源码分析
开发没切图怎么办?矢量图标(iconFont)上手指南
DarkMode、WKWebView、苹果登录是否必须适配?
奇舞团安卓团队——aTaller
奇舞周刊