关于泛型
「泛型」(Generic Code)也许是Swift相对于OC的最大特性之一吧!基于Swift的「泛型」特性,你能够写出扩展性更强、复用性更强的方法、类型,它可以让你尽可能避免重复代码,用一种清晰和抽象的方式来表达代码的意图。
许多的Swift标准库是基于「泛型」够构建的。譬如,Swift中的集合类型都是泛型集,你可以创建一个Int
数组,也可以创建一个String
数组,甚至任意其他任何类型的数组,这其实就是「泛型」。
泛型所解决的问题
泛型解决什么样的问题呢?如下是一个常见的、非泛型的函数swapTwoInts,用来交换两个Int值:
func swapTwoInts(inout a: Int, inout b: Int) { let temp = a a = b b = temp }
这个函数使用inout
参数来交换a和b的值:
var a: Int = 4 var b: Int = 2 println("a = (a), b = (b)") // prints "a = 4, b = 2" swapTwoInts(&a, &b) println("a = (a), b = (b)") // prints "a = 2, b = 4"
这个函数非常有用,但可惜的是它只能用来交换两个Int值,如果你想交换两个String或Double,就不得不写更多的函数,如swapTwoStrings和swapTwoDoubles函数,如下:
// 交换两个String实例的值 func swapTwoStrings(inout a: String, inout b: String) { let temp = a a = b b = temp } // 交换两个Double实例的值 func swapTwoDoubles(inout a: Double, inout b: Double) { let temp = a a = b b = temp }
很容易注意到swapTwoInts、swapTwoStrings和swapTwoDoubles函数功能都是相同的,唯一不同之处在于传入的变量类型不同,分别是Int、String和Double。但实际应用中通常需要一个用处更大并且尽可能的考虑到更多的灵活性函数,可以用来交换两个任意类型值,「泛型」正好可以用来解决这种问题。
泛型函数
泛型函数可以工作于任何类型,如下是上述swapTwoInts函数的一个泛型版本,也用于交换两个值:
func swapTwoValues<T>(inout a: T, inout b: T) { let temp = a a = b b = temp }
swapTwoValues函数body和swapTwoInts函数body完全一样,只是函数声明行不太一样:
func swapTwoInts(inout a: Int, inout b: Int)
func swapTwoValues<T>(inout a: T, inout b: T)
可以看到,「泛型」版本swapTwoValues使用「类型名占位符」T
(T
不是必须的,可以使用其他字符代替)代替了真正的类型。在Swift中和其他语言一样,使用尖括号<>
来包含函数中会使用到的「类型名占位符」,这样编译器在编译时就不会去审查T
的合法性了。「类型名占位符」没有提示T必须是什么类型,但是它提示 了形参a和b必须是同一种类型,而不管T表示什么类型。只有swapTwoValues函数在每次被调用时所传入实际类型才能确定T所代表的类型。
「泛型」函数swapTwoValues的使用示例如下:
var intA = 4 var intB = 2 println("a = (intA), b = (intB)") // prints "a = 4, b = 2" swapTwoValues(&intA, &intB) println("a = (intA), b = (intB)") // prints "a = 2, b = 4" var stringA = "4" var stringB = "2" println("a = (intA), b = (intB)") // prints "a = 4, b = 2" swapTwoValues(&intA, &intB) println("a = (intA), b = (intB)") // prints "a = 2, b = 4"
泛型类型
除了「泛型函数」,Swift还允许你定义自己的「泛型类型」。这些「泛型类型」可以适应任意类型,就像Array/Set/Dictionary那样。
这一部分会展示如何写一个泛型集类型 — Stack(栈)。一个栈是一系列值域的集合,和Array类似,但是比Swift的Array有更多的限制。一个数组允许向其中任何位置执行插入/删除操作,而Stack,只允许在集合的末端添加新的元素(即所谓的push操作),也只能从末端移除元素(即所谓的pop操作)。
栈的概念已被UINavigationController类使用来模拟控制器的导航结构。你可以通过调用UINavigationController的pushViewController:animation:
方法来为导航控制器添加新的视图控制器;而通过popViewControllerAnimation:
方法来从导航栈中pop某个视图控制器。每当你需要一个严格的后进先出方式来管理集合时,栈都是最实用的模型。
下图展示了栈的push和pop行为:
如图,有5个过程:
- 有三个元素在栈中;
- 第四个元素正在被push到栈的顶部;
- 有四个元素在栈中;
- 栈中的最顶部元素正在被移除;
- 有三个元素在栈中;
先来写一个非泛型版本的栈,这个栈只盛装Int型元素:
struct IntStack { var items = [Int]() mutating func push(item: Int) { items.append(item) } mutating func pop() -> Int { return items.removeLast() } }
struct Stack<T> { var items = [T]() mutating func push(item: T) { items.append(item) } mutating func pop() -> T { return items.removeLast() } }
其实也蛮简单的,不多说了!
扩展一个泛型类型
和其他类型一样,我们也可以对一个泛型类型进行扩展,语法也差不多,如下对上文的Stack扩展了一个方法topItem。注意,在「泛型类型」的扩展中,可能也会使用到「类型名占位符」,如下:
extension Stack { var topItem: T? { return items.isEmpty ? nil : items[items.count - 1] } }
值得注意的是,扩展中不再需要定义参数表。很显然感觉到,我们可能是没办法对一个我们看不到源码的「泛型类型」进行扩展的,因为我们不知道人家的「类型名占位符」。
类型约束
上文中的泛型函数swapTwoValues函数和泛型类型Stack都可以作用于任何类型,不过有时候,我们需要对泛型函数和泛型类型中的泛类型做些限制,譬如定义某个函数,传入的参数必须是可迭代类型,或者参数必须遵循某个协议。
Swift的泛型类型Dictionary对作用域key的类型做了些限制,要求key类型必须是可哈希的。因此Dictionary要求key类型必须遵循Hashable协议(Swift标准库中定义的一个协议)。
P.S:Swift的基本类型,如Int、String、Bool等都是可哈希的。
当你创建自定义「泛型类型」时,你可以定义你自己的「类型约束」。「类型约束」加强了泛型编程的威力。
类型约束语法
Swift规定在「类型名占位符」后面添加「类型约束」,通过冒号分隔,如下:
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) { // function body goes here }
类型约束实战
如下定义了一个名为findStringIndex的非泛型函数,该函数的功能是去查找包含一个给定String值的数组。若找到匹配的字符串,该函数返回该字符串在数组中的index,反之返回nil:
func findStringIndex(array: [String], valueToFind: String) -> Int? { for (index, value) in enumerate(array) { if value == valueToFind { return index } } return nil } 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"
T
值替换掉提到的字符串。OK,参考这个findStringIndex函数写一个泛型函数findIndex,该函数的作用类似,只不过适用于更多类型(而不仅仅是[String]
和String
)。
某人可能这样编码:
func findIndex<T>(array: [T], valueToFind: T) -> Int? { for (index, value) in enumerate(array) { if value == valueToFind { return index } } return nil }
但是很快就会发现Xcode报错:Binary operator ‘==’ cannot be applied to two T operands.
简单来说,编译器无法知道「类型名占位符」T
所表示的类型是否支持==
操作符。
找到问题就好办了,添加一个「类型约束」就可以解决这问题了。Swift标准库中有一个名为Equatable
的协议,遵循该协议的类型都得支持==
操作。所以上述泛型函数findIndex定义如下:
func findIndex<T: Equatable>(array: [T], valueToFind: T) -> Int? { for (index, value) in enumerate(array) { if value == valueToFind { return index } } return nil }
关联类型
在《Swift协议》中总结了Swift协议的相关知识点。
之前阐述的「协议」里的所有类型都是确切的;但强大的Swift定义的「协议」不限于此,换句话说,你可以将泛型思想融入到「协议」中。
简单来说,Swift允许你在protocol中使用类似于「泛型函数」「泛型类型」的泛类型,这被Swift文档称为「关联类型」(Associated Types)。
关联类型实战
如下定义一个名为Container的协议,理论上Swift中的内置collection应该遵循该协议,定义如下:
protocol Container { typealias ItemType mutating func append(item: ItemType) var count: Int { get } subscript(i: Int) -> ItemType { get } }
似乎有些奇怪,我还以为Container应该这么定义:
protocol Container<T> { mutating func append(item: T) var count: Int { get } subscript(i: Int) -> T { get } }
暂时先不理会哪种定义方式更好,总之,Container协议定义了三个任何容器必须支持的兼容要求:
- 必须定义append方法用来添加新元素;
- 必须定义count属性用来获取元素数量,返回类型是Int;
- 必须定义下标,支持通过Int索引值检索到某个元素;
这个Container协议没有指定容器中的元素是如何存储的,也没有指定容器可以存储的元素类型,但是限制了「append方法的形参类型必须和subscript返回值类型一致。这种限制构成所谓的「关联类型」。
OK,我们让上文的非泛型类型IntStack遵循Container协议,如下:
struct IntStack: Container { var items = [Int]() mutating func push(item: Int) { items.append(item) } mutating func pop() -> Int { return items.removeLast() } // conformance to the Container protocol mutating func append(item: Int) { items.append(item) } var count: Int { return items.count } subscript(i: Int) -> Int { return items[i] } }
struct IntStack: Container { 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: ItemType) { items.append(item) } var count: Int { return items.count } subscript(i: Int) -> ItemType { return items[i] } }
typealias ItemType = Int
,将抽象的ItemType类型转换为具体的Int类型。对于泛型类型Stack,可以更简洁一点:
struct Stack<T>: Container { 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) { items.append(item) } var count: Int { return items.count } subscript(i: Int) -> T { return items[i] } }
Swift的Array已经提供「append方法」,「count属性」以及「通过下标来查找一个自己的元素」。这三个功能都达到Container协议的要求,也就意味着你可以扩展Array去遵循Container协议,只要通过简单声明Array遵循该协议而已。声明一个已有类型遵循某个协议非常简单:
extension Array: Container {}
Where语句
「类型约束」使得Swift的泛型更加强大,但是还不够灵活。想象一个应用场景。某个函数接受两个参数,这两个参数都要求遵循Container协议,除此之外,还都要求这两个参数(集合类型)的元素类型相同,这该怎么弄?单纯的「类型约束」是办不到的,好在Swift为我们带来了where
语句。
where
语句的目的很直接,增强了「泛型」的威力。根据我的理解,where
应该属于那种「约束少」「灵活大」的语言特性,关于它的使用想必非常繁杂。
下面举个栗子引出where
的应用场景。定义一个名为allItemsMatch的泛型函数,顾名思义,该函数用来检查两个Container是否包含相同顺序的相同元素,如果所有元素顺序相同且值相同,则返回true,否则返回false,如下:
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 }
泛型函数allItemsMatch头部信息告诉我们:
- 该函数定义了两个参数,这两个参数的类型都遵循Container协议;
- 该函数的两个参数(容器)中元素的类型一致;
- 该函数的两个参数中的元素遵循Equatable协议;
OK,我们来演示了allItemsMatch函数运算的使用:
var stackOfStrings = Stack<String>() stackOfStrings.push("张不坏") stackOfStrings.push("张无忌") stackOfStrings.push("张全蛋") var arrayOfStrings = ["张不坏", "张无忌", "张全蛋"] if allItemsMatch(stackOfStrings, arrayOfStrings) { println("All items match.") } else { println("Not all items match.") } // prints "All items match."
上面的例子创建一个Stack单例来存储String,然后push三个字符串进栈。然后创建了一个Array实例,并初始化三个同样顺序同样值的字符串。这样,即便栈和数组属于不同的类型,但他们都遵循Container协议,而且它们都包含同样的类型值,完全满足allItemsMatch对参数的要求。