Protocols顾名思义它是一种约定,在Swiftprotocol比较类似于其他面向对象语言中的interface(接口)。空口说还是比较抽象,我们还是先看代码。

protocol Staff{
    func work()
}

class Programmer:Staff{
    func work() {
        print("我在写代码")
    }
}


class Designer:Staff{
    func work() {
        print("我在搞设计")
    }
}

let 张三 = Programmer()
let 李四 = Designer()

张三.work()
李四.work()

除了开头三行定义protocol的代码,其他的我们之前都用过或者见过类似的。即使不运行代码,你应该也能猜到最后程序的输出内容。可能引起费解的应该是定义ProgrammerProgrammer:Staff以及Designer:Staff这段,因为我们之前介绍过,如果需要一个类继承另外一个类,就是需要用:做连接的。但是此刻,显然我们不是在做继承,因为前面那个叫Staff就不是class,当然也不能当作父类Staff前面的关键字亮明了身份,它就是我们今天要介绍的protocol

protocol可以用来规定一组行为,它自己并不告诉我们这些行为具体该怎么做,但是它可以用来约定其他人,要求这些其他人必须把这些事情做到位,这就是protocol的主要用途。

结合上面的例子已经很好理解了。工作需要有职工 Staff,他们的主要能力就是工作 work,但是不同的工种有不同的work内容。所以ProgrammerDesigner都有不同的work函数的具体实现(例子中就是打印一句话)。因为ProgrammerDesigner都被Staff约束着,你就可以想象,存在一位老板,他会口头禅就是:“我管你是什么工种,来我这就得给我干活”。

此时,如果我们再创造一个类ProductManager,它也通过:Staff受到约束,但是它就不干活,我们来看看会怎样。

CleanShot-2022-05-13-16.20.13

如上图,我们没有在ProductManager里提供work函数,此时编译器就会报错了,告诉我们这个ProductManager违背了Staff的约定。

protocol不光可以约定别人的函数,也可以约定别人的属性。接着我们的例子说,老板对每位员工都一视同仁,管你是中国人还是外国人,都必须有个工号(jobNumber),这就可以用protocol来实现。

CleanShot-2022-05-13-16.25.37

但是如果我们贸然对Staff增加一个变量是不行的,就会收到编译器给出的报错,如上图。同时,编译器也给出了一个提示帮我们改进代码,改进过后的代码如下:

protocol Staff{
    var jobNumber:String{get set}
    func work()
}

当你改完Staff之后,之前的ProgrammerDesigner也会同时跟着报错,因为编译器检查到它俩已经不在满足之前的约定了,也得与时俱进才行。这个时候,你只要在ProgrammerDesigner中追加属性var jobNumber: String就可以了。但是因为它俩毕竟是class,但是你声明了一个不为空的变量,又没有构造函数来初始化数据,肯定是说不过去的,编译器还是不愿意放过你,所以你会看到下面的报错:

CleanShot-2022-05-13-16.36.02

如果你没有忘记之前我们讲过的init方法的话,想修复这个问题其实很容易,最终Programmer代码如下:

class Programmer:Staff{
    var jobNumber: String
        
    init(jobNumber:String){
        self.jobNumber = jobNumber
    }

    func work() {
        print("我在写代码")
    }
}

那么创建一个程序员的代码也得稍加变动,还以张三为例的话,就得这样写了:

let 张三 = Programmer(jobNumber: "001")

这里留个小小的思考题给你,如果我想在张三.work()时输出我是工号:001,我在写代码,该怎么修改代码呢?


在前面的例子中,我们展示了protocol是如何作用于class的,但其实它也可以作用于structenum,主要用法差不多,但是有一点是值得注意的,因为structenum毕竟是值类型,默认不具备自己修改自己的能力,在一些特殊场景下还是要注意的。

下面我们就来看一下稍微特殊点的场景,回到前面的Staff的例子,我们再给它加一个属性薪资 salary,以及追加一个函数涨薪 salaryIncrease,调整过的Staff如下:

protocol Staff{
    var salary:Float{get}
    var jobNumber:String{get set}
    
