Swift 协议 Protocol 整理

4,170 阅读10分钟

协议是Swift中非常重要的一块内容,基本上所有号称“掌握中级以上Swift”才可以看的书/教程,代码中都是面向协议编程、泛型、闭包的广泛应用。

协议的语法

协议是干嘛的呢,顾名思义,它规定了一些东西,如果你遵循这个协议,你就要遵循协议中写定的某些东西。

//定义一个协议
protocol DemoProtocol {
// 规定一些东西
}

当然我们自定义的类型(struct,class,enum)也可以遵循多个协议,在各个协议之间,我们用逗号来分割开。

struct DemoStruct: DemoProtocol, DemoProtocol2 {

}

如果是class类型,同时它拥有父类,我们将父类名放在协议之前,因为毕竟遵守孝道是中外统一美德。

class SonClass: FatherClass, DemoProtocol {

}

我们讲完了协议的定义,自然需要讲一讲协议里面规定的内容:

那我们能在协议里放些什么东西呢,你肯定首先想到属性啊方法啊之类的,对的,这是我们面向对象编程的习惯,我们先来看看协议里的属性有啥要求。

协议中的属性

协议可以要求遵循协议的类型提供特定名称和类型的实例属性或类型属性。

我们来看一看这句话,协议,可以,要求遵循这份协议的类型(类或者结构体等),提供特定名称和类型实例属性\color{red}{实例属性}或者类型属性\color{red}{类型属性}

这句话好拗口啊。我们逐个来看。

我们正好借此复习一下属性的知识,在Swift中,属性就两大类,实例属性和类型属性。

实例属性又分为存储实例属性和计算实例属性。
类型属性分为存储类型属性和计算类型属性。

存储实例属性\color{red}{存储实例属性}: 存储在实例的内存中的属性,只有一份。

计算实例属性\color{red}{计算实例属性} : 不占用系统内存,调用的时候才计算得出的实例属性,类似实例的方法。

存储类型属性\color{blue}{存储类型属性} : 整个程序运行过程中就只有一份内存,类似全局变量或常量。

计算类型属性\color{blue}{计算类型属性}: 不占用系统内存,调用的时候才计算得出的属性,类似全局函数。

也就是说,如果你起草某个协议,你可以要求遵循此份协议的类型,提供特定名称和类型的各种属性。

但我们的协议,不能定太死,太死板不是21世纪的风格,所以呢,我们的协议不指定属性是存储属性还是计算属性,都行,都可以,都来。你只需要给属性特定的名字和类型就好了。

但是,我们写在协议中的内容,有时候需要一点细致的东西,就像讨论一份合同一样,合同的样式我们不死板,合理就可以,但是涉及的内容,我们需要考虑,需要指定。所以,协议可以指定属性是可读的\color{red}{可读的}还是可读可写的\color{red}{可读可写的}

在协议中,我们给属性类型后面加上一对花括号,花括号里填上关键字,来指定它的读写类型。

protocol DemoProtocol {
	var demoInt: Int { get } //可读属性
    var demoString: String { get set } //可读可写属性
}

当我们在协议中定义类型属性\color{blue}{类型属性}的时候,我们通常在类型属性前面加一个static关键字。但如果你确定,遵循这个协议的只是我们的类类型,除了static,我们还可以使用class关键字放在类型属性前面。

protocol DemoProtocol {
	static var demoTypeProperty: Int { get set } 

}

所以,当你看到一个协议的时候,你很容易得出一些信息:

这个协议内定义的属性是什么属性?如果前面有static,奥是类型属性,如果没有,那就是实例属性,如果是class,奥是类型属性,而且这个协议被一个class类型所遵循。 属性后面有一个花括号get,奥是可读的,有一个花括号get set ,奥是可读可写的。

比如:

protocol FullyNamed {
    var fullName: String { get }
}

这个协议定义了一个实例属性 fullName, 且是可读的。除此之外没有任何要求,当我们遵循它时:


Struct PersonFullName {
	var fullName: String // 遵循FullName协议,我们必须得有一个fullName属性在这里。
}

let person = Person(fullName: "DGH")

我们再来看一个复杂点的:

class StartShip: FullyNamed {
    var prefix: String?
    var name: String
    init(name: String, prefix: String? = nil) {
        self.name = name
        self.prefix = prefix
    }
    var fullName: String {
        (prefix != nil ? prefix! + " " : "") + name
    }
}
var demo = StartShip(name: "豆国华号", prefix: "傻逼")
print(demo.fullName) 
// 傻逼豆国华号

我们fullName属性的只读特点被体现出来了。

OK ,协议中的属性我们讲完了,下面我们来看看另一个玩意,协议中的方法。

协议中的方法

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

官方的解释总是读起来让人老跑神,来,咱们来一步一步看。

首先,实例方法或类方法:

我们顺便复习一下方法的内容,在Swift中,方法分为实例方法和类方法:


class Demo {
//实例方法
	func printSomething() {
    	print("something")
    }
}

//实例方法调用
let demo = Demo()
demo.printsomething()

class Demo2 {
// 类方法
	class func printSomethingClass() {
    	print("somethingClass")
    }
    static func printSomethingStatic() {
    	print("somethingStatic")
    }
}

//类方法调用
Demo2.printSomethingClass()
Demo2.printSomethingStatic()

再看这句话: 可以在协议中定义具有可变参数的方法,和普通方法的定义方式相同。但是,不支持为协议中的方法提供默认参数。

意思就是:

protocol DemoProtocol {
    func printInt (input: Int = 1) // its wrong, will popup error
    func printString (input: String) // its right
}

