Associated Types 关联类型
文中所描述的遵循协议的类型,之所以写为类型是因为在Swift中遵循协议的可以是类,结构体或者协议等等,官方文档用type,所以文中写为类型。
当我们定义一个协议的时候,在里面声明一个或多个关联类型会很有帮助。关联类型可以让我们以占位符的形式在协议中定义一个类型。这个自定义的类型只有在该协议被遵循时才被特别指定。关联类型的关键字是associatedtype
Associated Types基本使用
我们定义一个叫做Container的容器协议,里面声明一个关联类型叫Item:
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
这个容器协议定义了三个必须被实现的方法:
- 这个容器可以通过
append(_:)
来添加数据 - 通过count属性我们可以获取这个容器中数据的个数
- 我们可以通过下标来获取这个容器中的每个数据
这个容器协议没有特别指名其中的数据是如何被存储的,以及具体的数据类型。这个容器协议只是特别定义了三个必须遵循的方法。只要能实现这三个方法,那么遵循协议了的类型也可以提供更多的其他方法。
任何遵循了这个容器协议的类型中必须指明这个Item是哪个类型。这里,可以通过特别指定来保证加入容器的每个对象的类型是正确的,同时也保证了当用下标取出的对象的类型也是正确的。
为了满足这个需求,Container协议需要使用一种方式来指定容器中存储的这些数据的类型,但该类型在协议中声明是并不能指定为具体类型。而且在协议中的append(_:)
方法中参数以及subscript(i: Int) -> Item
返回值的类型必须一致。
为了达到这个目的,Container协议声明了一个关联类型Item,写作associatedtype Item
,这个协议并没有指明Item是什么,这个将留给遵循该协议的类来指定。虽然如此,但这个Item指定了容器协议中每个数据的类型,从以上两个方法可以看出,它保证了每个容器类型中的数据类型是一致的。
看一下遵循了该容器协议的具体类:
struct IntStack: Container {
// original IntStack implementation
var items = [Int]()
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
// conformance to the Container protocol
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类型实现了容器协议的所有需求。而且,IntStack也明确指定了Item其实就是Int类型。typealias Item = Int
这句代码就是将协议中的抽象类型变为具体类型。
由于Swift的类型推断,我们也不必特别指名Item是Int类型,因为IntStack遵循了容器协议,所以简单的从append(_:)
方法和subscript(i: Int) -> Item
方法中可以推断出Item就是Int,所以我们可以删除typealias Item = Int
这行代码。
当然,我们也可以将Stack做成范型,如下:
struct Stack<Element>: Container {
// original Stack<Element> implementation
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
// conformance to the Container protocol
mutating func append(_ item: Element) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Element {
return items[i]
}
}
现在,从append(_:)
方法和subscript(i: Int) -> Item
方法中可以看出使用的是Element,因此Swift可以推断出关联类型Item这回是Element。
扩展一个指定类型来指定这个关联类型
我们可以扩展一个已存在的类型来遵循这个声明了关联类型的容器协议。
Swift的Array类型已经提供了append(_:)
方法,一个count
属性,以及通过下标返回元素的方法。这也就是说我们可以扩展Array类型使其遵循该容器协议。如下
extension Array: Container {}
Array中已存在的方法append(_:)
和下标返回方法使得Swift的类型判断可以明确Item的类型,就和之前Stack
一样。这样扩展了Array之后,我们就可以把Array
和Stack
一起当作Container来使用。
对关联类型添加约束
我们可以在协议中对关联类型进行类型约束,使得遵循该协议的类型同样受到这个约束。如下,我们可以看到,遵循这个版本的Container的类型必须保证item是一样的。
protocol Container {
associatedtype Item: Equatable
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
拿协议自身来作为协议内关联类型的约束
再从上面引深,我们可以将一个协议作为满足自身需求来约束关联类型。来看下下面这个例子,定义了一个遵循容器协议的协议,并添加了suffix(_:)
方法。这个方法就是将从指定的下标开始到末尾的数据装到一个叫做Suffix的容器里并返回。
protocol SuffixableContainer: Container {
associatedtype Suffix: SuffixableContainer where Suffix.Item == Item
func suffix(_ size: Int) -> Suffix
}
在这个协议里面,Suffix
就和之前的Item
一样是关联类型。
Suffix
有两个约束:
- 必须遵循
SuffixableContainer
协议 - 该协议内的
Item
和Container
中的Item
必须一致
接下来让我们看下遵循了该协议的Stack
是怎么写的:
extension Stack: SuffixableContainer {
func suffix(_ size: Int) -> Stack {
var result = Stack()
for index in (count-size)..<count {
result.append(self[index])
}
return result
}
// Inferred that Suffix is Stack.
}
var stackOfInts = Stack<Int>()
stackOfInts.append(10)
stackOfInts.append(20)
stackOfInts.append(30)
let suffix = stackOfInts.suffix(2)
// suffix contains 20 and 30
从上面的例子可以看出,Stack
中Suffix
关联类型就是Stack
自身(别忘了,Stack是个范型哦),所以suffix(_:)
操作将返回另一个Stack
。然而,我们也可以在遵循了SuffixableContainer
协议的同时使Suffix
关联类型不是自身,也就是说,我们可以在suffix(_:)
方法返回一个不同的类型,来看下下面这个例子,我们用一个非范型类型IntStack
来遵循SuffixableContainer
协议,但是我们把关联类型IntStack
改为范型Stack<Int>
extension IntStack: SuffixableContainer {
func suffix(_ size: Int) -> Stack<Int> {
var result = Stack<Int>()
for index in (count-size)..<count {
result.append(self[index])
}
return result
}
// Inferred that Suffix is Stack<Int>.
}