11、Swift中的protocol
协议定义了一个蓝图,规定了用来实现某一特定任务或者功能的方法、属性,以及其他需要的东西。类、结构体和枚举都可以遵循协议,并为协议定义的这些要求提供具体实现。某个类型能够满足某个协议的要求,就可以说该类型遵循这个协议。
1、协议的基本用法
1.1 协议的语法
protocol Shapable{}
- 如果让自定义的类型遵循某个协议,在定义类型时,需要在类型名称后面加上协议名称,中间以冒号(:)隔开,如果需要遵循多个协议时,个协议之间用逗号(,)分割:
struct Circle : Shapable,Pointable{}
- 如果自定义类型拥有一个父类,应该将父类名放在遵循协议名之前,以逗号分隔:
class Circle {}
class Segment:Circle,Shapable,Pointable{}
1.2 属性要求
- 属性可以是实例属性和类型属性
- 属性需要使用
var
修饰,不能属于let
- 类型属性只能使用
static
修饰,不能使用class
- 我们需要声明属性必须是可读的或者可读可写的
protocol Shapable{
//这里的协议的定义部分
var area:Int {get}
var radius:Int {get set}
}
1.3 方法要求
- 可以是实例方法或类方法
- 像普通方法一样放在协议定义中,但不需要大括号和方法体
- 协议中不支持为协议中的方法提供默认参数
- 协议中的类方法也只能使用static关键字作为前缀,不能使用class
- 可以使用mutating提供异变方法,以使用该方法时修改实体的属性等。
- 可以定义构造方法,但是使用的时候需要使用required关键字
protocol Shapable{
var point:(Double,Double){get set}
func calculateArea()->Double
static func printName()->String
init()
}
class Rectangle:Shapable {
var point: (Double, Double) {
get {
return (x,y)
}
set {
self.x = newValue.0
self.y = newValue.1
}
}
var x:Double
var y:Double
var width:Double
var height:Double
required init(){
self.x = 0
self.y = 0
self.width = 0
self.height = 0
}
init(point:(Double,Double),width:Double,height:Double){
self.x = point.0
self.y = point.1
self.width = width
self.height = height
}
func calculateArea() -> Double {
return width*height
}
static func printName() -> String {
return "矩形"
}
}
1.4 protocol中使用extension
可以在extention中给protocol中定义的变量和函数增加一个默认实现。
protocol Shapable{
var point:(Double,Double){get set}
func calculateArea()->Double
static func printName()->String
init()
}
extension Shapable {
var point:(Double,Double){
return (0,0)
}
}
如果直接把属性或者方法定义在extension中,那么这个方法或者属性将会遵循直接派发的规则。
protocol Shapable{
// var point:(Double,Double){get set}
func calculateArea()->Double
static func printName()->String
init()
}
extension Shapable {
var point:(Double,Double){
return (0,0)
}
}
class Rectangle:Shapable {
var point: (Double, Double) {
get {
return (x,y)
}
set {
self.x = newValue.0
self.y = newValue.1
}
}
var x:Double
var y:Double
var width:Double
var height:Double
required init(){
self.x = 0
self.y = 0
self.width = 0
self.height = 0
}
init(point:(Double,Double),width:Double,height:Double){
self.x = point.0
self.y = point.1
self.width = width
self.height = height
}
func calculateArea() -> Double {
return width*height
}
static func printName() -> String {
return "矩形"
}
}
let rect:Shapable = Rectangle(point:(10,10),width: 100, height: 100)
print(rect.calculateArea())
print(rect.point) //(0,0)
1.5 如何在协议中定义可选方法
1.方法一: 使用@objc修饰的protocol。缺点:只能被class类型实现了。
@objc protocol OptionalProtocol {
@objc optional func optionalMethod() //可选
func necessaryMethod() //必须
}
2.方法二:在Swift2.0之后,可以声明一个protocol后,在使用extension给出部分默认实现。这样的方法在实际的类中就是可选实现了
protocol OptionalProtocol {
func optionalMethod() //可选
func necessaryMethod() //必须
}
extension OptionalProtocol {
func optionalMethod() {}
}
1.6 swift中如何避免代理导致的循环引用?
通过添加class
关键字来限制协议只能被类类型遵守,而结构体不能遵循该协议。class
关键字必须第一个出现在协议的继承列表中。
protocol LoopVievDelegate:class {}
class TestVC:UIViewController {
weak var delegate:LoopVievDelegate?
}
将delegate使用weak修饰,var delegate前面加上weak,编译器会报错,这是因为swift遵守protocol类型很多,其中有些类型不支持weak修饰,比如struct结构体这里需要限制protocol只能被类实现,让protocol继承自class.
为什么结构体不支持weak呢?值类型不存在循环引用的问题,持有的都是副本。如果struct被weak修饰,就意味着没有强引用,按照ARC的基本原则,struct结构体会被直接销毁。
1.7 为什么delegate模式不适用于struct类型?
protocol FinishAlertViewDelegate {
mutating func buttonPressed(at Index: Int)
}
class FinishAlertView {
var delegate: FinishAlertViewDelegate?
func goToTheNext() {
delegate?.buttonPressed(at: 1)
}
}
struct PressCounter: FinishAlertViewDelegate {
var count = 0
mutating func buttonPressed(at Index: Int) {
self.count += 1
}
}
class EpisodeViewController {
var episodeAlert: FinishAlertView!
var counter: PressCounter!
init() {
self.episodeAlert = FinishAlertView()
self.counter = PressCounter()
self.episodeAlert.delegate = self.counter
}
}
let evc = EpisodeViewController()
evc.episodeAlert.goToTheNext()
evc.episodeAlert.goToTheNext()
evc.episodeAlert.goToTheNext()
evc.episodeAlert.goToTheNext()
evc.episodeAlert.goToTheNext()
evc.episodeAlert.goToTheNext()
evc.counter.count // Still 0
在我们模拟了6次点击事件之后,evc.counter.count仍然是0,因为PressCounter是一个值类型,当我们执行self.episodeAlert.delegate = self.counter,delegate实际上是self.counter的拷贝,它们引用的并不是同一个推向,调用goTotheNext()修改的是拷贝的值,原来的值没有发生变化。调用(evc.episodeAlert.delegate as! PressCounter).count得到的结果是6.
1.8 为协议扩展添加限制条件
在扩展协议的时候,可以指定一些限制条件,只有遵循协议的类型满足这些限制条件时,才能获得协议扩展提供的默认实现。这些限制写在协议名之后,使用where子句来描述。
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
associatedtype Iterator: IteratorProtocol where Iterator.Element == Item
func makeIterator() -> Iterator
}
以下是在KingFisher里面抽离出来的一段代码:
//定义一个带有泛型的类
public final class Kingfisher<Base> {
public let base:Base
public init(_ base:Base) {
self.base = base
}
}
//只有Base是UIImageView类型的才能调用下面的方法
extension Kingfisher where Base : UIImageView {
public func setImage() {
print("设置图片")
}
}
1.9 协议方法中出现Self该怎么处理
protocol Copyable {
func copy()->Self
}
Self表示返回自己,由于声明协议的时候,我们并不知道究竟会是什么样的类型来实现这个协议,而在声明协议时,我们希望在协议中使用的类型就是实现这个协议本身的类型的话,就需要使用Self进行指代。但是在这种情况下,Self指代的不仅是实现协议的类型本身,也是该类型的子类。
- 1.错误实现方法
class MyClass:Copyable {
var num = 1
func copy()->Self {
let result = MyClass()
result.num = num
return result
}
}
协议中的方法要求返回一个抽象的、表示当前类型的Self,但是我们返回了真实类型MyClass,者将导致无法编译。
- 2.正确实现方法:
class MyClass:Copyable {
var num = 1
func copy()->Self {
let result = type(of:self).init()
result.num = num
return result
}
required init() {
}
}
1.10 如何在协议中使用associatedtype定义占位符
1、有时候我们在定义协议方法时,不希望指定具体的类型,可以使用associatedtype关键来定义占位符,让实现该协议的类型来指定具体的类型
2、swift的protocol在添加了associatedtype和Self之后,就不能当做独立的类型使用了。主要是因为Swift需要在编译时确定所有的类型,这里因为Animal包含了一个不确定的类型,所以随着Animal本身类型的变化,其中F将无法确定。在一个协议中加入了associatedtype或者Self的约束后,它将只能用作泛型约束,而不能当做独立类型的占位使用,也失去了动态派发的特性。在这种情况下,只能将函数改写为泛型,如上面的isDangerous函数。
protocol Food{}
protocol Meat {}
protocol Grass:Food {}
protocol Animal {
var numberOfLegs:Int{get}
var weight:Double{get set}
associatedtype F //定义占位符
func eat(_ food:F)
mutating func changeWeight()
}
struct Tiger:Animal {
var numberOfLegs: Int {return 4}
var weight: Double = 100
typealias F = Meat
func eat(_ food: Meat) {
print("吃肉")
}
mutating func changeWeight() {
print("吃完肉变胖了")
self.weight = 200
}
}
struct Sheep:Animal {
var numberOfLegs: Int = 4
var weight: Double = 30
typealias F = Grass
func eat(_ food: Grass) {
print("羊吃草")
}
mutating func changeWeight() {
print("杨吃完草就胖了")
self.weight = 50
}
}
func isDarangerous<T:Animal>(animal:T)->Bool? {
if animal is Tiger {
return true
}else if animal is Sheep{
return false
}else{
return nil
}
}
1.11 其他一些Tips
通过扩展遵守协议,当一个类型已经符合了某个协议中的所有要求,却还没有声明遵守该协议时,可以通过空扩展体来遵守该协议。
extension Image: KingfisherCompatible {}
这样一个空拓展体,就让Image遵守KingfisherCompatible协议了。
2.协议可以要求遵循协议的类型实现指定的构造函数。实现构造函数时,需要在前面加上required修饰符,使用required修饰符可以确保所有的子类也实现此构造函数。
如果一个子类重写了父类的指定构造函数,并且构造函数满足了某个协议的要求,那么该构造函数需要同时标注required和overried修饰符。
2、从隐式接口和编译期多态说起
自从有了泛型编程的概念,人们就开始不断尝试用这种方式去抽象一个算法或数据结构需要的核心接口集合。这和我们在面向对象编程中设计一个类接口的行为类似,却又有着根本的不同。
2.1 面向对象的方式
在面向对象的世界里,经常会发生下面这样的事情。假设我们有一个驾驶汽车的函数,它接受一个Car类型的参数,通过查看Car的文档我们知道,它有selfCheck / startEngine / shiftUp / go这4个方法:
func drive(_ car: Car) {
if !car.selfCheck() {
car.startEngine()
car.shiftUp()
car.go()
}
}
但是通常,一个类支持的方法要比我们在某个具体的业务逻辑中使用的丰富:
class Car {
func selfCheck() -> Bool {
return true
}
func startEngine() {}
func shiftUp() {}
func go() {}
func lightUp() {}
func horn() {}
func lock() {}
// ...
}
因此,对于drive这个算法来说,虽然它只要求它的参数支持“自检”、“启动引擎”、“升挡”以及“前进”就好了。但是由于在声明里,参数的类型被定义成了Car,因此,无论需要与否,它必须是一个完整的Car类型。所谓完整,就是严格按照Car的规格包含所有的init,deinit以及Car的所有方法和属性。
因此,即便一个类型实现了drive中的所有方法,但是只要它不在Car的继承体系里,drive就无法正常工作。我们管这种面向对象的方式约定的接口,叫做explicit interface。
并且,由于Car是一个class,当我们传递Car的不同派生类时,各种方法的调用会在运行时被动态派发,这就是我们熟悉的运行时多态。
2.2 泛型编程的思维
protocol Drivable{
func selfCheck()->Bool
func startEngine()
func shiftUp()
func go()
}
func drive<T:Drivable>(_ car:T){
if !car.selfCheck(){
car.startEngine()
car.shiftUp()
car.go()
}
}
从字面上看,意思是说,drive接受一个T类型的参数,只要这个类型支持了“自检”、“启动引擎”、“升挡”以及“前进”这4个操作,就可以把车开走。当然,和C++中的泛型编程不同,在语法上,Swift要求我们明确把刚才这个要求表达出来,而不能仅仅通过drive的实现隐式表达这个要求。
现在,对于drive这个算法来说,它唯一的要求,就是类型支持算法需要的4个方法就好了,至于这个类型的对象如何初始化,如何被回收,有什么属性,统统没有约定。因此,我们管通过泛型方式约定的接口,叫做implicit interface。
然后,假设现在我们有两个独立的class,表示两类不同的汽车:
class Roadster:Drivable {
func selfCheck() -> Bool {
return true
}
func startEngine() {}
func shiftUp() {}
func go() {}
}
class SUV:Drivable {
func selfCheck() -> Bool {
return true
}
func startEngine() {}
func shiftUp() {}
func go() {}
}
当我们分别对它们的对象调用drive时:
drive(Roadster())
drive(SUV())
和面向对象中的运行时多态不同,泛型编程中调用方法的选择是在编译期完成的。编译器会根据参数的类型在正确的类中选择要调用的方法。这种行为,叫做编译期多态。
3、编译器是如何理解面向protocol编程的
从某种意义上说,protocol也算是某种形式的“泛型”类型,只不过所有的类型都需要遵从一些共同的行为约束。通过protocol,无关的两个值类型也可以实现类似多态的效果,来看下面这个例子。
protocol Drawable {
func draw()
}
struct Point:Drawable {
var x:Int
var y:Int
func draw() {
print("A point at (x:\(x),y:\(y))")
}
}
struct Line:Drawable {
var x1:Int
var y1:Int
var x2:Int
var y2:Int
func draw() {
print("A line from (x:\(x1),y:\(y1))" + " to (x:\(x2),y:\(y2))")
}
}
这里,Point和Line是完全无关的两个类型。然后,我们看下面的测试代码:
let point:Drawable = Point(x: 1, y: 1)
point.draw() //A point at (x:1,y:1)
let line:Drawable = Line(x1: 2, y1: 2, x2: 10, y2: 10)
line.draw() //A line from (x:2,y:2) to (x:10,y:10)
其实这就是我们经常说起的protocol oriented programming。它让value类型也有了实现多态的能力。
Protocol witness table
Point和Line并没有继承关系,我们无法通过之前讲过的witness table的机制实现方法的动态派发,编译器是如何为这两个类型的对象选择方法的呢?
实际上,对于每一个实现了protocol的类型,编译器都会创建一个叫做protocol witness table的对象,其中存放了这个类型实现的每一个protocol方法的地址。因此,对于我们的Point和Line来说,这个表是这样的: