/** * 谨献给Yoyo * * 原文出处:https://www.toptal.com/swift/introduction-protocol-oriented-programming-swift * @author dogstar.huang <chanzonghuang@gmail.com> 2016-12-06 */
协议是Swift编程语言中一个很是强大的特性。html
协议用于定义“符合某个指定任务或者功能片的方法蓝图,属性,以及其余要求”。程序员
Swift在编译时检查协议一致性问题,使得开发者能够在运行程序前发现代码中的一些致命错误。协议使得开发者能够在Swift编写灵活和可扩展的代码而不用妥协该语言的表现力。编程
Swfit经过提供一些最多见奇怪问题的解决方案以及许多其余编程语言的接口限制,进一步得到了使用协议的便利性。
swift
经过面向协议编程,编写灵活、可扩展的Swfit代码。数组
在早期的Swfit版本中,只能扩展类、结构以及枚举类型,在不少现代编程语言里也是这样的。然而,自从Swift 2 开始,也能对协议进行扩展了。安全
此文章会考察在Swfit中协议如何用于编写可重用、可维护的代码,以及经过使用协议扩展如何修改可以让单个小模块合并成一个大型面向协议的代码库。数据结构
什么是协议?app
在其最简单的定义里,协议是指描述某些属性和方法的接口。任何符合协议的类型,都应该使用合适的值填充协议中定义的特定属性,而且实现其必要的方法。例如:编程语言
protocol Queue { var count: Int { get } mutating func push(_ element: Int) mutating func pop() -> Int }
此Queue协议描述了一个队列,它包含了整型的元素。此语法至关明了。spa
在协议块里面,当描述某个属性时,咱们必须指定该属性是只读{ 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) }
协议扩展基于协议自己的威力。这使得咱们能够:
让咱们再来建立一个协议:
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
协议没有描述这个方法是由于它和队列功能不相关。
因此它不是协议方法的默认实现,而是一个新的“装饰”所有符合Queue
协议的类型方法实现。没有协议扩展咱们将不得不分别把这个方法添加到每一个类型上。
协议扩展和使用基类可能看起来至关类似,但使用协议扩展有几点好处。包括但不限于:
除了扩展本身的协议,还能够扩展来自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应用代码库作出修改时更有自信。
咱们但愿这篇文章对你有所帮助,同时也欢迎评论、留言、反馈或者进一步的看法。
------------------------