类型约束
swapTwoValues(_:_:)
函数和 Stack
适用于任意类型。不过,如果能对泛型函数或泛型类型中添加特定的 类型约束 ,这将在某些情况下非常有用。类型约束指定类型参数必须继承自指定类、遵循特定的协议或协议组合。
例如,Swift 的 Dictionary
类型对字典的键的类型做了些限制。在 字典的描述 中,字典键的类型必须是可哈希(hashable)的。也就是说,必须有一种方法能够唯一地表示它。字典键之所以要是可哈希的,是为了便于检查字典中是否已经包含某个特定键的值。若没有这个要求,字典将无法判断是否可以插入或替换某个指定键的值,也不能查找到已经存储在字典中的指定键的值。
这个要求通过 Dictionary
键类型上的类型约束实现,它指明了键必须遵循 Swift 标准库中定义的 Hashable
协议。所有 Swift 的基本类型(例如 String
、Int
、Double
和 Bool
)默认都是可哈希的。如何让自定义类型遵循 Hashable
协议,可以查看文档 遵循 Hashable 协议。
当自定义泛型类型时,你可以定义你自己的类型约束,这些约束将提供更为强大的泛型编程能力。像 可哈希(hashable)
这种抽象概念根据它们的概念特征来描述类型,而不是它们的具体类型。
类型约束语法
在一个类型参数名后面放置一个类名或者协议名,并用冒号进行分隔,来定义类型约束。下面将展示泛型函数约束的基本语法(与泛型类型的语法相同):
funcsomeFunction<T:SomeClass, U:SomeProtocol>(someT: T, someU: U) { // 这里是泛型函数的函数体部分 }
上面这个函数有两个类型参数。第一个类型参数 T
必须是 SomeClass
子类;第二个类型参数 U
必须符合 SomeProtocol
协议。
类型约束实践
这里有个名为 findIndex(ofString:in:)
的非泛型函数,该函数的功能是在一个 String
数组中查找给定 String
值的索引。若查找到匹配的字符串,findIndex(ofString:in:)
函数返回该字符串在数组中的索引值,否则返回 nil
:
funcfindIndex(ofStringvalueToFind: String, inarray: [String]) ->Int? { for(index, value)in array.enumerated() { if value == valueToFind { return index } } returnnil }
findIndex(ofString:in:)
函数可以用于查找字符串数组中的某个字符串值:
let strings = ["cat", "dog", "llama", "parakeet", "terrapin"] iflet foundIndex =findIndex(ofString:"llama", in: strings) { print("The index of llama is \(foundIndex)") } // 打印“The index of llama is 2”
如果只能查找字符串在数组中的索引,用处不是很大。不过,你可以用占位类型 T
替换 String
类型来写出具有相同功能的泛型函数 findIndex(_:_:)
。
下面展示了 findIndex(ofString:in:)
函数的泛型版本 findIndex(of:in:)
。请注意这个函数返回值的类型仍然是 Int?
,这是因为函数返回的是一个可选的索引数,而不是从数组中得到的一个可选值。需要提醒的是,这个函数无法通过编译,原因将在后面说明:
funcfindIndex<T>(ofvalueToFind: T, inarray:[T]) ->Int? { for(index, value)in array.enumerated() { if value == valueToFind { return index } } returnnil }
上面所写的函数无法通过编译。问题出在相等性检查上,即 "if value == valueToFind
"。不是所有的 Swift 类型都可以用等式符(==
)进行比较。例如,如果你自定义类或结构体来描述复杂的数据模型,对于这个类或结构体而言,Swift 无法明确知道“相等”意味着什么。正因如此,这部分代码无法保证适用于任意类型 T
,当你试图编译这部分代码时就会出现相应的错误。
不过,所有的这些并不会让我们无从下手。Swift 标准库中定义了一个 Equatable
协议,该协议要求任何遵循该协议的类型必须实现等式符(==
)及不等符(!=
),从而能对该类型的任意两个值进行比较。所有的 Swift 标准类型自动支持 Equatable
协议。
遵循 Equatable
协议的类型都可以安全地用于 findIndex(of:in:)
函数,因为其保证支持等式操作符。为了说明这个事情,当定义一个函数时,你可以定义一个 Equatable
类型约束作为类型参数定义的一部分:
funcfindIndex<T:Equatable>(ofvalueToFind: T, inarray:[T]) ->Int? { for(index, value)in array.enumerated() { if value == valueToFind { return index } } returnnil }
findIndex(of:in:)
类型参数写做 T: Equatable
,也就意味着“任何符合 Equatable
协议的类型 T
”。
findIndex(of:in:)
函数现在可以成功编译了,并且适用于任何符合 Equatable
的类型,如 Double
或 String
:
let doubleIndex =findIndex(of:9.3, in: [3.14159, 0.1, 0.25]) // doubleIndex 类型为 Int?,其值为 nil,因为 9.3 不在数组中 let stringIndex =findIndex(of:"Andrea", in: ["Mike", "Malcolm", "Andrea"]) // stringIndex 类型为 Int?,其值为 2
关联类型
定义一个协议时,声明一个或多个关联类型作为协议定义的一部分将会非常有用。关联类型为协议中的某个类型提供了一个占位符名称,其代表的实际类型在协议被遵循时才会被指定。关联类型通过 associatedtype
关键字来指定。
关联类型实践
下面例子定义了一个 Container
协议,该协议定义了一个关联类型 Item
:
protocolContainer { associatedtype Item mutatingfuncappend(_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
协议:
structIntStack:Container { // IntStack 的原始实现部分 var items: [Int] = [] mutatingfuncpush(_item: Int) { items.append(item) } mutatingfuncpop() ->Int { return items.removeLast() } // Container 协议的实现部分 typealiasItem=Int mutatingfuncappend(_item: Int) { self.push(item) } var count: Int { return items.count } subscript(i: Int) ->Int { return items[i] } }
IntStack
结构体实现了 Container
协议的三个要求,其原有功能也不会和这些要求相冲突。
此外,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
协议:
structStack<Element>:Container { // Stack<Element> 的原始实现部分 var items: [Element] = [] mutatingfuncpush(_item: Element) { items.append(item) } mutatingfuncpop() ->Element { return items.removeLast() } // Container 协议的实现部分 mutatingfuncappend(_item: Element) { self.push(item) } var count: Int { return items.count } subscript(i: Int) ->Element { return items[i] } }
这一次,占位类型参数 Element
被用作 append(_:)
方法的 item
参数和下标的返回类型。Swift 可以据此推断出 Element
的类型即是 Item
的类型。
扩展现有类型来指定关联类型
在扩展添加协议一致性 中描述了如何利用扩展让一个已存在的类型遵循一个协议,这包括使用了关联类型协议。
Swift 的 Array
类型已经提供 append(_:)
方法,count
属性,以及带有 Int
索引的下标来检索其元素。这三个功能都符合 Container
协议的要求,也就意味着你只需声明 Array
遵循Container
协议,就可以扩展 Array,使其遵从 Container 协议。你可以通过一个空扩展来实现这点,正如通过扩展采纳协议中的描述:
extensionArray:Container {}
Array
的 append(_:)
方法和下标确保了 Swift 可以推断出 Item
具体类型。定义了这个扩展后,你可以将任意 Array
当作 Container 来使用。
给关联类型添加约束
你可以在协议里给关联类型添加约束来要求遵循的类型满足约束。例如,下面的代码定义了 Container
协议, 要求关联类型 Item
必须遵循 Equatable
协议:
protocolContainer { associatedtype Item:Equatable mutatingfuncappend(_item: Item) var count: Int { get } subscript(i: Int) -> Item { get } }
要遵守 Container
协议,Item
类型也必须遵守 Equatable
协议。
在关联类型约束里使用协议
协议可以作为它自身的要求出现。例如,有一个协议细化了 Container
协议,添加了一个 suffix(_:)
方法。suffix(_:)
方法返回容器中从后往前给定数量的元素,并把它们存储在一个 Suffix
类型的实例里。
protocolSuffixableContainer:Container { associatedtype Suffix:SuffixableContainer where Suffix.Item == Item funcsuffix(_size: Int) -> Suffix }
在这个协议里,Suffix
是一个关联类型,就像上边例子中 Container
的 Item
类型一样。Suffix
拥有两个约束:它必须遵循 SuffixableContainer
协议(就是当前定义的协议),以及它的 Item
类型必须是和容器里的 Item
类型相同。Item
的约束是一个 where
分句,它在下面 具有泛型 Where 子句的扩展 中有讨论。
这是上面 泛型类型 中 Stack
类型的扩展,它遵循了 SuffixableContainer 协议:
extensionStack:SuffixableContainer { funcsuffix(_size: Int) -> Stack { var result =Stack() for index in(count-size)..<count { result.append(self[index]) } return result } // 推断 suffix 结果是Stack。 } var stackOfInts = Stack<Int>() stackOfInts.append(10) stackOfInts.append(20) stackOfInts.append(30) let suffix = stackOfInts.suffix(2) // suffix 包含 20 和 30
在上面的例子中,Suffix
是 Stack
的关联类型,也是 Stack
,所以 Stack
的后缀运算返回另一个 Stack
。另外,遵循 SuffixableContainer
的类型可以拥有一个与它自己不同的 Suffix
类型——也就是说后缀运算可以返回不同的类型。比如说,这里有一个非泛型 IntStack
类型的扩展,它遵循了 SuffixableContainer
协议,使用 Stack<Int>
作为它的后缀类型而不是 IntStack
:
extensionIntStack:SuffixableContainer { funcsuffix(_size: Int) -> Stack<Int> { var result = Stack<Int>() for index in(count-size)..<count { result.append(self[index]) } return result } // 推断 suffix 结果是 Stack<Int>。 }