第二十二章 泛型
8. Associated Type (关联类型)
当我们在定义协议的时候,定义一个或者多个关联类型作为协议定义的一部分是非常有用的,关联类型会给出一个占位符名称,该占位符名称会作为协议定义的一部分,知道协议被采用的时候,实际为关联类型使用的类型才会被使用。关联类型可以用关键字associatedtype
来加以说明。
8.1 Associated Types in Action (实践中的关联类型)
下面是一个协议Container
,它声明了一个关联类型Item
。
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
该协议Container
定义了三个任何容器都要提供的功能。
- 它必须要用
append(_ : )
方法来给容器里添加新的物品(item)。 - 它必须要能够读取容器中的物品的总数,并且能返回一个
Int
的值 - 它必须要能够用下标语法和
Int
的索引值来检索容器中的每一个物品。
该协议并没有指明容器中的物品时怎样被存储的和什么类型的物品可以存储在容器中。该协议只是指明了三个任何遵循Container
协议的类型必须提供的功能。遵循协议的类型在满足这三个条件的情况下也可以提供其他额外的功能。
任何遵循该Container
协议的类型都必须指明要需要存储值的类型。特别是,它必须确保只有正确类型的物品可以被添加到容器里,并且它还要通过用下标返回来明确物品的类型。
为了定义这三个条件,Container
协议需要在不知道容器中元素的具体类型的情况下引用这种类型, 协议需要指定任何通过 append(_:)
方法添加到容器中的元素和容器中的元素是相同类型,并且通过容器下标返回的元素的类型也是这种类型。
为了实现该目的,协议Container
通过定义一个关联类型Item
可以将该关联类型写成associatedtype Item
,该协议并没有定义什么是Item
,这个信息将留给遵循协议的类型来提供。尽管如此,Item别名还是提供了可以引用容器中元素的方法,并且定义了用来使用append(_:)
方法和下标的一个类型,从而来确保期望Container
来执行的某些行为。
下面是一个非泛型版本IntStack
的类型,采用和遵循来这个Container
协议。
// adopt and conform container protocol
struct IntStack: Container {
// 原IntStack的实现
var items = [Int]() // an array of intger value
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
// 遵循协议Container,该协议有一个关联类型叫Item.
typealias Item = Int // 将抽象类型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
协议的要求,每一个case都是满足了IntStack
现有的功能。此外,IntStack指明了Container的实现的要求时,即Item是Int类型。typealias Item = Int
的定义是将Container协议中的抽象类型Item
转换成具体的类型Int
。
swift中的类型推断,所以我们不需要声明将Int
中的具体Item
作为IntStack定义的一部分,因为IntStack遵循了协议Container的所有要求。Swift只需通过append(_:)
方法的item参数类型和下标返回值的类型,就可以推断出ItemType的具体类型。事实上,如果你在上面的代码中删除了typealias ItemType = Int
这一 行,一切仍旧可以正常工作,因为Swift清楚地知道 ItemType应该是哪种类型。
泛型类型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()
}
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的特定类型。
8.2 Extending an Existing Type to Specify an Associated Type (扩展已有类型来指明一个关联类型)
我们可以扩展一个现有的类型来添加遵循某个协议。详情见通过协议3章节中扩展添加协议一致性。其中包括了含有关联类型的协议。
swift中的Array
类型已经提供了append(_:)
方法,count
属性,和一个接受Int
为索引值来检索元素的下标。这三个功能都符合协议Container的要求。可以通过声明Array
采用该协议。用一个空白的扩展来做声明和采用协议。
// 创建一个空白的扩展,扩展一个Array来采用Container协议。
extension Array: Container {}
Array的现有append(_:)
方法和下标可以使Swift为使用Item
推断出合适的类型。就像上面那个泛型类型Stack那样。定义了这个扩展之后 我们可以把任何一个Array
当作Container
来使用。
8.3 Adding Constraints to an Associated Type (为关联类型添加约束)
我们可以在一个协议里面为关联类型添加一个类型约束,从而来要求遵循的类型必须要能满足这些约束。举个例子,下面这些代码定义了Container的一种版本,来要求容器中的所有物品必须是相同的。来遵行这个版本的Container,容器中的Item
类型不得不遵循Equatable
协议
protocol Container {
associatedtype Item: Equatable
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
8.4 Using a Protocol in Its Associated Type’s Containts (在关联类型的约束中使用协议)
一个协议可以作为协议本身要求的一部分出现,举个例子,下面是一个重新定义Container的协议,添加了suffix(_:)
方法要求,该方法从容器的必读返回一定数量的元素,并把这些元素存储在Suffix类型的实例中。
protocol SuffixableContainer: Container {
associatedtype Suffix: SuffixableContainer where Suffix.Item == Item
func suffix(_ size: Int) -> Suffix
}
在上面的协议中 Suffix是一个关联类型,就像上面Container中的Item类型那样。Suffix有两个约束:它必须遵循SuffixableContainer
协议(当前该协议已定义),并且Item
类型要和Container中的Item
类型保持一致。该协议中的Item
约束是一个泛型的where
字句,将会在下段有详细描述。
下面是一个来自于上面给协议SuffixableContainer添加一致性的泛型类型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
类型。举个例子,下面是一个给非泛型类型IntStack的扩展,不过该扩展添加了SuffixableContainer的一致性。使用Stack<Int>
作为替代IntStack
的suffix类型。
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>.
}