本文概述
协议是Swift编程语言的一项非常强大的功能。
协议用于定义”适合特定任务或功能的方法, 属性和其他要求的蓝图”。
Swift会在编译时检查协议一致性问题, 从而允许开发人员甚至在运行程序之前就发现代码中的一些致命错误。协议使开发人员可以在Swift中编写灵活且可扩展的代码, 而不必牺牲语言的表现力。
Swift提供了解决许多其他编程语言困扰的一些最常见的怪癖和接口限制的方法, 从而进一步方便了使用协议。
使用面向协议的编程在Swift中编写灵活且可扩展的代码。
鸣叫
在早期的Swift版本中, 可能仅扩展类, 结构和枚举, 这在许多现代编程语言中都是如此。但是, 从Swift版本2开始, 也可以扩展协议。
本文探讨了如何使用Swift中的协议来编写可重用和可维护的代码, 以及如何通过使用协议扩展将对大型面向协议的代码库的更改合并到单个位置。
通讯协定
什么是协议?
在最简单的形式中, 协议是描述某些属性和方法的接口。符合协议的任何类型都应使用适当的值填充协议中定义的特定属性, 并实现其必需的方法。例如:
protocol Queue {
var count: Int { get }
mutating func push(_ element: Int)
mutating func pop() -> Int
}
队列协议描述了一个队列, 其中包含整数项。语法非常简单。
在协议块内, 当我们描述一个属性时, 必须指定该属性是仅gettable {get}还是gettable和settable {get set}。在我们的例子中, 变量Count(类型为Int)仅是可获取的。
如果协议要求某个属性是可获取和可设置的, 则不能通过常量存储属性或只读计算属性来满足该要求。
如果协议仅要求一个属性是可获取的, 则该要求可以由任何种类的属性来满足, 并且该属性对于可设置也是有效的(如果这对你自己的代码很有用)。
对于协议中定义的功能, 重要的是使用mutating关键字指示该功能是否会更改内容。除此之外, 函数的签名就足以作为定义。
为了符合协议, 类型必须提供所有实例属性并实现协议中描述的所有方法。例如, 下面是符合我们的Queue协议的结构容器。该结构实质上将推入的Ints存储在私有数组项中。
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协议有一个主要缺点。
只有处理Ints的容器才能符合此协议。
我们可以使用”关联类型”功能来消除此限制。关联类型像泛型一样工作。为了演示, 让我们更改队列协议以利用关联的类型:
protocol Queue {
associatedtype ItemType
var count: Int { get }
func push(_ element: ItemType)
func pop() -> ItemType
}
现在, 队列协议允许存储任何类型的项目。
在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, 如果我们要提供本地化的描述, 则)协议。
然后, 可以在整个代码中将相同的错误处理逻辑应用于这些错误对象中的任何一个。因此, 你不需要使用任何特定的对象(如Objective-C中的NSError)来表示错误, 可以使用符合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)
}
协议扩展
协议扩展基于协议的强大功能。他们使我们能够:
-
提供协议方法的默认实现和协议属性的默认值, 从而使其成为”可选”。符合协议的类型可以提供自己的实现, 也可以使用默认的实现。
-
添加协议中未描述的其他方法的实现, 并使用这些其他方法”修饰”符合协议的任何类型。此功能使我们可以向已经符合该协议的多种类型添加特定的方法, 而不必分别修改每种类型。
默认方法实施
让我们再创建一个协议:
protocol 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方法的默认实现, 但是打印到控制台对最终用户并不是很有帮助。
在错误处理程序是视图控制器的情况下, 我们可能希望向他们显示带有本地化描述的某种警报视图。为此, 我们可以扩展ErrorHandler协议, 但可以将扩展限制为仅适用于某些情况(即, 当类型是视图控制器时)。
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通过允许程序员控制编译器不足之处的协议扩展, 非常优雅地处理了这一问题。
添加新方法
让我们再来看一次队列协议。
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协议的类型。没有协议扩展, 我们将不得不将此方法分别添加到每种类型。
协议扩展与基类
协议扩展似乎与使用基类非常相似, 但是使用协议扩展有很多好处。这些包括但不一定限于:
-
由于类, 结构和枚举可以符合多个协议, 因此它们可以采用多种协议的默认实现。从概念上讲, 这类似于其他语言中的多重继承。
-
协议可以被类, 结构和枚举所采用, 而基类和继承仅适用于类。
Swift标准库扩展
除了扩展自己的协议之外, 你还可以从Swift标准库中扩展协议。例如, 如果我们想找到队列集合的平均大小, 可以通过扩展标准的Collection协议来实现。
Swift的标准库提供的序列数据结构通常可以遵循Collection协议, 该数据结构的元素可以通过索引下标进行遍历和访问。通过协议扩展, 可以扩展所有此类标准库数据结构或有选择地扩展其中的一些。
注意:该协议在Swift 2.x中以前称为CollectionType, 在Swift 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())))
}
}
现在, 我们可以计算任何队列集合(数组, 集合等)的平均大小。如果没有协议扩展, 我们将需要将此方法分别添加到每个集合类型中。
在Swift标准库中, 协议扩展用于实现例如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更改为ErrorHandler来看到它。
let handler: ErrorHandler = Handler()
现在, 控制台的输出为:该操作无法完成。 (ApplicationError错误0。)
但是, 如果我们将handle(error:Error)方法的声明返回给协议, 则结果将变为致命错误。
protocol ErrorHandler {
func handle(error: Error)
}
让我们看一下每种情况下发生的顺序。
当协议中存在方法声明时:
该协议声明handle(error:Error)方法并提供默认实现。该方法在Handler实现中被覆盖。因此, 无论变量的类型如何, 都会在运行时调用方法的正确实现。
如果协议中不存在方法声明:
因为该方法未在协议中声明, 所以该类型无法覆盖它。这就是为什么调用方法的实现取决于变量的类型的原因。
如果变量是Handler类型, 则从该类型调用方法实现。如果变量的类型为ErrorHandler, 则调用协议扩展中的方法实现。
面向协议的代码:安全而富有表现力
在本文中, 我们演示了Swift中协议扩展的一些功能。
与其他具有接口的编程语言不同, Swift不会对协议进行不必要的限制。 Swift允许开发人员根据需要解决歧义, 从而解决了这些编程语言的常见怪癖。
使用Swift协议和协议扩展, 你编写的代码可以像大多数动态编程语言一样表现力, 并且在编译时仍然是类型安全的。这使你可以确保代码的可重用性和可维护性, 并更有信心地对Swift应用程序代码库进行更改。
我们希望本文对你有所帮助, 并欢迎你提供任何反馈或进一步的见解。