总结一下,协议中我们可以定义一些方法,包括实例方法和类方法,我们不用写方法的花括号和方法体,如果你愿意,你可以给方法一些默认参数,但是你不能给默认参数赋原始值。

如果你在协议中定义类方法时,和定义类型属性一样,你可以再类方法前加一个关键字 static 。

协议中的异变方法

我们有时候需要在方法中改变方法所属的实例,比如在值类型(结构体,枚举)的实例方法中,我们在方法前面加一个关键字 mutating, 表示可以在这个方法中修改它所属的实例以及实例任意属性的值。

我们在协议中也可以定义异变方法,这样,当值类型去遵循这个协议时,它们也可以正常使用异变方法。

比如:

protocol Togglable {
    mutating func toggle()
}

enum onOffSwitch: Toggleable {
    case on, off
    mutating func toggle() {
    	switch self {
            case .on:
            self = .off
            case .off:
            self = .on
        }
    }
}

var lightToggle = onOffSwitch.off
lightToggle.toggle() // on

协议中的构造器

我们当然也可以在协议中写构造器,让遵循它的类型去实现它。我们就像写普通构造器一样,但同样不需要写花括号和构造器的实体。

protocol SomeProtocol {
    init(someParameter: Int)
}

当我们在遵循该类型的类中实现构造器时,无论是指定构造器还是便利构造器,我们都需要在它前面加上一个关键字 required

class Demo: SomeProtocol {
   required init(someParameter: Int){
   //实现部分
   }
}

使用 required 修饰符可以确保所有子类也必须提供此构造器实现,从而也能遵循协议。 如果类已经被标记为 final,那么不需要在协议构造器的实现中使用 required 修饰符,因为 final 类不能有子类。

我们设想一个情况,如果我们有一个类是这样的:

class Father {
	init() {
    }
}

同时我们有一个协议:

protocol SomeProtocol {
    init()
}

如果我们写一个类,它是Father的子类,我们又想让它遵循SomeProtocol , 这可怎么办呢? 我们既需要重写Father的init,又需要实现 SomeProtocol 的init。

你可以这样写:

class SonClass: Father,  SomeProtocol {
	required override init () {
  	// required 在先, override在后
    }
}

协议作为类型,以及委托 delegate

协议虽然不实现什么东西,但它可以作为一种类型来使用,我们会在委托中结合着梳理。

delegate是一种设计模式,它允许类或者结构体将一些他们负责的功能委托给其他类型的实例。委托模式的实现很简单: 定义一个协议来封装那些需要外包的功能,这样就能确定遵循协议的类型能提供这些功能。委托就是外包,理解了吧。

Swift官方给的例子不太友好,很乱,不好理解。在网上找了一个很好的例子:

//定义一个协议,协议里有一个方法来写log
protocol LogManagerDelegate {
  func writeLog()
}

// 一个用户登录的类
class UserController {
  var delegate: LogManagerDelegate?  // 协议作为类型
  func login() {
    delegate?.writeLog() 
  }
}

// 一个遵循LogManagerDelegate 的类
class SqliteLogManager: LogManagerDelegate {
  // 实现协议的方法
  func writeLog() {
    print("将数据写入sqlite数据库中")
  }
}

//我们来实例化调用看一看
let userController = UserController()
userController.login() // 什么都不会发生。

let sqliteLogManager = SqliteLogManager()
userController.delegate = sqliteLogManager
userController.login() // 输出: 将数据写入sqlite数据库中

通俗解释一下,我们的UserController类(登录公司), 它负责用户的登录,它想把写日志这个活外包出去,让其他的类帮它干这个活。于是,我们使用委托机制来实现这种想法。首先,我们定义了一个LogManagerDelegate协议,这个协议(合同)规定了一个写日志的方法func writeLog(),但是具体怎么干,是外包公司干的活。然后呢,SqliteLogManager类(外包公司)最近缺钱花,说这个活我们接了,所以这个类要遵循协议(遵循合同),然后它实现了这个func writeLog()。

当我们实例化我们的UserController类的时候,就像是登录公司授权给一个人,比如实习生豆国华好了,它全权代表了登录公司,这时候傻逼豆国华看到公司里有一台机器login() , 他马上去启动了这台机器想,觉得这不就直接完事了吗。然后登录的机器果然启动了,但是后台小哥发现你他瞄的就没有搞定写入日志的功能,怒喷豆国华一顿。豆国华灰溜溜地找到了外包公司SqliteLogManager(),外包公司全权授权给了他们公司的十佳员工崔鹏程 (sqliteLogManager), 豆国华终于找到了代理人,把崔鹏程带到了登录公司(userController.delegate = sqliteLogManager),然后再次启动login机器,崔鹏程全权代表了公司,把公司给他的优盘(print("将数据写入sqlite数据库中"))同时插入了login()机器,这时候两个臭皮匠合作,把日志写入成功。

这就是代理模式,这就是把协议作为类型使用。

扩展里的协议遵循

这个没什么好聊的,也就是说如果你为一个已有类型添加了扩展,这个扩展遵循了某个协议,跟你在原类型中遵循协议效果和要求都是一样的。

有条件地遵循协议

对于一些泛型类型来说,遵循协议是可以有特定条件的, 一般我们通过扩展来列出限制,让泛型类型有条件地遵循协议。 比如:

protocol TextRepresentable {
    var textualDescription: String { get }
}

extension Array: TextRepresentable where Element: TextRepresentable {
    var textualDescription: String {
        let itemAsText = self.map{ $0.textualDescription }
        return "[" + itemAsText.joined(separator: ", ") + "]"
    }
}