Swift 中面向协议编程的介绍

已发表: 2022-03-11

协议是 Swift 编程语言的一个非常强大的特性。

协议用于定义“适合特定任务或功能的方法、属性和其他要求的蓝图”。

Swift 在编译时检查协议一致性问题,允许开发人员在运行程序之前发现代码中的一些致命错误。 协议允许开发人员在 Swift 中编写灵活且可扩展的代码,而不必损害语言的表达能力。

Swift 通过为困扰许多其他编程语言的接口的一些最常见的怪癖和限制提供变通方法,进一步提高了使用协议的便利性。

Swift 中面向协议编程的介绍

使用面向协议的编程在 Swift 中编写灵活且可扩展的代码。
鸣叫

在早期版本的 Swift 中,只能扩展类、结构和枚举,这在许多现代编程语言中都是如此。 然而,从 Swift 的第 2 版开始,扩展协议也成为可能。

本文探讨了如何使用 Swift 中的协议来编写可重用和可维护的代码,以及如何通过使用协议扩展将大型面向协议的代码库的更改合并到一个地方。

协议

什么是协议?

在最简单的形式中,协议是描述一些属性和方法的接口。 任何符合协议的类型都应该用适当的值填充协议中定义的特定属性,并实现其必要的方法。 例如:

 protocol Queue { var count: Int { get } mutating func push(_ element: Int) mutating func pop() -> Int }

Queue 协议描述了一个包含整数项的队列。 语法非常简单。

在协议块内部,当我们描述一个属性时,我们必须指定该属性是只有 gettable { get }还是同时 gettable 和 settable { get set } 。 在我们的例子中,变量 Count (类型为Int )只能获取。

如果协议要求属性是可获取和可设置的,那么常量存储属性或只读计算属性无法满足该要求。

如果协议只要求一个属性是可获取的,那么任何类型的属性都可以满足该要求,并且该属性也可设置是有效的,如果这对您自己的代码有用的话。

对于协议中定义的函数,重要的是指示函数是否会使用mutating关键字更改内容。 除此之外,函数的签名足以作为定义。

为了符合协议,类型必须提供所有实例属性并实现协议中描述的所有方法。 例如,下面是一个符合我们Queue协议的 struct 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() } }

然而,我们当前的队列协议有一个主要缺点。

只有处理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 ,如果我们想提供本地化描述)协议。

然后可以将相同的错误处理逻辑应用于整个代码中的任何这些错误对象。 因此,您不需要使用任何特定对象(如 Objective-C 中的 NSError)来表示错误,您可以使用任何符合ErrorLocalizedError协议的类型。

您甚至可以扩展 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. 添加协议中未描述的附加方法的实现,并使用这些附加方法“装饰”符合协议的任何类型。 这个特性允许我们为已经符合协议的多种类型添加特定的方法,而无需单独修改每种类型。

默认方法实现

让我们再创建一个协议:

 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 通过协议扩展非常优雅地处理这个问题,允许程序员控制编译器的不足之处。

添加新方法

让我们再看一下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协议的类型。 如果没有协议扩展,我们将不得不单独将此方法添加到每种类型。

协议扩展与基类

协议扩展可能看起来与使用基类非常相似,但使用协议扩展有几个好处。 这些包括但不一定限于:

  1. 由于类、结构和枚举可以符合多个协议,因此它们可以采用多个协议的默认实现。 这在概念上类似于其他语言中的多重继承。

  2. 类、结构和枚举可以采用协议,而基类和继承仅适用于类。

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()))) } }

现在我们可以计算任何队列集合( ArraySet等)的平均大小。 如果没有协议扩展,我们将需要将此方法分别添加到每个集合类型。

在 Swift 标准库中,协议扩展用于实现例如mapfilterreduce等方法。

 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()

现在控制台的输出是:操作无法完成。 (ApplicationError 错误 0。)

但是如果我们将handle(error:Error)方法的声明返回给协议,结果会变回致命错误。

 protocol ErrorHandler { func handle(error: Error) }

让我们看看每种情况下发生的顺序。

当协议中存在方法声明时:

该协议声明了handle(error: Error)方法并提供了默认实现。 该方法在Handler实现中被覆盖。 因此,无论变量的类型如何,都会在运行时调用该方法的正确实现。

当协议中不存在方法声明时:

因为方法没有在协议中声明,所以类型不能覆盖它。 这就是为什么调用方法的实现取决于变量的类型。

如果变量是Handler类型,则调用该类型的方法实现。 如果变量的类型是ErrorHandler ,则调用协议扩展中的方法实现。

面向协议的代码:安全而富有表现力

在本文中,我们展示了 Swift 中协议扩展的一些强大功能。

与其他具有接口的编程语言不同,Swift 不会对协议进行不必要的限制。 Swift 通过允许开发人员在必要时解决歧义来解决这些编程语言的常见怪癖。

使用 Swift 协议和协议扩展,您编写的代码可以像大多数动态编程语言一样富有表现力,并且在编译时仍然是类型安全的。 这使您可以确保代码的可重用性和可维护性,并更有信心地更改您的 Swift 应用程序代码库。

我们希望这篇文章对您有用,并欢迎任何反馈或进一步的见解。