Swift 协议属性和方法的默认实现

3,397 阅读3分钟

协议是 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"))