- Published on
在协议中定义关联类型
- Authors
- Name
今天在看之前写的一个 RxSwift+MVVM 的一个工程代码的时候,其中有一个这样定义的协议
protocol ViewModelType {
associatedtype Input
associatedtype Output
func transform(input: Input) -> Output
}
在 ViewModelType 这个协议中,以 associatedtype 定义了两个关联类型 Input 和 Output。 这里为什么要使用关联类型来作为类型,以及和泛型的区别。
从代码把处的业务中来讲,
这里定义 Input 代表遵循ViewModelType 协议的 ViewModel 对应的视图的加载事件和用户交互事件:
- 所有的事件都使用 Driver 类型,这是 RxSwift 中专门用于 UI 绑定的类型
- Driver 的特点:
- 不会产生错误(error)
- 在主线程上执行
- 共享副作用(shareReplay)Driver 的“共享副作用”指的是它通过共享订阅机制(share(replay: 1))确保底层的副作用(如网络请求、计算等)只执行一次,多个订阅者共享相同的结果。这在 UI 开发中特别有用,能提高性能并保证数据一致性。
Output代表视图的状态和需要更新的UI。
- 同样使用 Driver 类型,确保 UI 更新的安全性
- 每个属性都代表一个需要更新到 UI 的状态
- 这些状态会通过 RxSwift 的绑定机制自动更新到对应的 UI 组件
回到我们的主题,对关联类型的讨论上来。
什么是关联类型
关联类型的定义 定义一个协议时,声明一个或多个关联类型作为协议定义的一部分将会非常有用。关联类型为协议中的某个类型提供了一个占位符名称,其代表的实际类型在协议被遵循时才会被指定。关联类型通过 associatedtype 关键字来指定。
这里需要和枚举中的关联值区分开来,是哪个不同的名称。
关联类型实践
下面的例子定义了一个 Container 协议,该协议定义了一个关联类型 Item:
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
Container 协议定义了三个任何遵循该协议的类型(即容器)必须提供的功能:
- 必须可以通过 append(_:) 方法添加一个新元素到容器里。
- 必须可以通过 count 属性获取容器中元素的数量,并返回一个 Int 值。
- 必须可以通过索引值类型为 Int 的下标检索到容器中的每一个元素。
该协议没有指定容器中的元素的类型以及如何存储。该协议只指定了任何遵循 Container 协议的类型必现提供上述三个功能。在遵循该协议的前提下,容器也可以提供其他额外的功能。
任何遵循 Container 协议的类型必须能够指定它存储的值的类型。具体来说,它必须确保添加到容器内的元素以及下标返回的元素类型都是正确的。
为了定义这些条件,Container 协议需要在不知道容器中元素的具体类型的情况下引用这种类型。Container 协议需要指定任何通过 append(_:) 方法添加到容器中的元素和容器内的元素是相同类型,并且通过容器下标返回的元素的类型也是这种类型。
为此,Container 协议声明了一个关联类型 Item, 写作 associatedtype Item。 协议没有定义Item是什么,这个信息留给遵循协议的类型来提供。尽管如此,Item 别名提供了一种方式来引用 Container 中元素的类型, 并将之用于 append(_:) 方法和下标,从而保证任何 Container 的行为都能如预期。
下面是一个非泛型的 IntStack,通过遵循 Container 协议,修改后的版本:
struct IntStack: Container {
var items: [Int] = []
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return item.removeLast()
}
//遵循Container 协议的实现部分
typealias Item = Int
mutating func append(_ item: Int) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Int {
return items[i]
}
}
IntStack 类型实现了 Container 协议的三项要求,,并且在每种情况下都封装了 IntStack 类型的现有功能的一部分,以满足这些要求。
此外,IntStack 在实现 Container 协议的要求时,指定 Item 为 Int 类型,即 typealias Item = Int,从而将 Container 协议中抽象的 Item 类型转换为具体的 Int 类型。
得益于 Swift 的类型推断机制, 实际上在 IntStack 的定义中不需要声明 Item 为 Int。因为 IntStack 遵循 Container 协议的所有要求,Swift 只需通过 append(_:) 方法的 item 参数类型和下标返回值的类型,就可以推断出 Item 的具体类型。事实上,如果你在上面的代码中删除了 typealias Item = Int 这一行,一切也可正常工作,因为 Swift 清楚地知道 Item 应该是哪种类型。
你也可以让泛型 Stack 结构体遵循 Container 协议:
struct Stack<Element>: Container {
// Stack<Element> 的原始实现部分
var items: [Element] = []
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
// Container 协议的实现部分
mutating func append(_ item: Element) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Element {
return items[i]
}
}
这一次,类型参数 Element 被用作 append(_:) 方法的 item 参数类型和下标的返回类型。因此,Swift 可以推断出 Element 即是 item 的类型。
未完待续