初步理解Swift中的泛型

时间:2021-11-11 14:18:20

如果你已经动手写过Swift的程序,相信你已经了解了Swift语言的知识,比如如何写类(class)和结构体(struct)。但Swift可没这么简单,呵呵呵。这篇教程主要讲述Swift的一个强力的特性:泛型。这个特性在很多程序设计语言里都非常受欢迎。

对于类型安全(type-safe)语言,一个常见的问题就是如何编写适用于多种类型输入的程序。想象一下,两个整型数相加和两个浮点数相加的程序看起来应该非常类似,甚至一模一样才对。唯一的区别就是变量的类型不同。

在强类型语言中,你需要去定义诸如addInts, addFloats, addDoubles 等方法来正确地处理参数及返回值。

许多编程语言已经解决了这个问题。例如,在C++中,使用Template来解决。而Swift,Java和C#则采用了泛型来解决这个问题。泛型,也是这篇文章要重点介绍的。

在这篇文章中,你将会学到Swift中如何使用泛型,也许你已经接触过,也许没有,不过没关系,我们会来一一探索。然后,我们会创建一个Flicker图片搜索应用,这个应用使用了自定义的泛型数据结构来保存用户搜索的内容。

备注:本文假设你已经对Swift有基本的了解或者有过Swift开发经验。如果你第一次接触Swift或者对Swift不是太了解,建议你首先阅读下other Swift tutorials

泛型介绍

也许你不知道这个术语,但相信你已经在Swift中见到它了。Swift中的数组和字典类型就是使用泛型的经典例子。

Object-C开发者已经习惯使用数组和字典去保存多种数据类型。这种方式提供了很大的灵活性,但是谁又能知道一个API返回的数组里面到底是啥(数据类型)呢?你唯一能做的就是查看文档或者查看(方法的)变量命令(这也是另外一种文档哟!)。即使你查看了文档,你也不能保证程序在运行期不产生bug或者其他异常。

相比Object-C,Swift中的数组和字典都是类型安全的。一个Int型数组只可以保存Int而不可以保存String。这意味着你不用再查看文档啦,编译器就可以帮你做类型检查,然后你就就快可以愉快地coding了!


例如,在Object-C的UIKit中, 在自定义的View里面处理触摸事件可以这么写:
 

复制代码 代码如下:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;

上述方法里面的set只可以保存UITouch实例, 因为文档里面就是这么说的。由于这个集合里面可以放任何对象,所以你需要在代码里面进行类型转换,也就是说把touches里面的对象转为UITouch对象。

当前Swift的标准库里面没有定义集合对象,但是你可以使用数组来代替集合对象,你可以用swift重写上面的代码:
 

复制代码 代码如下:
func touchesBegan(touches: [UITouch]!, withEvent event: UIEvent!)

上面的代码明确告诉你 touches数组只可以包含 UITouch实例, 否则编译器就会报异常。这样一来,你就再不用去做那烦人的类型转换了,因为编译器给你做了类型安全检查,保证数组里面只允许有 UITouch对象。


简要说来,泛型为类提供了一个类型参数。所有的数组都有相同的作用,即按顺序储存变量的数值,泛型数组除了多了一个类型参数之外,没有其他的不同之处。或许这样想更容易理解:你将要应用在数组上的各种算法和储存的数值类型无关,因此这些算法对于泛型数组和非泛型数组都适用。

既然你已经明白了泛型的基础知识和用法,那我们就开始把它应用在一个具体的例子上吧。
泛型实例

为了测试泛型,你将会编写一个在Flickr上搜索图片的应用。

首先请下载这个程序雏形,并尽快熟悉里面主要的类。其中Flickr类用于和Flickr的API交互。请注意这个类里面包含了一个API key(通常用于用户授权—译者注),但如果你想要扩展这个应用的话可能需要用自己的key,注册请点我。


构造并运行这个应用,你会看到这个:

初步理解Swift中的泛型

好像什么都没有?别急,用不了多久你就可以让它帮你抓取可爱的喵图了!
有序字典(原文Ordered Dictionaries)

你的应用会根据每个用户的查询情况下载图片,按照图片被搜索到的频率由高到低排序并显示。

但如果用户对同样的关键字搜索了两次会怎样?如果这个应用能显示上次搜索的结果就好了。

