Swift系列十六 - 协议

190 阅读5分钟

协议在Swift中极其重要,可以说任何项目开发都会用到协议。

一、协议(Protocol)的定义

协议可以用来定义方法、属性、下标的声明,协议可以被枚举、结构体、类遵守(多个协议之间用逗号隔开)。

示例代码:

// 定义协议
protocol Drawable {
    var x: Int { get set }
    var y: Int { get }
    func draw()
    subscript(index: Int) -> Int { get set }
}
protocol Test1 { }
protocol Test2 { }
protocol Test3 { }

// 遵守协议
class TestClass: Test1, Test2, Test3, Drawable {
  // do something...
}

特点:

  • 协议中定义方法时不能有默认参数值;
  • 默认情况下,协议中定义的内容必须全部都实现。

二、协议中的属性

协议中定义属性时必须用var关键字。

示例代码:

protocol Drawable {
    var x: Int { get set }
    var y: Int { get }
    func draw()
    subscript(index: Int) -> Int { get set }
}

上面示例代码中协议属性xy并不是计算属性的意思,它的意思仅仅是表达该属性是具备可读/可写/可读写功能。

协议属性的实现: 实现协议时的属性权限要【不小于】协议中定义的属性权限:

  • 协议定义getset,用var存储属性 或 getset计算属性去实现;
  • 协议定义get,用任何属性都可以实现。

场景一:

class Person: Drawable {
    // 可读写的存储属性
    var x: Int = 0
    // 只读的存储属性
    let y: Int = 0
    func draw() {
        print("Person draw")
    }
    subscript(index: Int) -> Int {
        set { }
        get { index }
    }
}

场景二:

class Person: Drawable {
    // 可读写的计算属性
    var x: Int {
        set { }
        get { 0 }
    }
    // 只读的计算属性
    var y: Int { 0 }
    func draw() {
        print("Person draw")
    }
    subscript(index: Int) -> Int {
        set { }
        get { index }
    }
}

三、static、class、mutating、init在协议中的使用

3.1. static、class

为了保证通用,协议中必须用static定义类型方法,类型属性、类型下标。

示例代码:

protocol Drawable {
    static func draw()
}

实现协议的时候既可以使用static,也可以使用class,取决于子类是否需要重写。

class Person1: Drawable {
    static func draw() {
        print("Person1 draw")
    }
}

class Person2: Drawable {
    class func draw() {
        print("Person2 draw")
    }
}

3.2. mutating

只有将协议中的实例方法标记为mutating,才允许结构体、枚举的具体实现修改自身内存。

类在实现方法时不加mutating(加了会报错),枚举、结构体才需要加mutating(不加会报错)。

示例代码:

protocol Drawable {
    mutating func draw()
}

class Size: Drawable {
    var width: Int = 0
    func draw() {
        width = 10
    }
}

struct Point: Drawable {
    var x: Int = 0
    mutating func draw() {
        x = 10
    }
}

如果协议中没有加mutating,不影响class修改属性值。但是值类型实现协议函数时也不能加mutating,否则报错。

3.3. init

  1. 协议中还可以定义初始化器init,非final类实现时必须加上required

示例代码:

protocol Drawable {
    init(x: Int, y: Int)
}

class Point: Drawable {
    required init(x: Int, y: Int) {
        
    }
}

final class Size: Drawable {
    init(x: Int, y: Int) {
        
    }
}

思考:为什么协议限制非final类必须加上required?因为协议肯定希望定义的初始化器被遵守协议的类及其子类都能实现,所以需要加上required,遵守协议的子类也必须实现该协议。但是加上final的类是不能被继承的,所以也就没必要加required

  1. 如果从协议实现的初始化器,刚好是重写了父类的指定初始化器,那么这个初始化必须同时加上requiredoverride

示例代码:

protocol Animal {
    init(age: Int)
}

class Person {
    init(age: Int) { }
}

class Student: Person, Animal {
    required override init(age: Int) {
        super.init(age: age)
    }
}

注意:子类重写父类的required指定初始化器,子类不用加override,和是否遵守协议无关。上面的示例中required代表遵守协议,override代表重写父类。

init、init?、init!的使用:

  • 协议中定义的init?init!,可以用initinit?init!去实现;
  • 协议中定义的init,可以用initinit!去实现。

示例代码:

protocol Animal {
    init()
    init?(age: Int)
    init!(height: Int)
}

class Person: Animal {
    required init() { }
    // required init!() { }
    
    required init?(age: Int) { }
    // required init!(age: Int) { }
    // required init(age: Int) { }
    
    required init!(height: Int) { }
    // required init?(height: Int) { }
    // required init(height: Int) { }
    
}

注意:在继承关系中,可以用一个非可失败初始化器重写一个可失败初始化器,但反过来是不行的。但是实现协议初始化器时,可以用且只能用隐式解包的可失败初始化器。

四、协议的继承和组合

4.1. 协议继承

一个协议可以继承其他协议(也可以说是遵守)。

示例代码:

protocol Runnable {
    func run()
}

protocol Livable: Runnable {
    func breath()
}

class Person: Livable {
    func breath() { }
    func run() { }
}

4.2. 协议组合

  • 协议组合最多可以包含1个类类型
  • 多个协议和类类型用&连接

示例代码:

// 定义协议和类
protocol Runnable { }
protocol Livable: Runnable { }
class Person: Livable { }

// 接收Person或者其子类的实例
func fn0(obj: Person) { }
// 接收遵守Livable协议的实例
func fn1(obj: Livable) { }
// 接收同时遵守Livable、Runnable协议的实例
func fn2(obj: Livable & Runnable) { }
// 接收同时遵守Livable、Runnable协议、并且是Person或者其子类的实例
func fn3(obj: Person & Livable & Runnable) { }

上面示例代码中fn2fn3就是协议组合。

fn3还可以使用下面的方式:

typealias RealPerson = Person & Livable & Runnable
func fn4(obj: RealPerson) { }

五、常用协议

5.1. CaseIterable协议(枚举迭代器)

让枚举遵守CaseIterable协议,可以实现遍历枚举值。

示例代码:

enum Season: CaseIterable {
    case spring, summer, autumn, winter
}
let seasons = Season.allCases
print(type(of: seasons))
// 输出:Array<Season>
print(seasons.count)
// 输出:4
for season in seasons {
    print(season)
}
/*
 输出:
 spring
 summer
 autumn
 winter
 */

CaseIterable提供了一个allCases的类型属性,返回一个数组,数组包含了枚举的所有值。

扩展:let seasons = Season.allCases等价于let seasons = [Season.spring, Season.summer, Season.autumn, Season.winter]

5.2. CustomStringConvertible/CustomDebugStringConvertible

遵守CustomStringConvertibleCustomDebugStringConvertible协议,都可以自定义实例的打印字符串。

示例代码:

class Person: CustomStringConvertible, CustomDebugStringConvertible {
    var age = 0
    var description: String {
        "person_\(age)"
    }
    var debugDescription: String {
        "debug_person_\(age)"
    }
}
var p = Person()
print(p) // 输出:person_0
debugPrint(p) // 输出:debug_person_0

  • print调用的是CustomStringConvertible协议的description
  • debugPrintpo调用的是CustomDebugStringConvertible协议的debugDescription