Swift 进阶【九】协议

Swift 的协议和 Objective-C 的协议不同。Swift 协议可以被用作代理,也可以让你对接口进行抽象 (比如 IteratorProtocolSequence )。它们和 Objective-C 协议的最大不同在于我们可以让结构体和枚举类型满足协议。除此之外,Swift 协议还可以有关联类型。我们还可以通过协议扩展的方式为协议添加方法实现。我们会在面向协议编程的部分讨论所有这些内容。

协议允许我们进行动态派发,也就是说,在运行时程序会根据消息接收者的类型去选择正确的方法实现。

在面向对象编程中,子类是在多个类之间共享代码的有效方式。不过在 Swift 中,Sequence 中的代码共享是通过协议和协议扩展来实现的。通过这么做,Sequence 协议和它的扩展在结构体和枚举这样的值类型中依然可用,而这些值类型是不支持子类继承的。

协议扩展是一种可以在不共享基类的前提下共享代码的方法。协议定义了一组最小可行的方法集合,以供类型进行实现。而类型通过扩展的方式在这些最小方法上实现更多更复杂的特性。

面向协议编程


比如在一个图形应用中,我们想要进行两种渲染:我们会将图形使用 Core GraphicsCGContext 渲染到屏幕上,或者创建一个 SVG 格式的图形文件。我们可以从定义绘图 API 的最小功能集的协议开始进行实现:

1
2
3
4
protocol Drawing {
mutating func addEllipse(rect: CGRect, fill: UIColor)
mutating func addRectangle(rect: CGRect, fill: UIColor)
}

协议的最强大的特性之一就是我们可以以追溯的方式来修改任意类型,让它们满足协议。对于 CGContext,我们可以添加扩展来让它满足 Drawing 协议。对于 SVG 我们一样可以通过扩展让它满足 Drawing 协议。

我们现在就可以写出独立于渲染目标的代码了;下面的代码只对 context 变量实现了 Drawing 协议进行了假设。如果我们决定使用 CGContext 来初始化一个 context,我们并不需要改变代码的任何部分:

1
2
3
4
5
6
7
8
9
10
11
12
var context: Drawing = SVG()
let rect1 = CGRect(x: 0, y: 0, width: 100, height: 100)
let rect2 = CGRect(x: 0, y: 0, width: 50, height: 50)
context.addRectangle(rect: rect1, fill: .yellow)
context.addEllipse(rect: rect2, fill: .blue)
context
/*
<svg>
<rect cy="0.0" fill="#010100" ry="100.0" rx="100.0" cx="0.0"/>
<ellipse cy="0.0" fill="#000001" ry="50.0" rx="50.0" cx="0.0"/>
</svg>
*/

协议扩展

Swift 的协议的另一个强大特性是我们可以使用完整的方法实现来扩展一个协议。你可以扩展你自己的协议,也可以对已有协议进行扩展。

通过在扩展中添加 addCircle,我们就可以在 CGContext 和 中使用它了。

1
2
3
4
5
6
7
8
9
extension Drawing {
mutating func addCircle(center: CGPoint, radius: CGFloat, fill: UIColor) {
let diameter = radius * 2
let origin = CGPoint(x: center.x - radius, y: center.y - radius)
let size = CGSize(width: diameter, height: diameter)
let rect = CGRect(origin: origin, size: size)
addEllipse(rect: rect, fill: fill)
}
}

通过协议进行代码共享相比与通过继承的共享,有这几个优势:

  • 我们不需要被强制使用某个父类。
  • 我们可以让已经存在的类型满足协议 (比如我们让 CGContext 满足了 Drawing )。子类就没那么灵活了,如果 CGContext 是一个类的话,我们无法以追溯的方式去变更它的父类。
  • 协议既可以用于类,也可以用于结构体,而父类就无法和结构体一起使用了。
  • 最后,当处理协议时,我们无需担心方法重写或者在正确的时间调用 super 这样的问题。

在协议扩展中重写方法

作为协议的作者,当你想在扩展中添加一个协议方法,你有两种方法。首先,你可以只在扩展中进行添加,就像我们上面 addCircle 所做的那样。或者,你还可以在协议定义本身中添加这个方法的声明,让它成为协议要求的方法。协议要求的方法是动态派发的,而仅定义在扩展中的方法是静态派发的。它们的区别虽然很微小,但不论对于协议的作者还是协议的使用者来说,都十分重要。

首先,我们对 SVG 写一个 addCircle 扩展:

1
2
3
4
5
6
7
8
9
10
11
extension SVG {
mutating func addCircle(center: CGPoint, radius: CGFloat, fill: UIColor) {
var attributes: [String:String] = [
"cx": "\(center.x)",
"cy": "\(center.y)",
"r": "\(radius)",
]
attributes["fill"] = String(hexColor: fill)
append(node: XMLNode(tag: "circle", attributes: attributes))
}
}

现在如果我们创建一个 SVG 实例并调用它的 addCircle 方法,结果将和你期待的一致:编译器将选择 addCircle 的最具体的版本,也就是定义在 SVG 扩展上的版本。

1
2
3
4
5
6
7
8
var sample = SVG()
sample.addCircle(center: .zero, radius: 20, fill: .red)
print(sample)
/*
<svg>
<circle cy="0.0" fill="#010000" r="20.0" cx="0.0"/>
</svg>
*/

如果像下面这样调用的话,它并不会使用 SVGaddCircle 方法。它调用的是 Drawing 中的 addCircle 方法。

1
2
3
4
5
6
7
8
var otherSample: Drawing = SVG()
otherSample.addCircle(center: .zero, radius: 20, fill: .red)
print(otherSample)
/*
<svg>
<ellipse cy="-20.0" fill="#010000" ry="40.0" rx="40.0" cx="-20.0"/>
</svg>
*/

我们可以这样考虑这个行为:当我们对存在容器调用 addCircle 时,方法是静态派发的,也就是说,它总是会使用 Drawing 的扩展。如果它是动态派发,那么它肯定需要将方法的接收者 SVG 类型考虑在内。

想要将 addCircle 变为动态派发,我们可以将它添加到协议定义里:

1
2
3
4
5
protocol Drawing {
mutating func addEllipse(rect: CGRect, fill: UIColor)
mutating func addRectangle(rect: CGRect, fill: UIColor)
mutating func addCircle(center: CGPoint, radius: CGFloat, fill: UIColor)
}

协议的两种类型


带有关联类型的协议和普通的协议是不同的。对于那些在协议定义中在任何地方使用了 Self 的协议来说也是如此。Swift 3 中,这样的协议不能被当作独立的类型来使用。这个限制可能会在今后实现了完整的泛型系统后被移除,但是在那之前,我们都必须要面对和处理这个限制。

类型抹消

我们可以将 Drawing 作为一个类型来使用。但是,对于 IteratorProtocol 来说,因为存在关联类型,这是不可能的 (至少现在还不可能)。编译器会给出这样的错误:“‘ IteratorProtocol ’ 协议含有 Self 或者关联类型,因此它只能被当作泛型约束使用。

1
let iterator: IteratorProtocol = ConstantIterator() // Error

这就是说,将 IteratorProtocol 是一个不完整的类型。我们必须为它指明关联类型,否则单是关联类型的协议是没有意义的。

Swift 团队指出过他们想要支持泛用存在 (generalized existentials)。这个特性将允许那些含有关联类型的协议也可以被当作独立的值来使用,这样它们就可以用来进行类型抹消了。如果你想要了解未来这方面会如何发展,你可以在 Swift 泛型声明一文中找到详细信息。

带有 Self 的协议


带有 Self 要求的协议在行为上和那些带有关联类型的协议很相似。最简单的带有 Self 的协议是 Equatable。它有一个 (运算符形式的) 方法,用来比较两个元素:

1
2
3
protocol Equatable {
static func ==(lhs: Self, rhs: Self) -> Bool
}

要为你自己的类型实现 Equatable 并不难。比如,我们有两个简单的 MonetaryAmount 结构体,我们可以通过比较它们的属性值来比较两个值:

1
2
3
4
5
6
7
8
struct MonetaryAmount: Equatable {
var currency: String
var amountInCents: Int
static func ==(lhs: MonetaryAmount, rhs: MonetaryAmount) -> Bool {
return lhs.currency == rhs.currency &&
lhs.amountInCents == rhs.amountInCents
}
}

我们不能简单地用 Equatable 来作为类型进行变量声明:

1
2
3
// 错误:因为 'Equatable' 中有 Self 或者关联类型的要求,
// 所以它只能被用作泛型约束
let x: Equatable = MonetaryAmount(currency: "EUR", amountInCents: 100)

这个关联类型所面临的问题是一样的:在这个 (不正确) 的声明中,我们并不清楚 Self 到底应该是什么。