或许用数组来实现这个功能也行得通,但为了学习泛型,你将会使用一个全新的数据结构:有序字典。


和数组不同的是,包括Swift在内地很多编程语言和框架都不保证集合(sets)和字典(dictionaries)的数据存储顺序。有序字典和普通的字典类似,不同之处在于它的key是有序的。你将会用这个特性,根据搜索关键字按顺序存储搜索结果。这样存储的好处是可以快速查询并更新图片列表。

一个草率的想法是自定义一个数据结构处理有序字典。但是你需要更加有前瞻性才行!你必须考虑到如何让你的应用在未来几年内都能正常工作!因此在这里使用泛型再合适不过了。
初始数据结构

点击“文件\新建\文件...”新建一个文件,并选择“IOS\Source\Swift File”。点击“下一步”并把这个文件命名为“OrderedDictionary”。最后,点击“创建”。

你会得到一个空的Swift文件,加这样一段代码进去:
 

复制代码 代码如下:
struct OrderedDictionary {  }

到现在为止应该都没有什么问题。通过语义可以看出这个对象是一个结构体。


注意:总之,值的语义可以想象为“复制、粘贴的行为”,而不是“分享、参考的行为”。值的语义带来一系列的好处,例如不用担心一段代码无意地修改你的数据。了解更多,点击"Swift by Tutorials"的第三章节:类和结构体。

现在你需要将其一般化,以便它能够装载你需要的任何类型的数据。通过下列改变你对Swift中“结构”的定义:
 

复制代码 代码如下:
struct OrderedDictionary<KeyType, ValueType>

在尖括弧中的元素是通用类型的参数。KeyType和ValueType不是他们自身的类型,而是你可以使用在结构里定义取代的类型。现在就简洁清新许多了!

最简单的实现一个有顺序的字典是保持一个数组和一个字典。字典中将会装载衍射,而数组将装载keys的顺序。


在结构体内部的定义中,加入以下的代码:
 

复制代码 代码如下:
typealias ArrayType = [KeyType]typealias DictionaryType = [KeyType: ValueType] var array = ArrayType()var dictionary = DictionaryType()

这样声明有两个目的,就像上例描述的,有两种类型的用于给已经存在的类型的取新的名称的别名。在这,你将分别地为后面的数组和字典赋值了别名。声明别名是将复杂类型定义为更短名称的类型的一种非常有效的方式。

你将注意怎么样从结构体中定义用“KeyType”和“ValueType”的参数类型中替换类型。上例的"KeyTypes"是数组类型的。当然这是没有这样的类型的“KeyType”;当在一般的实例化时,将替代Swift像对OrderedDictionary的类型的一切类型通过。

就因为这样,你将会注意到编译错误:
 

复制代码 代码如下:
Type 'Keytype' does not conform to protocol 'Hashable'

或许你会诧异怎么会这样?请再观察下Dictionary的继承者:
 

复制代码 代码如下:
struct Dictionary<KeyType: Hashable, ValueType>


除了在KeyType之后的HashTable, 其他的都和OrderedDictionary的定义特别的相似。在分号后面为KeyType声明的Hashable,一定符合Hashable的协议。这是因为字典需要为hash key实现。

用这种方式约束泛型参数是非常常见的。例如,你想要依据你的应用使用参数做什么,来约束值的类型以,确保相等性、可打印性协议。

打开OrderedDictionary.Swift,用下例来取代你对结构体的定义:
 

复制代码 代码如下:
struct OrderedDictionary<KeyType: Hashable, ValueType>

这样为OrderedDictionary声明KeyType,必须符合Hashable。这就意味着,无论KeyType变成什么类型,都可以接受为没有声明的字典的KEY。

这样,文件再次编译,将不会报错!

Keys, Values 和所有的这些趣事

如果不能为字典添加值,那么字典有什么作用了?打开OrderedDictionary.swift,在你的结构体定义中添加以下函数:
 

复制代码 代码如下:
// 1mutating func insert(value: ValueType, forKey key: KeyType, atIndex index: Int) -> ValueType?{
  var adjustedIndex = index
   // 2
  let existingValue = self.dictionary[key]
  if existingValue != nil {
    // 3
    let existingIndex = find(self.array, key)!     // 4
    if existingIndex < index {
      adjustedIndex--
    }
    self.array.removeAtIndex(existingIndex)
  }   // 5
  self.array.insert(key, atIndex:adjustedIndex)
  self.dictionary[key] = value
   // 6
  return existingValue}

