接上一篇 SWIFT语言中的泛型编程 【GENERIC】【PART 1】: http://www.cnblogs.com/bailey/p/3774839.html
类型约束
swapTwoValues函数和stack堆栈类可以实用各种具体类型,但是有时候我们要约束泛化的类型,指定它只能泛化相关的类或子类,类型约束就是用来做这个事情,在定义的时候限制占位符所能够泛化的具体类限制在某些类或子类、特定的协议或协议簇。
举个例子,Swift的Dictionary字典类型限制了使用者的类型必须是能够使用作为键值(Keys)使用的一些类型。引用字典类型的描述,字典的keys类型必须是可哈希的(hashable),也就是说,它要有办法给出个东西能够唯一代表该值(才能做Key)。字典需要它的Keys键值能够被哈希以便能够快速地被找出它所代表的值是否已经存在于字典中。没有这个功能,字典就无法插入,也无法代替指定键的具体值,更无法根据键值在字典中找出具体值。
字典Dictionary的key类型强制需要进行这种类型约束,它必须实现哈希协议(隶属于Swift语言的标准库)。所有的Swift语言的基本类型(比如String、Int、Double和Bool)都是默认可哈希的。
当创建一个自定义的泛型类型的时候,你可以定义你自已的类型约束,这些约束定义使你的泛型程序更有用。抽象概念方面,比如Hashable这样的协议依据概念特性赋予你定义类型的隐晦功能,而不是一些外在明显的功能。
类型约束语法
类型约束的定义是这样的:在类型参数如<T>后面加上一个冒号然后再加上一个类型名称。下面是泛型函数的最基本的类型约束定义示例(其实语法与泛型类型的约束定义也是一样的)
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
// function body goes here
}
假定上面的函数有两个参数,第一个是T,它有一个类型约束即T必须满足它是SomeClass的子类型。第二个参数是U,它必须满足具有SomeProtocol协议。
类型约束实践
这里是一个非泛型的函数叫findStringIndex,它指定一个字符串,然后在一个字符串数组中找到它。findStringIndex函数返回一个可选的Int整数值,它代表指向数组中与所查找的字符串一致的串的索引位置,如果找不到的话它就返回一个空值nil。
func findStringIndex(array: String[], valueToFind: String) -> Int? {
for (index, value) in enumerate(array) {
if value == valueToFind {
return index
}
}
return nil
}
那么,我们可以看看下面的例子,用findStringIndex在数组中找到相同的字符串:
let strings = ["cat", "dog", "llama", "parakeet", "terrapin"]
if let foundIndex = findStringIndex(strings, "llama") {
println("The index of llama is (foundIndex)")
}
// prints "The index of llama is 2"
另一方面,查找一个数值的方法与查找字符串的方法有所不同,但你可以写一个相同名的泛型函数findIndex,提供查找泛型T类型功能,替代刚才的只查找字符串类型的方案。
下面是你所期望的函数例子,名称是findIndex,它是findStringIndex函数的泛型版本。注意它返回的值仍然是可选的Int索引值,而不是其他的可选值类型。另外一点就是这个函数目前还不能通过编译,看完这个函数,接下去我们就来解释:
func findIndex<T>(array: T[], valueToFind: T) -> Int? {
for (index, value) in enumerate(array) {
if value == valueToFind {
return index
}
}
return nil
}
上面的函数无法通过编译问题在于等值检查这块,“if value==valueToFind”。并不是所有的Swift类型都有一个“==”等值比较操作符。当你为复杂数据模型创建类或结构的时候,实际上,Swift是无法帮你识别所有的类似“判断等值”的行为的。正因为如此,这段代码就无法适应所有的T类的情况,编译的时候也就出相关的错误报告了。
但是,我们还有办法,Swift标准库给我们提供了一个叫Equatable的协议(protocol),用于你比较确认==等号两边的类型值是否相等,或!=不等号来比较两个值。所有的Swift标准类型自动支持Equatable 协议
所有的Equatable类型能够安全的用于findIndex函数,因为它已声明支持这个等值操作。为了实现这种要求,你可以写Equatable的约束作为类型参数的定义,当你定义这个函数的时候可以这样写:
func findIndex<T: Equatable>(array: T[], valueToFind: T) -> Int? {
for (index, value) in enumerate(array) {
if value == valueToFind {
return index
}
}
return nil
}
这个类型参数的定义是T:Equatable,即意味着T必须是所有具有Equatable协议的类型。
现在findIndex函数可以被正确地编译了,你可以试用一下类型Double或String,它们都是有实现 Equatable协议的。
let doubleIndex = findIndex([3.14159, 0.1, 0.25], 9.3)
// doubleIndex is an optional Int with no value, because 9.3 is not in the array
let stringIndex = findIndex(["Mike", "Malcolm", "Andrea"], "Andrea")
// stringIndex is an optional Int containing a value of 2
关联类型
当你编写一个协议的时候,有时在协议里面定义一个或多个关联类型是有用的。关联类型留出了一个占位符名称 (或叫别名)给到协议里面的类型,而实际类型将在协议被采用的时候指向关联类型,关联类型用typealias关键字定义。
关联类型实践
这里是名字为Container的协议例子,它定义了一个关联类型,名叫:ItemType。
protocol Container {
typealias ItemType
mutating func append(item: ItemType)
var count: Int { get }
subscript(i: Int) -> ItemType { get }
}
Container协议定义了容器所需要的三个功能:
1、添加一个新条目到容器里面,必须使用append方法
2、可以使用一个叫count的整数属性返回容器里面有多少条目(items)
3、可以使用Int整数索引器来存储容器里面的内容条目
这个协议并不关心容器内所存储的条目是如何存放和内部的类型是怎么样。协议(protocol)只是要求容器必须具备这三点功能。一个具有该协议的类型可以提供附加的功能,只要满足这三个基本需求就可以。所有遵从Container协议的类型都要能够指定所存储的数据的类型。一定要保障正确的类型被加入到容器中,同时也必须清楚索引器所返回的条目的类型的一致性。
为了定义这些基本需求,容器(container)协议需要有手段知道所持有的元素的类型,而不提前知道容量将用于存放什么类型的元素。容器(container)协议需要确定通过append传给它的值必须与容器里面的元素类型是一样的,然后通过容器索引器存储容器返回元素的类型,也是和当初调用append时存入元素的类型一样。
为了达到这个目标,容器(container)协议定义关联类型(ItemType),表达为typealias ItemType。协议并不先确定目标的关联类型是什么具体类型,而将这一切留给了持有这些协议的类型去解释,虽然如此,我们还是需要ItemType别名来指定元素类型的方案,使具有Container协议的类能够使用append和索引下标来存储元素,以强制实现指定类的容器行为。
这个是上文早期的非泛型的IntStack类型,接入和遵从Container协议。
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 ItemType = 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声明实现Container容器类,ItemType被设置为Int类型,typealias ItemType = Int 将抽像的ItemType类型具体化为Int整数类型进行Container协议的实现。
感谢Swift的所带来的类型推导功能,我们无须在IntStack中定义具体的ItemType类为Int整型,因为IntStack遵从所有的Container协议需求,Swift能够推断合适的ItemType具体类型去套用,它只要简单地查看append方法所使用的item参数的和下标索引器返回的类型就可以确定具体的类型。其实,如果你删除了 typealias ItemType=Int这一行的话,代码仍然可以正常工作,代码类在没有明确指定ItemType类型的情况也是可以运行的。
你也可以把上文的堆栈类设计成遵守Container协议的类:
struct Stack<T>: Container {
// original Stack<T> implementation
var items = T[]()
mutating func push(item: T) {
items.append(item)
}
mutating func pop() -> T {
return items.removeLast()
}
// conformance to the Container protocol
mutating func append(item: T) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> T {
return items[i]
}
}
这一次,占位符类型参数T是作为append方法的item参数类型和索引下标存储容器返回值的类型。Swift可以根据特殊实现的容器类的ItemType指定元素类型来确定T类型。
扩展现有类型
你可以扩展现有的类型,使它们遵从一些新加入的协议(protocol),具体可以查看“使用扩展添加协议遵从性”章节,它包括了使用关联类型的协议。
Swift的数组类型已经提供了append的方法、count属性、还有Int整数的索引器来存取它的元素,这三个功能满足Container协议的基本要求。这就意味着你可以扩展Array数组来遵从Container协议,只要简单地声明Array采取该协议就行了(代码如下)。你可以使用extension扩展类型的方法来实现它,具体见“在extension扩展中定义采取新协议”章节。
extension Array: Container {}
数组的现有的append方法和索引器使用Swift能够推断合适的类型用于ItemType,正如上文泛型的Stack堆栈类型一样,定义了这个Array扩展之后,你就可以把数组当作容器使用了(具备了容器的特性)。
Where子句
类型约束,它使你可以定义对泛型函数或类的类型参数的要求。
你也可以定义关联类型的需求,你只要使用where子句就可以定义类型参数列表,一个where子句使用你能够要求关联类型满足一定的协议,确定或可选地制定类型参数和关联类型是否要一样。where子句的写就是将“where” 关键字放置在类型参数的名称后面,然后写上一个或多个对关联类型的约束,确定或可选地制定类和关联类型的约束要求。
下面的例子定义了泛型函数allItemsMatch,它检查两个容器实例是否拥有一样的内容条目,函数返回一个布尔值true说明所有的内容条目是一样的,而返回false代表存在不同的情况。
两个容器放在一起检查 不代表它们 一定是相同类型的容器(虽然它们可以是),但他们可以拥有相同类型的内容条目,这个说法可以使用类型约束和where子句来表现:
func allItemsMatch<
C1: Container, C2: Container
where C1.ItemType == C2.ItemType, C1.ItemType: Equatable>
(someContainer: C1, anotherContainer: C2) -> Bool {
// check that both containers contain the same number of items
if someContainer.count != anotherContainer.count {
return false
}
// check each pair of items to see if they are equivalent
for i in 0..someContainer.count {
if someContainer[i] != anotherContainer[i] {
return false
}
}
// all items match, so return true
return true
}
这个函数输入两个参数叫做someContainer和anotherContainer。 someContainer参数是C1类型,而anotherContainer参数是C2类型,两者都是占位符类型参数,对这两个容器类来说,具体的实例化类型要到调用的时候才知道。
下面是对函数的这两个类型参数的功能目标需求:
1、C1必须遵从Container协议(写为 C1:Container)
2、C2必须也遵从Container协议(写为 C2:Container)
3、C1的ItemType一定要和C2的ItemType一样(写为:C1.ItemType == C2.ItemType).
4、c1的ItemType一定要遵从Equatable协议(写为:C1.ItemType: Equatable).
第三和第四项需求是定义了where子句来实现它们的约束需求,作为类型参数定义的一部分来声明,使用到了where关键字。
这些需求的含义:
1、 someContainer是C1类型的容器
2、 anotherContainer是C2类型的容器
3、 someContainer和anotherContainer包含了相同类型的条目
4、 someContainer的所有条目能够使用!=不等操作符来检查判断是否与anotherContainer的内容一样
第三和第四项需求放在一起表示所有的在anotherContainer的条目能够使用!=运算符进行检查,因为它们也和someContainer一样拥有相同的类型。
我们设计allItemsMatch函数来看看容器的基本功能是否工作正常,检查两个容器实例工作的时候是否有所不同。
allItemsMatch函数先检查容器实例是否包含相同数量的元素条目,如果包含的元素条目数量不同,说明他们肯定内容就是不一样了,所以函数就可以直接返回false。
在实现这个检查功能之后,函数使用for-in循环和半开区间(..)迭代调用所有的元素项,函数检查someContainer的元素项是否等值于anotherContainer元素项,如果两个元素值是不同的,则说明两个容器实例内容不相同,函数返回false。
如果循环结束时仍没有找到不同的项,则判断这两个容器是一致的,函数返回值为true。
这是allItemsMatch函数的实现情况:
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
var arrayOfStrings = ["uno", "dos", "tres"]
if allItemsMatch(stackOfStrings, arrayOfStrings) {
println("All items match.")
} else {
println("Not all items match.")
}
// prints "All items match."
上面这个例子创建了Stack实现来存储String字符串数值,压入三个字符串到堆栈上。这个例子还创建了一个数组实例并初始化为与刚入压栈的字符串相同的三个串组成的数组。虽然堆栈和数组是不同的类型,但他们都遵从自相同的Container协议,也具有相同类型的值。你接着可以调用allItemsMatch函数,使用这两个容器作为该函数的传入参数,allItemsMatch函数将准确地检查并告诉你,这两个容器实例的所有元素是一样的。
【end】