现在,你已经知道了Swift中如何实现基本的类和结构,但Swift的强大远不止如此。本章要讲的是Swift的另一个非常强大的语法:泛型。
对于类型安全的语言来说,都有一个常见的问题。想写一个作用于一种类型的代码,但同时又能对另一种类型起作用。想象下,一个函数添加两个整数,一个函数添加两个浮点数。这两个函数看起来其实是一样的,唯一的区别在变量的类型不同。
在强类型语言中,你可能需要分别定义函数如addInts,addFloats,addDoubles等等。每个函数都能传入正确的参数返回正确的类型。
许多语言都有了解决这个问题的方案,例如c++用的是模板。Swift和java一样,使用泛型。在这一章中,你可以了解到泛型的相关内容,然后开发一个网络相册照片的搜索应用。自定义通用的数据结构来记录用户的搜索词。
现在开始吧!
Introducing generics - 引入泛型
你可能还不认识泛型,但是在本书中你已经看见过并使用过了。数组和字典便是经典的安全类型语言的泛型案例。
Object-C开发员习惯在一个相同的集合数组或字典中保存不同类型的对象。这确实提供了不小的灵活性,但是你怎么知道从一个api返回的数组都保存了些什么?你只能查看文档或是变量名来猜测。即使是文档(没有丁点错的文档),也没有办法预防某些集合在运行中的意外情况而导致里面保存的数据类型被改变。
Swift类型化了数组和字典 。让int数组只能持有int,绝不会包含有一个字符串。这意味着你可以编写代码文档让别人了解你的数组,并让编译器给你做数组元素的类型检查。
例如,在Object-C的UIKit中,下面的方法在自定义的view中处理触摸手势:
(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
set集合在该方法中已知的只有UITouch实例,但也只是因为文档是这么告诉我们的。没有办法阻止其他的对象进入这个集合,你通常需要将touches实例化有效的作为UITouch的对象。
Swift没有像上面那样定义一个set集合,然而当你使用一个数组的集合时,你可以将上面的方法修改为:
func touchesBegan(touches: [UITouch]!, withEvent event: UIEvent!)
这便告诉你touches数组只包含有UITouch实例,如果通过别的代码调用此方法则编译器抛出错误。编译器不仅控制了在touches数组中的类型,而且你不再需要将数组元素UITouch实例化。
总的来说,泛型提供了作为参数的类型限制。所有的数组采取同样的方式,泛型会控制参数的值类型,将值保存在一个列表中。如果你这样想会发现泛型很有用:因为你的数组中不会有不知道的类型元素,所以所有的有值的数组,你都可以放心的使用。
现在你已经有了对泛型的基本认识与使用,是时候将他们应用到一个具体的场景中了。
Generics in action - 泛型的运用
泛型练习,需要建一个应用程序用于搜索图片。为了你能快速进行学习,资源文件夹中提供了开始练习的项目工程。编译并运行代码,可以看到运行如下:
界面上貌似毛都没有一根,别担心,你马上就会有照片在上面展示。
Ordered dictionaries - 序列化字典
你的应用将为每个用户查询并下载图片。最近的搜索结果将在列表的最顶部显示。但如果你的用户同一个搜索词用了两次呢?如果应用程序能用最新返回的结果来替换掉列表顶部最近的旧数据就好了。你可以使用数组结构来开发,但是为了学习泛型,我们将创建一个新的集合:有序的字典。
在许多的语言(包括Swift)和框架中,sets集合和字典都不像数组一样可以以顺序保存。有序字典其实就像普通的字典,不过定义的key是一个有序的值。你可以使用这个功能来存储搜索词对应下的搜索结果,让你能快速的找到结果,同时也可以维护订单tableView的视图。
The initial data structure - 初始化数据结构
添加一个新的文件点击File\New\File… ,然后选择iOS\Source\Swift File. 再点击Next并给文件命名为OrderedDictionary 。
你将看到一个空的Swift文件,添加如下代码:
struct OrderedDictionary { }
到目前为止都还没有什么令人惊奇的。这个对象应该是个结构因为他是个值语义的对象。详见第三章类和结构的区别。
现在你需要让他可以容纳任何你想要的类型的值,修改结构定义如下:
struct OrderedDictionary<KeyType, ValueType>
在尖括号里的元素是泛型的类型参数。KeyType和ValueType本身不是类型,而是成为你在定义结构时的类型参数。等下你就会明白。
最简单的有序字典的实现方式是使用数组和字典。字典负责键值对的映射,数组负责key键的顺序。
在结构的定义中添加如下代码:
typealias ArrayType = [KeyType]
typealias DictionaryType = [KeyType: ValueType]
var array = ArrayType()
var dictionary = DictionaryType()
这里声明了两个属性,为了描述方便,给两个类型设置了别名。在这里,你给的数组和字典别名分别支持数组和字典。类型别名是个伟大的构思,让你可以给一个复杂的类赋予一个简短的名字。
注意你可以使用结构的类型参数KeyType和ValueType。这个数组是一个KeyType的数组,当然,其实并没有一个类型叫KeyType。而是Swift把他处理为在实例化泛型OrderedDictionary的任何一种类型。
同时,你会看到编译器错误:
这可能有点意外,看下系统自带的字典是如何实现的。
struct Dictionary
struct OrderedDictionary<KeyType: Hashable, ValueType>
这声明了KeyType在OrderedDictionary中必须符合哈希。也就意味着任何类型的KeyType都可以被接受作为字典的关键字。
现在编译文件不会再报错了。
Keys, values and all that jazz - 关键词,值的处理
如果你不添加值怎么使用字典呢?打开OrderedDictionary.swift 并将下面的函数添加到你定义的结构中
// 1
mutating func insert(value: ValueType, forKey key: KeyType, atIndex index: Int) -> ValueType?
{
var adjustedIndex = index
let existingValue = self.dictionary[key]
if existingValue != nil {
let existingIndex = self.array.indexOf(key)
if existingIndex < index {
adjustedIndex -= 1
}
self.array.removeAtIndex(existingIndex!)
}
self.array.insert(key, atIndex: adjustedIndex)
self.dictionary[key] = value
return existingValue
}
这里引入了几个新东西,让我们分步了解下:
1.插入方法(_:forKey:atIndex) 插入一个新的对象,这个方法需要三个参数:一个特定的坐标值index和一个键值对,这里有个关键词mutating你可能没有见过。
结构默认是不可修改的,也就是说你通常没法在一个实例方法中修改结构里的成员变量。因为这个限制非常不方便,所以你可以添加关键字mutating告诉编译器该方法可以改变结构的状态。这有利于编译器处理结构的复制问题(什么时候复制,什么时候重新写入),也有利于API文档的开发。
2.你通过类型key找到字典对应的值。如果这个key下面有值则返回对应的index。这个方法模拟的是字典更新值的方法,因此需要通过这个key来保存值。
3.如果这个key对应的值存在,则继续用key来获取在数组中的坐标
4.如果对应值在插入索引值之前就存在了,你需要删除掉数组中现有的值用于保存新的索引值。
5.适当的更新数组和字典的内容
6.最后,返回存在的值。因为对应的key下面可能没有值,所以返回的是一个可选类型的值
现在你可以向字典中添加内容了,那删除可以吗?
在OrderedDictionary结构定义中添加如下函数:
// 1
mutating func removeAtIndex(index: Int) -> (KeyType, ValueType) {
// 2
precondition(index < self.array.count, “Index超出边界”)// 3
let key = self.array.removeAtIndex(index) // 4
let value = self.dictionary.removeValueForKey(key)
// 5
return (key, value)
}
又该分步讲解了:
1.再一次的,因为这个函数要修改结构的内部状态,所以用mutating声明这个函数。取名和数组中删除的名字一样叫removeAtIndex。适当考虑使用系统api的方法名,可以让开发人员更易上手。
2.首先,你先要检查下index有没有超过数组的边界。尝试删除越界的元素会在运行时发生错误,所以一定要在使用前早点检查下。在Object-C中通常使用断言assertions.断言在Swift中同样有效。但前提是只能在发布应用前使用,因为断言失败则会使程序崩溃终止。
3.接着你从数组中获取到字典中需要的关键字key,同时删除数组中的值。
4.然后,你从字典中删除对应key下的值,同时也将值返回。字典可能包含有一个不存在对应key,所以removeValueForKey的返回值是可选的。在现在这个案例中,你知道字典一定包含了一个对应key的值。因为上面的插入方法是你唯一给这个字典添加值的入口。所以亦可以直接用强解!,因为你知道数组有值,则字典对弈的一定会有值。
5.最后,返回一个包含了key和值的元组。数组removeAtIndex和字典removeValueForKey的相似之处在于他们都返回了现有的值。
Accessing values - 访问值
你现在可以往字典中写入内容但却无法从中读取数据,这对于一个数据结构来说显然是不合理的!你现在要做的就是添加一个方法用来从字典中读取数据。
打开OrderedDictionary.swift并且将下面的代码添加到结构定义中,就放在刚刚的数组和字典变量声明的下边:
var count: Int {
return self.array.count
}
这是一个用来计算有序字典的计算属性。一个这样的数据结构通常需要这样的一个统计信息。字典的数量和数组的总数是匹配的,所以这个方法很好实现。
接着,你需要个方法去访问字典的元素。在Swift中,你访问一个字典通常使用下标语法,就和下面一样。
let dictionary = [1: "one", 2: "two"]
let one = dictionary[1] // Subscript
你现在应该对这个语法非常的熟悉,但是像这样的语法只能在字典和数组中使用。那你在自己的类和结构中如何实现这种方法呢?幸运的是,在Swift中,可以非常方便的在自己定义的类中使用下标语法。
在结构定义的代码下面添加代码:
// 1
subscript(key: KeyType) -> ValueType? {
// 2(a)
get { // 3
return self.dictionary[key]
}
// 2(b)
set { // 4
if self.array.indexOf(key) != nil
{
} else {
self.array.append(key)
}
// 5
self.dictionary[key] = newValue
}
}
下面来代码分析:
1.这是添加下标语法的方法,可以使用下标关键字subscript而不是用函数或变量来实现。在这里的参数代表着你希望在方括号中出现的类型。
2.下标语法中可以包含有setter和getter方法,就像计算属性那样。注意上面的(a)和(b)就是分别定义了闭包函数。
3.getter方法比较简单:需要用给定的key来访问字典的值。字典的下标返回的是一个可选类型的值,以便处理对应的key下面没有值的情况。
4.setter比较复杂:首先,他先检查有序字典中是否有这个对应的key,如果不存在,需要将这个key添加到数组中。将新的key添加到数组的结尾是有意义的,所以用append来往数组中添加内容。
5.最后,你用这个key往字典中添加新的内容,通过隐式传递一个叫newValue的变量赋值。
现在你可以像一般的字典一样用索引来访问这个有序字典了。但是只能使用特定的key才能访问字典,如何才能像数组那样通过index值来访问。对于一个有序字典来说,用index来访问元素内容也是同样重要的。
类和结构可以定义多个不同类型参数的下标。在结构的定义下面添加如下代码:
subscript(index: Int) -> (KeyType, ValueType) { // 1
get { // 2
precondition(index < self.array.count, "Index超出边界值")
// 3
let key = self.array[index] // 4
let value = self.dictionary[key]!
// 5
return (key, value)
}
}
这和你前面添加的下标语法是十分类似的,除了传入的参数类型是Int,其他的用法一样。因为这个参数会用来作为数组的索引,检查是否越界。这一次返回的是包含有key和value的元组,因为这就是你OrderedDictionary给定的结构值。
下面是代码详解:
1.这个下标语法中只有getter方法,当然你也可以实现一个setter方法。首先需要检查index的范围是不是在数组的范围内。
2.索引index必须在数组的边界值内,使用前需要用注意是是否会访问了数组边界值外的内容。
3.用index从数组中找到对应的key。
4.用获取到的key继续从字典中获取值value。再次需要注意的是,这里用的是强解,因为你知道在数组中的任何一个key在字典中都存在。
5.最后,返回一个包含有key和value的元组。
挑战:在这个下标语法中实现setter方法。就像前面定义的下标语法一样,添加一个setter的闭包函数。
提示:包含有key和value的叫newValue的元组可以用来设置为一个索引。
提示:你可以使用下面这种语法从元组中获取值:let(key,value) = newValue
此时,你可能想知道如果KeyType是Int会发生什么。泛型的好处是允许你任何哈希的类型都可以作为key,包括int。在这种情况下,下标语法如何知道是使用哪个下标方法呢?所以,我们需要给编译器提供更多的信息以便能知道你的意图。所以如果你想用返回一个键值对的元组,编译器就能知道它该使用array-stype的哪个下标语法。
让我们看看他是如何工作的。创建一个新的Playground文件,点击File\New\File…, 选择iOS\Source\Playground ,并命名为ODPlayground。
将OrderedDictionary中的代码全部复制粘贴进来。你必须这样做,因为悲催的是,Playground不能“看到”任何在应用程序模块的代码。现在在Playground的底部添加如下代码
var dict = OrderedDictionary<Int, String>()
dict.insert("dog", forKey: 1, atIndex: 0)
dict.insert("cat", forKey: 2, atIndex: 1)
print(dict.array.description+":"+dict.dictionary.description)
var byIndex: (Int, String) = dict[0]
print(byIndex)
var byKey: String? = dict[2]
print(byKey)
查看底部输出控制台可见:
在这个例子中,因为字典用的是Int类型的Key,所以编译器会根据看到的类型变量来分配使用哪个下标方法。因为byIndex是一个(Int,String)的元组,为了匹配你定义的元组类型的变量,所以编译器知道使用数组那个下标方法来进行返回。
当你尝试删除变量byIndex和byKey的类型定义时,你会看到编译器提示错误,表示编译器不知道该使用哪个下标方法。
技巧提示:对于类型推断。编译器必须要用一个明确的类型表达式来获知类型。当有多个有着相同参数类型但不同返回类型的方法时,需要具体的告知调用哪一个。在Swift中添加的方法可能会在编译的时候选择错方法,所以需要小心注意。
在playground中练习了解有序字典是如何工作的,尝试下添加,删除,更改键和值得类型。然后返回到应用程序继续开发。现在你的数据结构可以读和写入了。是时候将他运用到应用中了。
Aside: Assertions & preconditions - 断言和前置条件
我们在前一节中添加了几个先决条件语句的代码preconditions。用他来检查上下文传递的参数是否有效。断言和前置条件有很多不同的地方。
断言和前置条件都是当你想使用条件之前检查条件是否正确并继续执行代码。如果条件为false,则停止程序运行并让程序崩溃。
两者的区别在于,断言用在版本发布前,而前置条件无所谓。断言是为了捕捉在开发过程中发现的bug,而前置条件是为了当判断的条件不为真时抛出一个致命error的错误。
使用一个断言测试的例子。在控制器中设置一个view的层次结构,如果一个方法依赖的另一个方法已经被执行,则被assert断言,如下:
private func configureTableView() {
self.tableView = UITableView(frame: CGRectZero)
self.tableView.delegate = self
self.tableView.dataSource = self
self.view.addSubview(self.tableView)
}
private func configureHeader() {
assert(self.tableView != nil)
let headerView = UIView(frame: CGRectMake(0, 0, 320, 50))
headerView.backgroundColor = UIColor.clearColor()
let label = UILabel(frame: CGRectZero)
label.text = "My Table" label.sizeToFit()
label.frame = CGRectMake(0, 0,
label.bounds.size.width,
label.bounds.size.height) headerView.addSubview(label)
self.tableView.tableHeaderView = headerView
}
在这个例子中,configureHeader()需要一个已经被创建出来的tableView,所以要在tableView上设置头view时用断言确保这个对象存在。这可以防止在控制器初始化时弄错了调用这些方法的顺序从而调用nil对象引起应用崩溃。
你要用断言必须赶在开发过程中。当你的tableView的头view一直没出现先别急着抱怨是不是你的电脑有问题,你应该去检查下你的断言判断是不是写错了。因为你会在开发的时候找到这些bug,所以在发布的版本中就不需要再检查了,需要去掉断言。很明显你不希望仅仅是一个愚蠢的小错误就会让你的应用程序崩溃不是。
有趣的是编译器在发布应用的时候,允许断言假设的条件是正确的,考虑下下面的代码:
func foo(value: Int) {
assert(value > 0)
if value > 0 {
print("Yes, it's greater than zero”)
} else {
print("Nope")
}
}
这可能看起来有点奇怪,但他能说明了上面的观点。当你传入一个0时,如果是在调试状态,则断言失败,应用程序崩溃。然而在发布状态时编译会发生什么呢?编译器一直允许函数打印输出:Yes, it’s greater than zero。
编译器断言的值大于0,在发布状态时,编译器总是假设断言一直正确。为了优化代码,只要你喜欢,可以删除下面的if语句,因为他假定的值大于0.是不是很聪明,整洁。
正如上文所说,preconditions前置条件也做着和断言一样的事,但是可以在发布状态下使用。当你需要十分确定一个条件时你可以使用它。比如你想要读取一个数组里的值,所以你想要检查获取值的索引值是否有效,如下:
func fetchPeopleBetweenIndexes(start: Int, end: Int) -> [Person] {
precondition(start < end)
precondition(start >= 0)
precondition(end <= self.people.count)
return Array(self.people[start..<end])
}
在这个例子中,前置条件被用来检查函数的输入值是否有效。start必须比end小,start必须大于等于0,end必须小于等于people数组里的总量。
这些检查的代码是有用的,因为无论是在调试阶段还是在发布阶段,一旦你读取一个越界的数组值的时候都会让你的程序崩溃。但是用代码让程序崩溃能获取到更多和错误相关的信息。
断言和前置条件都可以把一个字符串作为第二个参数,用来在你错误时输出你添加的信息。
一般经验来说,执行时使用断言一般在开发过程中获取到错误的提示。前置条件用来防止当继续执行时会造成严重错误的情况,数据的破坏或其他什么的。用前置条件也不错,当你为别人开发代码使用,比如OrderedDictionary,你希望其他程序员在输入无效的输入时会让程序崩溃。
Adding image search - 添加图片搜索
是时候将你的注意力重新转移回程序开发中了。打开MasterViewController.swift并在两个@IBOutlets后面添加变量声明:
var searches = OrderedDictionary<String, [Flickr.Photo]>()
这个有序字典用来保存用户添加到Flickr中的搜索词。正如你看到的,String映射为key,Flicker数组映射为值。Photo是从FlickrApi返回的值。注意到当前的使用方式就和一般的字典一样,尖括号里的是key和value。而这两个属性变成有序字典的KeyType和ValueType来实现。
你可能想知道为什么类型Flickr.Phonto 有一个点在之间。因为Photo是被定义在Flickr类里面的类。在Swift中这样的层级结构很有用,帮助你通过用名称空间结构来保证类的命名足够简洁。在Flickr类中,你可以直接使用photo类中的Photo,因为通过上下文告诉了编译器这是什么。
接着找到被叫做
tableView(_:numberOfRowsInSection:)的TableView
的数据源方法,并进行修改。
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return self.searches.count
}
这个方法现在用OrderedDictionary来告诉tableView有多少行。
接着找到数据源方法
tableView(_:cellForRowAtIndexPath:)
并进行修改
func tableView(tableView: UITableView,cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
// 1
let cell = tableView.dequeueReusableCellWithIdentifier("Cell",
forIndexPath: indexPath) as UITableViewCell
// 2
let (term, photos) = self.searches[indexPath.row]
// 3
if let textLabel = cell.textLabel { textLabel.text = "\(term) (\(photos.count))"
}
return cell
}
讲解下这个方法都做了些什么:
1.首先你从tableView的列表中获取到cell。你需要将他转换为UITableviewCell,从dequeueReusableCellWithIndentifier中返回的是任意的对象(在Object-C中叫id),并不是UITableviewCell。也许以后,苹果会用泛型来重写这个api。
2.然后,你从下标索引中获取到指定行的key和value。
3.最后,你需要设置单元格的文本标签并返回单元格。
现在来点干货。找到UISearchBarDelegate扩展,然后再里面修改方法如下:
func searchBarSearchButtonClicked(searchBar: UISearchBar) {
// 1
searchBar.resignFirstResponder()
// 2
let searchTerm = searchBar.text
Flickr.search(searchTerm!) {
switch ($0) { case .Error:
// 3
break
case .Results(let results):
// 4
self.searches.insert(results,
forKey: searchTerm!,
atIndex: 0)
self.tableView.reloadData()
// 5
}
}
}
当你点击搜索按钮时这个方法被调用。说下这个方法做了些什么:
1.你让search bar注销掉第一响应者的身份,隐藏掉键盘。
2.然后用search栏上现在的文本作为变量的值。Flickr的搜索需要一个搜索词,所以用一个闭包来执行搜索的成功或失败的情况。这个闭包只有一个参数:一个error错误或者返回结果的枚举。
3.在错误的情况下,不进行任何操作。当然如果你愿意,你也可以在这里添加一个提示的对话框,但是我们现在就先这样吧,尽量保持代码的简洁。我们现在在这里告诉Swift的编译器,如果返回的是error,则什么都不做。
4.如果搜索成功,搜索返回的结果关联到SearchResult的枚举result中。将结果添加到有序字典中,搜索词作为key,将此搜索词放置于第一位。
5.最后,你需要刷新tableView,因为你现在更新了数据不是。(@ο@) 哇~,你现在可以看到应用程序可以搜索图片了。
运行程序,使用搜索功能,应该能看到下面这样的情况:
这里有两点需要注意。
1.如果访问不到的话可能需要打开vpn。
2.Info.plist修改下本地化,将en改为美国
不断的搜索,你会发现你最新的搜索词在tableView的最顶部。但是现在点开还无法看到图片,是时候来完善下了。
Show me the photos! - 让我们来展示照片
打开MasterViewController.swift 然后找到prepareForSegue并进行修改
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showDetail" {
if let indexPath = self.tableView.indexPathForSelectedRow {
let (_, photos) = self.searches[indexPath.row]
(segue.destinationViewController as! DetailViewController).photos = photos
}
}
}
使用相同的访问方法从tableView的选中cell中获取,因为你不需要搜索词key,所以元组只需要绑定一个局部变量即可。
编译并运行,搜索一个词,然后点击一行内容,可以看到ui如下:
提示一下,因为这个代码比较老了,所以还会提示无法安全访问http,所以在plist文件添加运行访问http
随着搜索的不断增加,搜索列表的内容会越来越长,所以我们再添加一个删除功能。
Deleting searches - 删除搜索
打开MasterViewController.swift 并在viewWillAppear后面添加代码:
self.navigationItem.leftBarButtonItem = self.editButtonItem()
这可以在导航栏的左侧添加一个编辑按钮,接着在viewWillAppear后面添加代码:
override func setEditing(editing: Bool, animated: Bool) {
super.setEditing(editing, animated: animated)
self.tableView.setEditing(editing, animated: animated)
}
允许tableView可以使用编辑功能。
最后,在tableView的代理中找到
tableView(_:commitEditingStyle:forRowAtIndexPath:)
并进行修改:
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
self.searches.removeAtIndex(indexPath.row)
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
}
}
因为你只想要一个删除功能,所以这个方法只检查了删除样式。当用户通过左滑点击了出现的删除按钮后,则这个方法会删除有序字典searchers的值并更新tableView。
恭喜恭喜,你已经完成了本应用的开发了!你现在有了一个有序的字典,并且你可以在任何应用中都可以使用。最重要的是,这个有序结构是通用的,只要支持hashable,所以类型的值都可以做字典的key。
Generic functions and protocols - 泛型函数和协议
你已经见过了泛型的结构和类,你可能还不知道,但是你已经用过了泛型的函数。前面用过的几次find()便是这样。但因为find()已经被废弃了,我现在一直用的indexOf()来替代的。不过无所谓,写出来理解下:
find()的定义:
func find<C: Collection where C.GeneratorType.Element: Equatable> (domain: C, value: C.GeneratorType.Element) -> C.IndexType?
indexOf()的定义:
public func indexOf(element: Self.Generator.Element) -> Self.Index?
好了该讲下find()了:
这是一个全局的函数,注意在他的方法名后面有一个尖括号(就像我们刚刚定义的泛型结构一样)。让他成为一个泛型函数。这里提供了方法内的参数类型(即被搜索的东西)。他也间接的给值定义了类型,泛型类型参数GeneratorType 类型。
简而言之,GeneratorType是集合中用来搜索的值的类型。返回类型也是基于集合中搜索值的类型。
打开OrderedDictionary.swift 并找到(_:forKey:atIndex:)。原来的代码截图:
可以注意到的是用的find()方法中并没有指定类型。
这是因为Swift再次自己推断了类型。泛型可以通过查看第一个参数的类型来推断这个类型(集合中是用来搜索的值)。这是泛型类型中的C类型。
但这个GeneratorType是个什么协议的集合呢?这个协议结合还必须要符合SequenceType协议,这表明类和结构都可以被认为是一个序列。序列是指什么呢?就是像数组这样的。定义的协议如下:
protocol SequenceType {
typealias GeneratorType : Generator
func generate() -> GeneratorType
}
这表示任何SequenceType必须有个类型别名叫GeneratorType,且本身是Generator类型。他还必须有个一用来返回GeneratorType的函数。
实际背后的操作是:当需要迭代一个序列时,他在序列中通过generate()生成一个generator。这个generator有个叫next()的方法让他获取到序列中的下一个对象。generator从序列的开头处开始,所以你可以用他来从到到尾的迭代序列。
提示:Generators在处理很大或者计算开销很高的集合时非常有用。因为他允许你只生成你需要的内容,而不是将所有的集合内容都编译一遍。
那么OrderedDictionary呢?你明显也会考虑他为一个序列(虽然他是个字典,有着key和value)。但他是个有序的字典,所以应该也让他符合SequenceType让你可以轻松的进行遍历。
打开ODPlayground.playground,这个playground是早前创建好的。在OrderedDictionary 结构定义的下边添加代码:
extension OrderedDictionary: SequenceType { // 1
typealias GeneratorType = AnyGenerator<(KeyType, ValueType)>
// 2
func generate() -> AnyGenerator<(KeyType, ValueType)> { // 3
var index = 0
// 4
return AnyGenerator { // 5
if index < self.array.count {
let key = self.array[index]
index += 1
return (key, self.dictionary[key]!)
} else {
return nil
}
}
}
}
这个扩展定义了OrderedDictionary必须遵循SequenceType协议。下面是处理情况:
1.GeneratorType作为SequenceType协议的别名,必须符合Generator。你也可以自己编写一个Generator,但Swift有个标准的称为GeneratorOf的结构,每次都可以执行闭包里的next()。返回的对象是一个泛型的结构。因此在OrderedDictionary中,你设置keyType和ValueType的元组作为泛型的返回值。
2.然后实现generate(),最后的实现需要符合SequenceType。
3.如上面提到的,GeneratorOf的闭包函数中每次都会执行next().在实现的地带中,你可以保存当前的index作为index的变量,因为他是从0开始的。
4.然后你生成并返回一个GeneratorOf。虽然他看起来不太像,但是实际上他调用了GeneratorOf的初始化。只需要一个参数,闭包函数就能每次都调用next()方法了。
5.在generator闭包函数中,你需要检查索引是否在数组边界值内。如果是,则返回key和value在增量索引下的值。如果超出了数组的边界值(即代表迭代结束),返回一个nil。
让我们来看看如何使用这个遍历功能,在playground的最底部添加代码:
for (key, value) in dict {
print("\(key) => \(value)")
}
可以看到控制台输出
1 => dog
2 => cat
就如你所希望的,命令执行正确,每一个键值对的关系也是正确的。
通过实现SequenceType,你让OrderedDictionary实现了枚举遍历的功能。
表急,这里还有点东东!删除在OrderedDictionary SequenceType扩展协议中的别名声明。你会发现playground仍然能够正确的执行。这不是很奇怪吗?这依然是Swift的类型推断功能。他能够推断出别名应该是GeneratorOf<(KeyType, ValueType)>,因为他返回的就是generate()的类型~~。
如果你仔细想想,这个SequenceType 协议其实是一个泛型协议。类型别名GeneratorType和泛型类,结构或函数中的泛型参数非常的像。但是泛型不用用尖括号语法来表示协议,不像java和C#那样。
这主要是语法的原因。协议的定义用来是接口类和结构都遵循。在某种程度上,他们也算是一种“泛型”!相反,Swift分离开了你关心的协议定义和类,结构类型以及其他的事物。
这部分是GeneratorOf存在的原因:他在一定程度上提升了Generator协议。当然,这只是实现了一个这样的Generator,以后你可能经常会使用到。
Where to go from here? - 接着干什么
在本章中,你已经了解了怎样在一个app中写一个泛型的结构。你可以用同样的方法写一个泛型类。根据你其他应用存储数据的需要,你可以重新定制你的有序字典。
你还在本章了解了下下标语法subscripts的用法。下标语法对集合来说非常有用,可以让你轻松的访问元素。在任何类和结构中用下标语法进行访问都是非常直观的。只要能实现,有什么理由不用下标语法来实现呢!
在Swift的代码继续学习中,想想如何让泛型和系统类型让开发更容易些。当用泛型时可以让你兼顾类型的安全与重用性。想象下你如何在项目中使用泛型,你就会发现为什么相比于Object-C,Swift的类型安全语言更好了。