下面介绍一些新的特性。让我们一步一步来介绍:

  •     插入一个新对象的方法,insert(_:forKey:atIndex),需要三个参数:一个特别的key的值,插入一对key-value的索引。这是你之前没有注意到的一个关键字:改变。
  •     结构体的设计是默认不变的,这意味着通常你在实例化的方法中,不能改变结构的成员变量。这十分有限,你能添加改变的关键字,并告诉编译器这个方式在结构体中是允许改变的。这将帮助编译器做出决定什么时候复制结构体(他们是写时复制的),也有助于API的编档。
  •     你为字典的索引器输入一个如果已经存在,那么返回已存在的值的key,这个插入方法模拟字典更新值相同的行为,因此为这个值保持已经存在的值。
  •     如果这有一个已经存在的值,只有这样函数才能为这个值在数组里找出索引。
  •     如果这个已经存在的key在插入索引的之前,这时你需要调整插入的索引,因为你需要移除已经存在的key。
  •     你将适当地更新数组和字典。
  •     最后,你返回已存在的值,当这或许没有已存在的值,这个函数返回一个可选的值!


现在你可以为字典添加移除值?

像下列对OrderedDictionary结构体的定义的函数:
 

复制代码 代码如下:
// 1mutating func removeAtIndex(index: Int) -> (KeyType, ValueType){
  // 2
  precondition(index < self.array.count, "Index out-of-bounds")   // 3
  let key = self.array.removeAtIndex(index)   // 4
  let value = self.dictionary.removeValueForKey(key)!   // 5
  return (key, value)}

现在再让我们一步一步分析:

1.这是改变结构体状态的函数,removeAtIndex的名称需要和数组的方法匹配。恰当的时候,考虑使用镜像系统库中API是不错的选择。这样帮助开发者在他们的工作平台里,非常容易地使用你的API。

2.首先,你需要检查索引,观察他们是否是在大量的数组里。尝试着从未声明的数组中移除越位的元素,将会导致超时错误,所有在这时检查将会更早符合这样的情况。你或许在Objective-C中使用断言函数;在Swift中断言也是可使用的。但是前提是在释放的工程中是活动的,否则你运行的应用的将会终止。

3.接着,当同时从数组中移除值时,你在给定的索引中数组中获得值。

4.然后,你从字典中为这个key移除的值,同时也会返回这个值。或许在给出的key中,字典也没有相应的值,所以removeValueForKey返回一个可选的。这种情况下,你知道字典将会为给出的key,包含一个值,因为这是唯一的自己给字典添加值的方法--insert(_:forKey:atIndex:),这时你可以选择使用“!”,表明这将会有正义感值。

5.最后,你在一个元组返回的key和value。数组的removeAtIndex和字典的removeValueForKey是一样的返回已存在的值功能。

值的读取跟写入

把值写入字典(dictionary)是没问题了, 可是这样还不够! 你还需要实现一个方法(method) 从字典中读出相应的值.

打开 OrderedDictionary.swift 文件, 然后把下列代码添加到结构定义(struct definition)当中, , 就放在 thearrayanddictionaryvariable 声明的下面:
 

复制代码 代码如下:
var count: Int {
  return self.array.count}

这个常用的属性, 用来算出字典里面有几条记录. 只要返回数组的 count 属性的中值就可以了!

接下来, 就是如何访问(Access)字典中的记录了(Element). 我们可以通过下标(Subscript)来访问, 代码如下:
 

复制代码 代码如下:
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 let index = find(self.array, key) {
    } else {
      self.array.append(key)
    }     // 5
    self.dictionary[key] = newValue 
  }
}

