Swift - 进阶之泛型编程
Swift语言有很多强大的特性,泛型编程(generic programming)就是其中之一,我们也可以将其简称为泛型(generic)。使用泛型编码,可以编写出更加灵活(flexible)、可复用(reuseable)的函数和类型,与此同时它还保证了swift的类型安全(type safety)。
痛点 -- The Problem
OC的集合类型在运行时可以包含任意类型的对象,这一方面让它具有很多的灵活性,但同时也意味着OC缺少安全性。当使用OC提供的API时,我们不能保证一个特定的集合返回文档中所指示(indicated)的类型。Swift使用类型集合(typed collections)解决了OC存在的安全问题,但是这也造成了很多重复冗余的代码。让我们看看下面列举的一些例子,
let stringArray = ["1", "2", "3", "4"]
let intArray = [1,2,3,4]
let doubleArray = [1.1, 1.2, 1.3, 1.4]
上面,我们定义了三个类型集合,分别是字符串数组stringArray
, 整形数组intArray
以及双精度浮点型数组doubleArray
,此时如果想遍历这3个数组,并打印出数组中的每一个元素,可以针对这三个不同的类型数组分别处理,如下代码所示,
func printStringFromArray(a: [String]) {
for s in a {
println(s)
}
}
func printIntFromArray(a: [Int]) {
for i in a {
println(i)
}
}
func printDoubleFromArray(a: [Double]) {
for d in a {
println(d)
}
}
这样做可以解决我们小小的需求,但是它并不够理想。上述3个函数的函数体可以说是完全相同的,唯一的区别在于它指定类型的函数签名,也就是3个函数的参数不一样。那么,我们可以将3个函数抽象、整合,并用下面的方式重写,
func printElementFromArray(a: [Any]) {
}
上面的Any类型在swift中表示所有的类型,包括结构体(struct), 函数(function), 类(class), 枚举(enum)等。我们可以使用函数printelementFromArray来遍历数组并打印每一个数组元素。但是这同样不够理想,因为当我们使用这个参数是[Any]类型的方法时,则失去了类型安全。为什么这么说呢?因为使用[Any]类型的数组时,我们并不能保证[Any]只包含字符串数据或整形数据。
让我们看一个使用[Any]数组失效的例子 -- 交换两个变量的值,可能我们都写过交换两个字符串的值,如下代码所示,
func swapTwoStrings(inout1 a: String, inout2 b: String) {
let tempA = a
a = b
b = tempA
}
当然这里不是讨论怎样交换两个字符串,而是要支持任意类型。现在我们对其作简单扩展,让它可以处理任意类型,那么我们使用Any类型来处理这样的需求,如下代码所示,
func swapTwo(inout1 a: Any, inout2 b: Any) {
let tempA = a
a = b
b = tempA
}
代码看起来很棒,如果我们调用该方法来交换两个字符串的值,这个方法可以通过编译,并且运行也可以得到我们想要的结果。看看下面的代码,调用该方法来交换两个字符串,
var firstString = "someString"
var secondString = "anotherString"
swapTwo(inout1: firstString, inout2: secondString)
然而,它还是存在一个隐藏的问题,如果调用该方法时传递的两个参数,一个是String类型,一个是Int类型,那么就会出现运行时崩溃,如下代码所示,
var firstString = "someString"
var someInt = 1
swapTwo(inout1: firstString, inout2: someInt)
上面的代码可以通过编译(compile),因为String和Int参数对于swapTwo函数的Any类型参数来说是合法的入参。但是在运行时则会导致崩溃,因为firstString
是一个String类型的变量,不能将其赋值给Int类型的对象。所以说,使用Any时,我们并不能保证类型安全。
泛型 -- Generics
上述的问题让我们很困扰,不过不必担心,我们可以使用泛型来重写swapTwo函数,它可以完美地解决类型安全的问题。
先看一个例子,使用泛型来输出数组中的值,如下代码所示,
func printElementInArray<T>(a: [T]) {
for element in a {
println(element)
}
}
使用泛型定义的函数与之前使用Any定义的函数最主要的区别在于泛型函数的参数省略了特定的参数类型,而使用T
来替代。那么,T
又是什么呢?
泛型函数使用占位符(placeholder)名称来代替一个具体的类型,比如String, Int或Double。上面代码定义的泛型函数中,占位符是T
。当然你可以使用任意的占位符,例如Placeholder
或SomeType
,不过推荐使用T
,因为它是一个约定俗称的规定。
占位符T
并不是表示这个方法的参数接收T
类型的入参,相反,T
会被替换为一个具体的类型,而这个类型是在方法被调用的时候才能决定。如果我们调用printElementInArray方法,并传入一个字符串数组,那么T
被指定为String类型。基于这样的机制,我们可以使用这一个方法来遍历并打印三个数组。
泛型定义的函数可以接受任意的类型作为入参,这无疑让我们编写的代码更具灵活性。事实上,在定义函数时我们可以指定多个泛型类型。例如下面的代码,
func someFunction<T, U>(a: T, b: U) {}
这里,我们使用了T
和U
两个泛型作为占位符来定义someFunction,此时参数a
必定是T
类型的,b
必定是U
类型的。如果我们在调用的时候传入的参数与指定的泛型不一致时,则编译器会报错,如下代码所示,
someFunction<String, Int>(1, "Test")
之所以报错是因为指定的泛型<String, Int>与入参不一致。
本文的代码是swift 2.3之前的语法,在swift 3.0中,调用someFunction不可以指定<String, Int>,读者可以用swift 2.3之前的版本验证。
如果输入参数不符合泛型的类型,则不能编译通过,那么基于这个原则,我们来考虑使用泛型来重新实现swapTwo函数,如下代码所示,
func swapTwo<U>(inout1 a: U, inout2 a: U) {
let tempA = a
a = b
b = tempA
}
这时,如果在调用swapTwo函数时,例如swatTow("124", 123)
,没有传递正确的参数,则编译器会报错。
其实,我们常用的swift集合操作map, filter, reduce和flatMap实现原理都是基于泛型编程,例如我们可以使用map操作快速对整形数组中每个元素求平方,如下代码所示,
let intArray = [1, 3, 4, 2, 8, 5]
let newIntArray = intArray.map { $0 * $0 }
如果让你来实现map函数,你会怎样处理呢,要保证map可以作用于包含任意类型的集合,那么首选当然是泛型了,可以参考下面的代码,它提供了map简单的实现方式,
extension Array {
func map<U>(transform: Element -> U) - [U] {
var result: [U] = []
// 分配存储空间
result.reserveCapacity(self.count)
for e in self {
result.append(transform(e))
}
return result
}
}
在这里,Element是数组中包含的元素类型的占位符,U是元素转换之后的类型的占位符。map函数本身并不关心Element和U究竟是什么,他们可以是任意类型,它们的实际类型会留给调用者来决定。
能够在方法调用的时候指定泛型的类型,无疑是一个巨大的优势,但有时候我们不想要任意或所有的类型,那么我们就需要学习了解泛型的另一个特性 -- 类型约束。
思考:鉴于map函数的实现方式,读者可以尝试实现filter的功能。
类型约束 -- Type Constraints
关于泛型编码,我们可以更进一步,现在来探讨一下泛型约束(constraints)。
第一种泛型约束 - 泛型继承自指定的父类
首先,关于泛型约束,我们可以指定泛型类型继承自一个指定的父类(superclass)。比如现在我们有一个数字图书馆(digital library)类型的应用,在这个数字图书馆应用中,我们有书籍(books),电影(movies),tv秀,音乐(songs)和博客(podcasts)等资源,这些资源都继承自一个父类Media。
我们可以定义一个泛型排序函数,这个函数允许我们使用多种过滤条件对图书馆里的资源进行排序。因为这个数字图书馆应用程序除了Media类型的资源,还包含其他的资源,为了简化模型,我们假设定义的排序函数只接受Media及其子类作为函数参数,如下代码所示,是我们实现排序函数的一种方式,
func sortEntertainment<T: Media>(collection: [T]) {
// todo
// sort by specified filters
}
这个泛型函数sortEntertainment,接收一个[T]类型的集合对象作为参数,并对该集合按照特定的方式进行排序。当我们调用这个函数,并传入一个集合作为参数时,这个指定的T
类型必须继承自Media类型,也就是说作为参数传入的集合只能包含Media或Media子类的对象。
所以如果我们传入了[Media]和[Book]集合,sortEntertainment函数可以有效运行,如下代码,
let medias: [Media] = [Media]()
sortEntertainment(collections: medias)
let bookShelf: [Book] = [Book]()
func sortMedia(collection:bookShelf)
但如果传入[Shirt]集合,编译器就会报错,如下代码,
let clothesRack: [Shirt] = [Shirt]()
func sortMedia(collection: clothesRack) // Error, insert as! [Media]
第二种泛型约束 - 泛型实现指定的接口
其次,关于泛型约束,我们可以要求泛型实现某个特定的接口(protocol)。
一个相关的例子就是swift中的Set,Set就像数组(Array)和字典(Dictionary)一样,它存储了无序且不重复的元素。在swift 1.0中并没有实现标准的Set功能,所以开发者为了使用Set的功能,就得自己来实现Set的功能。
例如,这里有一个来自于NSHipster站点供稿作者的实现,如下代码所示,
struct Set<T> {
typealias Element = T
private var contents: [Element: Bool]
init() {
self.contents = [Element: Bool]()
}
// implementation
}
上面代码中,定义Set结构时,使用swfit原生的字典来进行数据存储,该字典命名为contents。将传入的类型作为字典contents的键key
,并使用Bool值true来标志Set中已经包含了某个特定的值,这是就不能将该值添加到Set中,这样就保证了Set集合中的元素不重复。
在swfit中,将一个类型作为字典的键key
有一些限定条件,比如该类型的对象可以转换为hash。为了实现这个需求,我们可以再构造自定义Set的时候指定传入的类型实现了Hashable
接口。那么对代码做如下调整即可,
// 指定T实现Hashable协议
struct Set<T: Hashable> {
typealias Element = T
private var contents: [Element: Bool]
init() {
self.contents = [Element: Bool]()
}
// implementation
}
这样,我们就可以限定泛型函数的入参类型,它必须实现Hashable接口。
泛型类型 -- Generic Types
上面讨论的都是泛型函数,然而我们还能围绕泛型做更多事情,因为swift同样允许我们定义泛型类型(generic type),尽管在文章中并没有明确说出泛型类型这样的概念,但我们确实已经定义了一个泛型类型 -- 自定义的Set就是一个泛型类型的结构体。
泛型类型包括类(class),枚举(enumeration)和结构体(struct),它们可以操作任意类型,就像swift中Array和Dictionary等集合类型所能做的功能一样。
现在,我们把文章所提及的知识点总结一下,使用一个简单的返利,看看使用泛型我们可以做什么事情。我们有一个包含字符串的数组,并定义一个函数来返回一个随机的字符串,如下代码所示,
struct FactBook {
let facts = ["aFact", "anotherFact", "blah blah"]
static func getRandomFact() -> String {
let randomIndex = Int(arc4random_uniform(UInt32(facts.count)))
return facts[randomIndex]
}
}
如果我们想定义一个通用的结构体,可以用它返回一个随机的值,不论这个值是String类型还是Int类型。使用泛型可以轻易地解决这样简单的需求,
首先我们定义一个泛型结构体RandomContainer,它基于通用的类型T
,如下代码,
struct RandomContainer<T> {
}
接着,为它添加一个[T]类型的属性,用来存放该在初始化结构体时候传入的数据,如下代码所示,
struct RandomContainer<T> {
let items: [T]
init(items: [T]) {
self.items = items
}
}
最后,我们定义一个随机函数来返回一个T
类型的值,如下代码所示,
struct RandomContainer<T> {
let items: [T]
init(items: [T]) {
self.items = items
}
func getRandom() -> T {
let randomIndex = Int(arc4random_uniform(UInt32(items.count)))
return items[randomIndex]
}
}
我们来创建三个数组,它们分别是包含String和Int的数组,并将该数组作为RandomContainer结构体初始化的入参传入,并随机返回一个数组中的值,如下代码所示,
let randomIntegers = RandomContainer(items: [1,2,3,4,5,6,7])
randomIntegers.getRandom()
let randomStrings = RandomContainer(items: ["a", "b", "c", "d", "e"])
randomStrings.getRandom()
总结
尽管这是一篇简短的教程,但它应该能够给读者一个不错的概述,那就是泛型是怎样帮助减少冗余代码、编写更具灵活性的函数和类型的。
泛型并不是swift独创性的概念,在Java等其他语言中也有泛型的概念,但由于在OC中没有泛型,所以对于大多数iOS平台的开发者来说泛型是有点陌生的概念,现在笔者在使用swift编程,也感觉到使用泛型编码所带来的很实在的好处。希望读者也能习惯使用泛型,写出更好更简洁的swift代码。
参考链接
- https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Generics.html
- http://swiftyeti.com/generics/
- https://milen.me/writings/swift-generic-protocols/
公众号
微信扫描下方图片,欢迎关注本人公众号foolishlion,咱们来谈技术谈人生,因为这又不要钱,
foolishlion.jpg