协议是 Swift 中头等重要的内容,在实际开发中,我们会大量地使用面向协议编程。
方法的默认实现
一个协议中可能会定义许多的属性和方法,有些属性和方法却不是遵循该协议的对象必须实现的,又或者,我们不希望在每个遵循该协议的对象中写入过多重复的代码。
我们可以给协议的属性和方法添加默认的实现来解决上面的问题,我们先来看看方法的默认实现。
第一种方式是使用 @objc 来实现:
@objc protocol Human {
@objc optional func read()
func eat()
}
class Man: Human {
func eat() {}
}
Human 协议定义了两个方法,其中 eat 为必须实现的,而 read 并不是必须的,使用 @objc optional 表明该方法为可选。然后我们让 Man 这个类遵循 Human 协议,只需要实现 eat 方法即可。
第二种实现方式是使用 extension 给协议添加一个默认实现方法:
protocol Human {
func read()
func eat()
}
extension Human {
func read() {}
}
struct Man: Human {
func eat() {}
}
我们在协议扩展中默认实现了 read 方法,因此在结构体 Man 中就无需实现该方法了。
注意这里我将 Man 的类型从 class 改为了 struct ,这样是没有问题的。但是如果我们在第一种实现方式中,将 Man 的类型从 class 改为 struct,编译器就会报错。
因为使用 @objc 标记的协议,只有遵循 NSObject 的 class 类才能遵循该协议,这也是它的局限性所在。比如 struct 、enum 都不能遵循该协议,关联类型更是无法在该协议中使用。
使用 extension 为协议添加默认实现虽然需要多写点代码,但却摆脱了上面的限制,笔者个人习惯使用第二种实现方式,这也是笔者推荐的方式。
属性的默认实现
我们给 Human 添加一个属性 age 和 height,然后添加一个新的结构体 Woman ,代码变成如下:
protocol Human {
var age: Int { get set }
var height: CGFloat { get set }
func read()
func eat()
}
extension Human {
func read() {}
}
struct Man: Human {
func eat() {}
}
struct Woman: Human {
func eat() {}
}
毫不意外,编译器报错了,因为 Man 和 Woman 没有实现协议中的 age 和 height 属性,我们当然可以在 Man 和 Woman 中实现这两个属性,但是如果还有其它遵循 Human 协议的结构体,我们不想在每个结构体中都把这两个属性实现一遍,而是希望能有个默认值。那么该如何实现呢?很遗憾 extension 无法实现我们的需求,我们需要另辟蹊径。
在软件设计中,许多问题都可以通过添加中间层来实现。这里我们可以借鉴这个思路,先定义一个如下结构体将各个属性包装起来:
struct HumanProperties {
var age: Int = 0
var height: CGFloat = 30.0
}
然后我们在 Human 协议中添加一个属性:
protocol Human {
var hp: HumanProperties { get set }
func read()
func eat()
}
然后我们在扩展中添加属性:
extension Human {
var age: Int {
get { hp.age }
set { hp.age = newValue }
}
var height: CGFloat {
get { hp.height }
set { hp.height = newValue }
}
func read() {}
}
然后在遵循 Human 协议的结构体中实现 hp 属性:
struct Man: Human {
var hp = HumanProperties()
func eat() {}
}
struct Woman: Human {
var hp = HumanProperties()
func eat() {}
}
不管 Human 有多少属性要实现,我们都可以使用 HumanProperties 包装起来,并且为之提供了默认值。
方法参数的默认值
我们为 eat 方法添加如下参数:
func eat(breakfast: String, lunch: String, dinner: String)
假设我们现在正在健康饮食阶段,想给 breakfast 和 dinner 添加两个默认值来约束 Man 的行为。
遗憾的是,协议方法并不支持带默认值的参数。
我们可以故技重施,将上面的参数包装起来:
struct Recipe {
var breakfast: String = "Bread & Milk"
var lunch: String
var dinner: String = "Fruits"
}
然后将方法修改为:
func eat(_ food: Recipe)
接着实现该方法:
struct Man: Human {
func eat(_ food: Recipe) {
print(food)
}
}
调用:
var p = Man()
p.eat(.init(lunch: "Rice & Beef & Vegetables"))