我们详细解释下这段代码:

  •     上面代码注释中标有 1 的那一段:跟 func 和 var 类似, subscript 也是个关键字, 通过它定义下标.  参数 key 是出现在中括号中的那个对象.
  •     注释中标有 2 的那一段: 下标由 setter 跟 getter 两部分组成. 本例同时定义了 setter (代码中的 set  ) 跟 getter (代码中的 get ). 当然, 不是每个下标都要同时定义 setter 跟 getter.
  •     注释中标有 3 的那一段: getter 比较简单, 只要通过参数 key, 在字典中找到相应的值即可. 字典返回的是可选值(optinal), 如果 key 不存在, 该值为 nil.
  •     注释中标有 4 的那一段: setter 就复杂些. 首先要检测这个 key 在有序字典里面是不是已经存在. 如果不存在, 则把 key 添加到数组中. 由于我们需要把 key 添加到数组的尾部, 所以这里调用的是 append 方法.
  •     注释中标有 5 的那一段: 把值添加到字典中. 这里用隐性命名的变量 newValue 获取传递过来的值.

就像用 Swift 自带的字典类的下标那样去, 你可以通过 key 来查找某个值. 可是如果我们需要像访问数组那样, 用下标索引(index)访问某个值, 该怎么办呢? 既然是有序字典, 没有道理不能通过下标索引一个一个的按顺序访问.


结构体跟类可以定义多个参数类型不同的下标(subscript). 把下列代码添加到结构定义的底部:
 

复制代码 代码如下:
subscript(index: Int) -> (KeyType, ValueType) {
  // 1
  get {
    // 2
    precondition(index < self.array.count,
                 "Index out-of-bounds")     // 3
    let key = self.array[index]     // 4
    let value = self.dictionary[key]!     // 5
    return (key, value)
  }}

这段代码跟前面那段类似, 不同的是参数类型变成了 Int. 因为我们现在要实的功能是现像数组那样, 使用下标索引访问有序字典. 不过这次返回的是由 key 跟 value 组成的一个元组(tuple). 因为有序字典就是由这样一个一个的元组构成的.

下面具体解释下这段代码:

  •     这个下标只定义了 getter. 当然你也可以把 setter 加上. 不过要注意先检查 index 会不会越界.
  •     index 的值不能超出数组的界限, 也就是字典元组的个数. 我们可以利用 precondition 提示开发人员, 程序出现越界访问.
  •     用 index 从数组中读出 key.
  •     再用 key 从字典中读取 value. 需要注意的是, 由于数组中的每一个 key 跟字典的 value 是一一对应的, 所以这里使用符号 ! (unwrapped) 对读出来的 value 拆包.
  •     最后, 返回一个包含 key 和 value 的元组.


挑战: 为上面那个下标实现 setter . 可以参考前面的例子.
提示 1  


 注意, newValue 是个包含 key 跟 value 的元组.

 
提示 2  


 下列代码可以将值从元组中提取出来:
 

复制代码 代码如下:
let(key, value) = newValue

 

也许你会好奇, 如果 KeyType 是 Int 型的, 会出现什么问题? 使用泛型的好处是, 不管是什么类型, 只要能算出哈希值(hashable)的就行,  所以 Int 当然也能用.  问题是, 当 key 也是 Int 型的时候, 这俩个下标该怎么区分呢?

这就需要我们给编译器提供更多的类型信息. 让它知道在什么时调用哪个下标. 比如我们定义的这两个下标, 返回的类型不一样. 如果你用 key-value 类型的元组给它赋值, 编译器就会自动调用那个数组式(array-style )的下标.

在项目中测试

让我们在实际项目中,实验编译推断使用的下标函数,以及一般情况下,OrderedDictionary是怎么工作的。

通过点击"文件"、"创建"、"文件",新建一个项目,依次选择"IOS"、"Source"、"Playground",再点击下一步。然后点击创建。

你必须得这样操作:复制和粘贴OrderedDictionary.swift整体到新建的项目中。因为不能在写教程时你的应用模型中”看见”代码

注意:这有一个解决方法,可以取代复制、粘贴的方式。如果你需要将你应用的代码加入到一个框架中,你的项目将接受的你代码,就像Corrine Krych指出的这样。


现在,在你的项目底部加入下列的代码:
 

复制代码 代码如下:
var dict = OrderedDictionary<Int, String>()dict.insert("dog", forKey: 1, atIndex: 0)dict.insert("cat", forKey: 2, atIndex: 1)println(dict.array.description
        + " : "
        + dict.dictionary.description) var byIndex: (Int, String) = dict[0]println(byIndex) var byKey: String? = dict[2]println(byKey)

在侧栏中(或者通过视图/助理编辑/显示助理编辑/),你将可以看到println()函数输出的内容:

初步理解Swift中的泛型

在这个例子中,字典有一个整数型的key,所以编译器会审查被使用分配决定使用哪个下标变量的类型。如果被使用的下标是一个(Int, String)的byIndex,编译器会匹配期望的返回值类型,使用数组类型的索引的下标。

如果你想从一个 byIndex 或者 byKey的变量中,移除类型的定义。编译器将会报错,表明编译器不知道使用哪一个下标。


小贴士:由于编译器是按照类型推理来工作的,所以需要明确地表示出类型。当存在多个有相同的争议的返回值类型的函数时,调用者需要具体化。需要注意:Swift中函数,可以“建-破”改变。

通过在项目中,对有顺序的字典的实验中,你可以发现他的工作原理。在重返app之前,尝试从中添加、移除、以及改变key和value的类型。现在,你可以在你的顺序字典中读、写操作!但要小心你的数据结构。现在你可以通过app来感受其中的乐趣了!

添加图片查找

现在是时候让你回过头来注意手中的app了。打开 MasterViewController.swift。在两个@IBOutlets 的下面,添加下列变量的定义:
 

复制代码 代码如下:
var searches = OrderedDictionary<String, [Flickr.Photo]>()

你或许在困惑,为什么Flickr.Photo的类型中有个句号。那是因为Photo是在Flickr类的内部定义的类。在Swift中,这样的层次结构是非常有利的。它将有助于类的名称简短化。在Flickr的内部,你可以单独使用Photo类,因为上下文关系告诉了编译器这是什么。这是顺序化字典查询用户订阅的Flickr的功能。真像你看到的,包含查询的字符串,和Flickr.Photo数组,或是从Flickr API 中返回的照片。注意,你在尖括号里给出的key和value,将成为在具体实现中KeyType和ValueType的参数类型。


接下来,找到表格视图数据源的tableView(_:numberOfRowsInSection:)方法,然后把它改为如下所示:
 

复制代码 代码如下:
func tableView(tableView: UITableView,
               numberOfRowsInSection section: Int) -> Int{
  return self.searches.count}

这个方法使用有序字典来告诉表格视图有多少行。接着,找到表格视图数据源的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. 首先,从UITableView中挪出一个单元格。你需要把它直接转换为UITableViewCell,因为dequeueReusableCellWithIdentifier仍旧返回AnyObject(id in Objective-C),而不是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)       // 5
      self.tableView.reloadData()
    }
  }}

当用户点击查询按钮时,这个方法将会被调用。下列就是在这个方法中,你正做的事:

1.你第一反应是放弃使用查询框以及键盘。

2.然后,你又会使用搜索框查询你输入的文字,为查询的文字而使用Flickr类来寻找。Flickr的查询方法是:查询术语,关闭执行查询成功或者失败。通过参数关闭:要不是错误,那就是结果。

3.在错误的情况下,不会发生任何事。但是你可以通过使用警报来提示错误,但是现在我们可以简化这样的操作。代码需要在这时暂停会儿,告诉Swfit编译器你的错误没有任何反应的动机。

4.如果查询有效,将会在查询结果中显示相关的值。你将查询的术语作为key加入到顺序字典中。如果已经在字典中存在了,将会把他放入到list的顶部,然后用最后的结果更新整个字典。

5.最终,由于你有新的数据,将再次加载table view。

哇!你的app将可以用于查询图片了!


构建并运行app,做几次查询。你将会看到下面这样的一些东西:

初步理解Swift中的泛型

现在再用与之前搜索词不同的另外一个进行查询. 你将会看到它跳到了顶部:

初步理解Swift中的泛型

选择一个查询结果点击去,你会发现它并没有显示照片。现在是时候修复这个问题了!
给我看照片!

打开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    }
  }}

这是用了同创建单元项时访问被排好序的查询结果字典一样的方法. 尽管没有使用关键词(检索词), 你也可以用下划线来显示出元组的这个部分不需要被绑定到一个本地变量.


构建并运行app,做一次查询然后点进去。你会看到像下面这样的东西:

初步理解Swift中的泛型

你好,小猫猫! 你有没有想要发出快乐的呼声呢? :]