    func work()
    func salaryIncrease(ratio:Float)
}

有一点注意的是,我们并没给salary``set的机会,因为薪资嘛,岂能让别人说改就改,只有老板通过salaryIncrease才能对员工变动薪资。同时,salaryIncrease的参数我们设计为一个比例值,如果该值为0.1的话,就是涨薪10%的意思。下面我们来优化Programmer,最终代码如下:

class Programmer:Staff{
    var salary: Float
    var jobNumber: String
        
    init(jobNumber:String, salary:Float){
        self.jobNumber = jobNumber
        self.salary = salary
    }

    func work() {
        print("我是工号:\(jobNumber),我在写代码")
    }
    
    func salaryIncrease(ratio: Float) {
        self.salary = self.salary + self.salary*ratio
    }
    
}

上面的代码是基于class的,当我们把关键字class改成struct的时候,它就变成结构体了。但是同时,我们会收到一个报错:

CleanShot-2022-05-13-17.16.23

也就是前面提到的struct默认不能自己改自己的问题。此时你需要声明这个方法是mutating的,从而获得可修改自己的能力。

mutating func salaryIncrease(ratio: Float) {
    self.salary = self.salary + self.salary*ratio
}

当然这样改了之后,protocol那边也得要改,最终Staff代码如下:

protocol Staff{
    var salary:Float{get}
    var jobNumber:String{get set}
    
    func work()
    mutating func salaryIncrease(ratio:Float)
}

这样改动之后,如果我们真的想给张三加薪的话,就可以通过下面的代码实现:

var 张三 = Programmer(jobNumber: "001",salary: 1000)
print(张三.salary) //1000.0
张三.salaryIncrease(ratio: 0.1)
print(张三.salary) //1100.0

顺便说一下,因为class也可以实现protocol,但是约定方法的关键字mutating是不适用class的,那么这样含有mutatingprotocol还能用来约定class么?

答案是可以的,但是要注意的是,虽然protocol中的函数是有mutating作为修饰,但是在class中,要把mutating关键字删除就行了。说起来有点绕,这里我们分别用Programmer作为classDesigner作为struct来给大家演示一下,最终代码如下:

protocol Staff{
    var salary:Float{get}
    var jobNumber:String{get set}
    
    func work()
    mutating func salaryIncrease(ratio:Float)
}

class Programmer:Staff{
    var salary: Float
    var jobNumber: String
        
    init(jobNumber:String, salary:Float){
        self.jobNumber = jobNumber
        self.salary = salary
    }

    func work() {
        print("我是工号:\(jobNumber),我在写代码,我的薪资是\(salary)")
    }
    
    func salaryIncrease(ratio: Float) {
        self.salary = self.salary + self.salary*ratio
    }
    
}

struct Designer:Staff{
    var salary: Float
    var jobNumber: String
        
    init(jobNumber:String, salary:Float){
        self.jobNumber = jobNumber
        self.salary = salary
    }

    func work() {
        print("我是工号:\(jobNumber),我在搞设计,我的薪资是\(salary)")
    }
    
    mutating func salaryIncrease(ratio: Float) {
        self.salary = self.salary + self.salary*ratio
    }
    
}


let 张三 = Programmer(jobNumber: "001",salary: 1000)
张三.salaryIncrease(ratio: 0.1)
张三.work()



var 李四 = Designer(jobNumber: "002",salary: 1000)
李四.salaryIncrease(ratio: 0.15)
李四.work()

得到的输出结果会是:

我是工号:001,我在写代码,我的薪资是1100.0
我是工号:002,我在搞设计,我的薪资是1150.0

上面的代码中还有个小细节,张三我们用let声明,而李四却用的var,这可不是笔误,你能参透其中的原因么?

最后还要强调的是,用:去约定protocol是可以同时实现多个的,但是继承父类,只能继承一个,它们都是用唯一的:来做连接的。如果存在既要继承class又要实现protocol的情况,应该把父类放在紧随:之后,如图:

CleanShot-2022-05-13-17.47.54


参考资料:

往期回顾: