原文:Getting started with associated types in Swift Protocols
Swift 中的关联类型与协议紧密配合。你可以从字面上将它们视为协议的关联类型:从你将它们放在一起的那一刻起,它们就是一家人。
显然,解释关联类型的工作原理有点复杂,但一旦掌握了它,就会更频繁地使用它们。协议允许我们定义类型之间的通用接口。
什么是关联类型?
关联类型可以看作是 protocol 定义中特定类型的替代。换句话说:它是一个类型的占位符名称,直到遵守协议并指定确切的类型。
最好用一个简单的示例代码来解释。如果没有关联类型,以下协议将仅适用于我们定义的类型。在这种情况下:String。
protocol StringsCollection {
var count: Int { get }
subscript(index: Int) -> String { get }
mutating func append(_ item: String)
}
如果我们想对 Double 集合使用相同的逻辑,我们需要重新创建一个新协议。关联类型通过放置一个占位符来防止这种情况:
protocol Collection {
associatedtype Item
var count: Int { get }
subscript(index: Int) -> Item { get }
mutating func append(_ item: Item)
}
关联类型使用 associatedtype 关键字定义,并告诉协议:下标语法的返回类型等于关联类型。通过这种方式,我们允许协议与以后定义的任何关联类型一起使用。示例实现如下所示:
struct UppercaseStringsCollection: Collection {
var container: [String] = []
var count: Int { container.count }
mutating func append(_ item: String) {
guard !container.contains(item) else { return }
container.append(item.uppercased())
}
subscript(index: Int) -> String {
return container[index]
}
}
使用关联类型有什么好处?
一旦开始使用关联类型,它们的好处就会显现出来。它们通过更轻松地为多个场景定义通用接口来防止编写重复代码。这样,相同的逻辑可以重复用于多种不同的类型,让你只需编写和测试一次逻辑。
关联类型的一个有用示例
我总是喜欢用真实的案例来演示代码技术。在 Collect by WeTransfer 应用程序中,我们必须在新设计中使用品牌颜色。定义的颜色必须可用于 UIKit 中的 UIColor 和 SwiftUI 中的 Color。显然,我们不想将所有颜色都定义两次,因为那样很难维护并会导致大量重复代码。
我们已经有了一种使用十六进制输入的 UIColor 上的自定义便捷初始化程序来定义颜色的便捷方法:
let color = UIColor(hex: "FF7217")
我们决定重用该方法并围绕它定义一个协议:
public protocol BrandColorSupporting {
associatedtype ColorValue
static func colorFor(hex: String, alpha: CGFloat) -> ColorValue
}
一些颜色还需要使用我们也包含在该协议中的特定 alpha 值来定义。然后我们为 UIColor 和 Color 添加了对该协议的支持:
extension UIColor: BrandColorSupporting {
public static func colorFor(hex: String, alpha: CGFloat) -> UIColor {
return UIColor(hex: hex).withAlphaComponent(alpha)
}
}
@available(iOS 13.0, *)
extension Color: BrandColorSupporting {
public static func colorFor(hex: String, alpha: CGFloat) -> Color {
return Color(UIColor.colorFor(hex: hex, alpha: alpha))
}
}
如你所见,我们在 Color 协议实现中重用了 UIColor 的逻辑。这允许我们重用 UIColor 的便捷初始化方法来使用十六进制值创建颜色。
由于并非所有颜色都需要定义 alpha 组件,因此我们向 BrandColorSupporting 协议添加了默认扩展:
extension BrandColorSupporting {
static func colorFor(hex: String) -> ColorValue {
return colorFor(hex: hex, alpha: 1.0)
}
定义静态颜色
UIColor 和 Color 现在都符合 BrandColorSupporting 协议,这意味着我们可以定义对两者都可用的扩展。
我们开始定义颜色:
public extension BrandColorSupporting {
static var orangeCollectHero: ColorValue {
colorFor(hex: "FF7217")
}
}
使用关联类型的最好的事情是我们可以使用相同的逻辑,同时根据上下文更改结果类型:
let colorForSwiftUI: Color = Color.orangeCollectHero
let colorForUIKit: UIColor = UIColor.orangeCollectHero
正如您所想象的那样,这是处理品牌颜色的好方法。我们已经准备好将 SwiftUI 与 UIKit 应用程序集成以供早期采用,而无需重新定义所有颜色。
为关联类型添加协议约束
现在我们已经看到关联类型如何在真实案例中工作,是时候更深入地研究可用的可能性了。
让我们继续我们之前展示的用于定义 Collection 的示例:
protocol Collection {
associatedtype Item
var count: Int { get }
subscript(index: Int) -> Item { get }
mutating func append(_ item: Item)
}
如果我们想在插入之前将附加项与任何现有项进行比较怎么办?我们需要有一种方法来比较公平。
我们可以通过将关联类型约束为 Equatable 协议来做到这一点:
protocol Collection {
// 关联类型现在需要遵循 Equatable 协议
associatedtype Item: Equatable
var count: Int { get }
subscript(index: Int) -> Item { get }
mutating func append(_ item: Item)
}
使协议遵循具有已定义关联类型的协议
在某些情况下,您希望使一个协议遵循(另一个定义关联类型的)协议。
protocol CollectionSlice: Collection {
func prefix(_ maxLength: Int) -> CollectionSlice
}
遇到这样的错误并不少见:
协议 “CollectionSlice” 只能用作通用约束,因为它具有 Self 或关联类型要求
这是因为编译器无法确保返回的 CollectionSlice 会产生与定义的协议相同的底层关联类型。因此,我们需要设置一个约束以确保两种类型相等:
protocol CollectionSlice: Collection {
associatedtype Slice: CollectionSlice where Slice.Item == Item
func prefix(_ maxLength: Int) -> Slice
}
现在要求此协议的实现者返回与其父集合类型相同的切片:
extension UppercaseStringsCollection: CollectionSlice {
func prefix(_ maxLength: Int) -> UppercaseStringsCollection {
var collection = UppercaseStringsCollection()
for index in 0..<min(maxLength, count) {
collection.append(self[index])
}
return collection
}
}
结论
Swift 中的关联类型是定义可在多种不同类型之间重用的代码的一种强大方式。需要实践才能充分利用它,但是一旦您理解了原理,您就可以轻松地重用大量代码。
如果您想了解有关 Swift 的更多技巧,请查看 Swift category 页面。如果您有任何其他提示或反馈,请随时与我联系或在 Twitter 上发推文给我。
谢谢!