Swift面向协议编程简介
/** * 谨献给Yoyo * * 原文出处:https://www.toptal.com/swift/introduction-protocol-oriented-programming-swift * @author dogstar.huang <chanzonghuang@gmail.com> 2016-12-06 */
协议是Swift编程语言中一个非常强大的特性。
协议用于定义“符合某个指定任务或者功能片的方法蓝图,属性,以及其他要求”。
Swift在编译时检查协议一致性问题,使得开发者可以在运行程序前发现代码中的一些致命错误。协议使得开发者可以在Swift编写灵活和可扩展的代码而不用妥协该语言的表现力。
Swfit通过提供一些最常见奇怪问题的解决方案以及许多其他编程语言的接口限制,进一步获得了使用协议的便利性。
通过面向协议编程,编写灵活、可扩展的Swfit代码。
在早期的Swfit版本中,只能扩展类、结构以及枚举类型,在很多现代编程语言里也是这样的。然而,自从Swift 2 开始,也能对协议进行扩展了。
此文章会考察在Swfit中协议如何用于编写可重用、可维护的代码,以及通过使用协议扩展如何修改可让单个小模块合并成一个大型面向协议的代码库。
协议
什么是协议?
在其最简单的定义里,协议是指描述某些属性和方法的接口。任何符合协议的类型,都应该使用合适的值填充协议中定义的特定属性,并且实现其必要的方法。例如:
protocol Queue { var count: Int { get } mutating func push(_ element: Int) mutating func pop() -> Int }
此Queue协议描述了一个队列,它包含了整型的元素。此语法相当明了。
在协议块里面,当描述某个属性时,我们必须指定该属性是只读{ get }
还是既可读又可写{ get set }
。这里,变量Count(类型为Int
)是只读的。
如果某个协议要求有一个读写的属性,那就不能用一个存放常量的属性或者一个只读的计算值来填充。
如果协议只要求属性是可读的,那么可以是任意类型的属性,并且该属性也可以是可写的,如果这对于你的代码有用的话。
对于在协议里定义的方法,使用关键词mutating
指明该方法将会改变上下文是非常重要的。除此之外,方法的签名足以作为定义。
为了符合协议,类型必须提供全部实例属性以及实现在协议中描述的全部方法。例如下面,是一个符合我们Queue
协议的Container
结构。此结构本质上保存压入的Int
到一个私有的items
数组。
struct Container: Queue { private var items: [Int] = [] var count: Int { return items.count } mutating func push(_ element: Int) { items.append(element) } mutating func pop() -> Int { return items.removeFirst() } }
然而,我们当前的Queue协议有一堆缺点。
仅有处理Int
的容器才能符合此协议。
我们可以通过使用“关联类型”特性来去掉这个限制。关联类型的工作方式类似泛型。为了演示,让我们修改Queue协议以采用关联类型:
protocol Queue { associatedtype ItemType var count: Int { get } func push(_ element: ItemType) func pop() -> ItemType }
现在此Queue协议允许储存任意类型的元素了。
在这个Container
结构的实现里,编译器会根据上下文(例如方法返回类型和参数类型)决定此关联类型。这种方式使得我们可以创建一个带有泛型元素的Container
结构。例如:
class Container<Item>: Queue { private var items: [Item] = [] var count: Int { return items.count } func push(_ element: Item) { items.append(element) } func pop() -> Item { return items.removeFirst() } }
在很多情况下,使用协议都可以简化代码的编写。
例如,任何表示错误的对象都会符合Error
(或者LocalizedError
,以防我们想提供本地化的描述)协议。
然后在你的代码里,处理错误的相同逻辑就可以应用到任意这些错误对象上。因此,你不需要使用任何指定的对象来表示错误,用任何符合Error
或LocalizedError
协议的就可以了。
你甚至可以扩展String类型,让它符合LocalizedError
协议并且把字符串当作错误抛出。
extension String: LocalizedError { public var errorDescription: String? { Return NSLocalizedString(self,comment:””) } } throw “Unfortunately something went wrong” func handle(error: Error) { print(error.localizedDescription) }
协议扩展
协议扩展基于协议本身的威力。这使得我们可以:
- 1、提供协议方法的默认实现和协议属性的默认值,从而使得它们“可选”。符合协议的类型可以提供他们自己的实现或者使用默认提供的。
- 2、添加不在协议描述里的额外方法并且使用这些额外的方法“装饰”任意符合此协议的类型。这个特性允使我们添加指定方法到已经符合协议的混合类型上而不需要单独修改每一个类型。
默认的方法实现
让我们再来创建一个协议:
rotocol ErrorHandler { func handle(error: Error) }
这个协议描述了负责处理应用中出现的错误的对象。例如:
struct Handler: ErrorHandler { func handle(error: Error) { print(error.localizedDescription) } }
这里我们只是打印了错误的本地化描述。使用协议扩展我们可以让这个实现成为默认的实现。
extension ErrorHandler { func handle(error: Error) { print(error.localizedDescription) } }
通过提供一个默认的实现,这样使得handle
方法变成可选。
使用默认的行为来扩展已经存在的协议的能力相当强大,这使得协议可以发展和扩展,而不需要担心破坏既有代码的兼容性。
条件扩展
我们已经提供了handle
方法的默认实现,但打印到控制台对于终端用户一点用都没有。
当错误处理器是一个视图控制器时,我们可能更倾向通过本地化描述来显示某些排序好的警告视图给他们看。为了做到这一点,可以扩展ErrorHandle
协议,但限制此扩展只用于既定的场景(例如,当类型是视图控制器时)。
Swift允使我们使用where
关键字添加这样的条件到协议扩展里。
extension ErrorHandler where Self: UIViewController { func handle(error: Error) { let alert = UIAlertController(title: nil,message: error.localizedDescription,preferredStyle: .alert) let action = UIAlertAction(title: "OK",style: .cancel,handler: nil) alert.addAction(action) present(alert,animated: true,completion: nil) } }
在上面代码片段里的Self(“S”大写)是指类型(结构、类或枚举)。通过这样指定后我们只能为继承于UIViewController
的类型扩展此协议,我们可以使用UIViewController
指定方法(例如present(viewControllerToPresnt: animated: completion)
)。
现在, 任何符合ErrorHandler
协议的视图控制器都拥有了handle
方法的默认实现,即显示带有本地化描述的警告视图。
不明确的方法实现
假设这里有两个协议,两个都有一个方法,并且签名一样。
protocol P1 { func method() //some other methods } protocol P2 { func method() //some other methods }
这两个协议都扩展了这个方法的默认实现。
extension P1 { func method() { print("Method P1") } } extension P2 { func method() { print("Method P2") } }
假设这里有一个类型,符合这两个协议。
struct S: P1,P2 { }
在这里,我们遇到了一个问题:不明确的方法实现。此类型没有清楚指明它应该使用哪个方法的实现。结果,我们得到了一个编译错误。为了修复这点,我们需要添加这个方法的实现到此类型里。
struct S: P1,P2 { func method() { print("Method S") } }
许多面向对象编程语言困扰于围绕歧义扩展定义解决方案的限制。通过允使程序员在编译器快速失败时取得控制权,Swift相当优雅地处理了这一点。
添加新方法
再来看多一眼Queue
这个协议。
protocol Queue { associatedtype ItemType var count: Int { get } func push(_ element: ItemType) func pop() -> ItemType }
每个符合此Queue
协议的类型都有一个定义所存放元素数量的count
实例属性。这让我们,除其他事项外,可以比较这样的类型以决定哪个更大。可以通过协议扩展来添加这样的方法。
extension Queue { func compare<Q>(queue: Q) -> ComparisonResult where Q: Queue { if count < queue.count { return .orderedDescending } if count > queue.count { return .orderedAscending } return .orderedSame } }
所以它不是协议方法的默认实现,而是一个新的“装饰”全部符合Queue
协议的类型方法实现。没有协议扩展我们将不得不分别把这个方法添加到每个类型上。
协议扩展 vs 基类
协议扩展和使用基类可能看起来相当相似,但使用协议扩展有几点好处。包括但不限于:
- 1、因为类、结构和枚举可以符合多个协议,他们可以接收多种协议的默认实现。这在概念上和其他语言里的多继承相类似。
- 2、协议可以被类、结构和枚举采用,而基类和继承只能用于类。
Swift标准库扩展
除了扩展自己的协议,还可以扩展来自Swift标准库的协议。例如,如果想找到队列集合里的平均值,可以通过扩展标准的Collection
协议来做到这一点。
由Swfit标准库提供的序列化数据结构,其元素可以通过索引下标进行遍历和访问,通常符合Collection
协议。通过协议扩展,可以扩展全部这些标准库数据结构或者有选择性地只扩展一部分。
注意:此协议在Swfit 2.x 以前叫
CollectionType
,而在Swfit 3 里已改名为Collection
。
extension Collection where Iterator.Element: Queue { func avgSize() -> Int { let size = map { $0.count }.reduce(0,+) return Int(round(Double(size) / Double(count.toIntMax()))) } }
现在可以统计任何队列集合(Array
,Set
等)的平均值了。没有协议扩展的话,我们需要为每个集合类型分别添加这个方法。
在Swfit标准库里,协议扩展用于实现,例如,这样的方法诸如:map
,filter
,reduce
等。
extension Collection { public func map<T>(_ transform: (Self.Iterator.Element) throws -> T) rethrows -> [T] { } }
协议扩展和多态
正如我曾经说过的,协议扩展使得我们可以为某些方法添加默认实现,也可以添加新的方法实现。但这两种特性有什么区别呢?让我们回到前面的错误处理器,找出答案。
protocol ErrorHandler { func handle(error: Error) } extension ErrorHandler { func handle(error: Error) { print(error.localizedDescription) } } struct Handler: ErrorHandler { func handle(error: Error) { fatalError("Unexpected error occurred") } } enum ApplicationError: Error { case other } let handler: Handler = Handler() handler.handle(error: ApplicationError.other)
结果是一个致命错误。
现在删除声明在协议里的handle(error: Error)
方法。
protocol ErrorHandler { }
结果还是一样:一个致命错误。
这是不是意味着,为协议方法添加默认实现和为协议添加新的方法实现,没有什么不同?
不!还是有区别的,如果把handler
变量的类型从Handler
改成ErrorHandler
,你就能出来了。
let handler: ErrorHandler = Handler()
现在输出到控制台的是:The operation couldn’t be completed. (ApplicationError error 0.)
但如果把handle(error: Error)
这个方法还原到协议的声明里的话,结果又会变回到致命错误。
protocol ErrorHandler { func handle(error: Error) }
一起来看下在各个场景中依次发生了什么。
当协议存在方法声明时:
协议声明了handle(error: Error)
方法并且提供了一个默认的实现。此方法在Handler
实现中被重载。所以,此方法的正确实现将会在运行时被调用,不管是什么类型的变量。
当协议不存在方法声明时:
因为这个方法没有声明在协议里,类型不能对它进行重载。那就是为什么一个被调用的方法的实现依赖于变量的类型。
如果变量的类型是Handler
,来自该类型的方法实现将会被调用。如果变量的类型是ErrorHandler
,来自该协议扩展的方法将会被调用。
面向协议的代码:安全而富有表现力
在这篇文章中,我们演示了一些在Swift里协议扩展的强大之处。
不像其他使用接口的编程语言,Swift没有用不必要的限制来约束协议。Swift通过允许开发人员根据需要解决歧义来解决这些编程语言常见的问题。
使用Swift协议和协议扩展,可以编写出像大部分动态编程语言富有表现力且在编译时类型安全的代码。这使得你可以确保代码的可重用性和可维护性,以及在对Swift应用代码库做出修改时更有自信。
我们希望这篇文章对你有所帮助,同时也欢迎评论、留言、反馈或者进一步的见解。
------------------------
- 本作品采用知识共享署名-非商业性使用-相同方式共享 3.0 未本地化版本许可协议进行许可。
- 本文翻译作者为:dogstar,发表于艾翻译(itran.cc);欢迎转载,但请注明出处,谢谢!