刘毅的技术博客

记录自己的学习生活点滴,也希望和大家交流分享!

Swift by Tutorials--Generics

经过前三章,基本对Swift的基本语法有了较全面的介绍,接下来会分别就Swift比较重要的几个技术要点介绍,这一节将讨论一个比较流行的语言特性,generics,即泛型。对于类型安全的编程语言,希望代码可以在一个场景下运行,但又想要在其他场景中也可以是合法的,比如对于一个加法函数,Int和Float类型的函数形式是一样的,只是变量类型不同,在强调类型的语言中,你必须分开定义这两个方法。很多语言为这一问题提供了解决方案,C++是使用了模板,而Swift、Java、C#是使用了泛型,也就是这一章的主角,配合主题这一章将创建一个Flickr照片搜素App来实践这一技术。

Introducing generics

1.泛型是什么?举例来说Array和Dictionary就是类型安全的泛型应用实例。在OC中Array和Dictionary是可以存放不同类型的对象的,当然这有时是提供了很多方便,但当你去使用一个Array或Dictionary时,你如何知道其中的类型?只能通过文档或其他代码,而且没有任何办法去控制在runtime中出现数据异常。
2.而Swift中对Array和Dictionary中类型是固定的,编译器会完成类型检查,而你的代码本身也对自己做了注释,比较下处理点击的方法在OC和Swift中的区别,在OC中调用这一方法,你一般是需要将set中的对象转化为UITouch类型,而Swift不仅省去你这一操作,代码可读性也更优。

1
2
3
4
//in OC
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
//in Swift
func touchesBegan(touches: [UITouch]!, withEvent event: UIEvent!)

2.所以说泛型就是类似Array这样,所有的Array运作方式都是一致的,都是将数据存在一张有序的表中,但泛型的Array将值的类型作为了参数,也就是不论Array中类型如何,都可以执行Array的方法。

Generics in action

1.本章的实例项目是要从Flickr的搜索接口获取对应图片,并进行展示的一个App,其中网络访问部分大神已经写好了。

Ordered dictionaries

1.第一个需求就是希望将用户最近搜索的图片放在前面,那么如果正常来讲,我们要用Array来存数据源,而不是Dictionary或Set,因为后两者是无序的,而这里为了应用泛型,打算自己创建一个有序的Dictionary,实际上就是想把key作为顺序。

The initial data structure

1.首先声明有序字典为Struct类型,并声明它的泛型类型参数,KeyType和ValueType并不是真实的类型,只是两个你用来替代类型的参数,一般用T来表示,如果单词表示的话用驼峰式大写首字母来表示。

1
2
struct OrderedDictionary<KeyType, ValueType> {
}

2.创建一个有序字典最简单的方法是,在内部同时维护一个Dictionary和Array,这里使用了typealias分别给[KeyType]和[KeyType: ValueType]做了类型名替代,这样下面声明Array和Dictionary就可以直接用替代类型名来定义,同理这个也可以用在替换函数类型和闭包等比较长的类型的替换。

1
2
3
4
5
typealias ArrayType = [KeyType]
typealias DictionaryType = [KeyType: ValueType]

var array = ArrayType()
var dictionary = DictionaryType()

3.对比Dictionary的定义,KeyType需要遵循Hashable协议,因为Dictionary需要对key做hash,所以在定义泛型那儿要加上遵循该协议。

1
struct OrderedDictionary<KeyType: Hashable, ValueType>

Keys, values and all that jazz

1.第一个要加入的方法是insert方法,因为是有序字典,所以有所不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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--
    }
    self.array.removeAtIndex(existingIndex)
  }

  self.array.insert(key, atIndex:adjustedIndex)
  self.dictionary[key] = value

  return existingValue
}

2.有几点需要说明,首先,该方法前的mutating关键字,因为Struct默认是不可变的,也就是是说你不能在实例方法中修改struct的成员变量,加上mutating是为了告诉编译器该方法可以修改struct成员变量,使编译器在适当的时候对struct做copy操作(前面说过,实际上是copy-on-write),同时也增加了可读性。
3.然后是remove方法,这里先对index是否越界做了判断,可以像OC中使用Assertions断言,也可以如下使用precondition,如果失败,会退出App。

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

4.这里在结束后会返回一个元组类型的删除值,使之与Swift的Array和Dictionary的remove方法保持一致。

Accessing values

1.上一节为有序字典添加了写入的方法,接下来添加一些读取的方法,首先是获取count的方法,如下,使用了前面提到的computed property技术。

1
2
3
var count: Int {
  return self.array.count
}

2.在Swift中我们一般使用subscript来访问变量,类似dictionary[1],一般是见于Array和Dictionary,不过我们计划在我们的有序字典也加入这一特性。

1
2
3
4
5
6
7
8
9
10
11
12
subscript(key: KeyType) -> ValueType? {
  get {
    return self.dictionary[key]
  }
  set {
    if let index = self.array.indexOf(key) {
    } else {
      self.array.append(key)
    }
    self.dictionary[key] = newValue
  }
}

3.上述代码就是如何在自己的Struct中加入subscript行为,类似computed property,subscript有两个闭包,分别是getter和setter。
4.因为这是一个有序数组,我们打算让他支持通过index来访问,需要注意的是:一,无论setter,getter都需要判断index是否越界;二,setter中输入的值newValue是一个元组类型,所以需要用let (key, value) = newValue将键值取出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
subscript(index: Int) -> (KeyType, ValueType) {
  get {
    precondition(index < self.array.count, "Index out-of-bounds")
    let key = self.array[index]
    let value = self.dictionary[key]!
    return (key, value)
  }
  set {
    precondition(index < self.array.count, "Index out-of-bounds")
    let (key, value) = newValue
    let originalKey = self.array[index]
    self.dictionary[originalKey] = nil
    self.array[index] = key
    self.dictionary[key] = value
  }
}

5.这里可能有个疑问,就是如果使用者使用Int作为KeyType呢?因为Int也遵循hashable,完全可以作为key,那么编译器如何判断该用哪组方法呢?遇到这种情况,setter方法当然没问题,因为赋值也不同,那么getter方法只能在取值时就声明返回值的类型,这样编译器会通过这个类型选择使用哪个方法。

1
2
3
4
5
6
7
8
9
10
11
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)
//"[1, 2] : [2: "cat", 1: "dog"]"
var byIndex: (Int, String) = dict[0]
print(byIndex)
//"(1, "dog")"
var byKey: String? = dict[2]
print(byKey)
//"Optional("cat")"

6.在使用type interface时,编译器需要明确知道返回值的类型,如果出现上述相同方法,返回值类型不同的情况,必须caller指明类型,否则编译器是不会知道该返回那个值的。

Aside: Assertions & preconditions

1.assertions和precondition都是判断程序是否能继续执行时的判断条件,不同的是,assertion是不会在release build时编译的,而precondition可以;assertion是被用于在开发时获取bug,而precondition是用于当一个条件不满足时,抛出严重异常的。
2.assertion的一个使用场景是有多个创建view的方法共同来构建页面,但其中一些依赖于另一些完成,这时要使用assertion。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
}

3.关于assertion一个有趣的现象是编译器允许在release build时假设assertion是true,有时也会导致一些bug,如下,输入0时,在debug下没问题,会触发断言;而在release中,编译器自动认为assertion是true,然后optimizer就会跳过if,直接进入>0的分支。

1
2
3
4
5
6
7
8
func foo(value: Int) {
  assert(value > 0)
  if value > 0 {
      print("Yes, it's greater than zero")
  } else {
      print("Nope")
  }
}

4.再来看下precondition,它和assertion做的是一样的工作,但是可以在release下运行,使用它是为了确保一些必要的条件,如下例,数组越界即使不加precondition,也会crash,但是通过precondition,可以获取到自定义的log信息,方便调试。

1
2
3
4
5
6
func fetchPeopleBetweenIndexes(start: Int, end: Int) -> [Person] {
  precondition(start < end)
  precondition(start >= 0)
  precondition(end <= self.people.count)
  return Array(self.people[start..<end])
}

5.一般的经验是,在你release时可以跳过,但是希望在debug阶段获取失败信息时使用assertion;而在将会导致数据损坏或者其他严重问题前,使用precondition。同时在你开发一些第三方库时,在容易出现数据输入错误这些位置可以使用precondition来提示开发者。

Adding image search

1.创建App的数据源,使用的就是之前自定义的有序字典,你可能注意到使用了Flickr.photo,Photo是一个定义在Flickr中的类,这样的机制非常有用,在保持类名尽量短的基础上实现了命名空间,在Flickr类中,可以单独使用Photo类。

1
var searches = OrderedDictionary<String, [Flickr.Photo]>()

2.然后实现tableView的委托和数据源协议。

1
2
3
4
5
6
7
8
9
10
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  return self.searches.count
}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell
  let (term, photos) = self.searches[indexPath.row]
  cell.textLabel!.text = "\(term) (\(photos.count))"
  return cell
}

3.然后是UISearchBarDelegate,这里调用Flickr的search方法是使用了Trailing Closures技术,即如果closure作为一个方法最后一个变量,那么可以写到所调用方法的外面(后面),如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
func searchBarSearchButtonClicked(searchBar: UISearchBar!) {
  searchBar.resignFirstResponder()
  let searchTerm = searchBar.text
  Flickr.search(searchTerm!) {
    switch ($0) {
    case .Error:
      break
    case .Results(let results):
      self.searches.insert(results, forKey: searchTerm!, atIndex: 0)
      self.tableView.reloadData()
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
func someFunctionThatTakesAClosure(closure: () -> Void) {
    // function body goes here
}
// here's how you call this function without using a trailing closure:
someFunctionThatTakesAClosure({
    // closure's body goes here
})
// here's how you call this function with a trailing closure instead:
someFunctionThatTakesAClosure() {
    // trailing closure's body goes here
}

Show me the photos!

1.这一节来完成详情页,先是在prepareForSegue方法中设置DetailViewController。

1
2
3
4
5
6
7
8
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
    }
  }
}

Deleting searches

1.为搜索页添加滑动删除功能。

1
self.navigationItem.leftBarButtonItem = self.editButtonItem()
1
2
3
4
override func setEditing(editing: Bool, animated: Bool)  {
  super.setEditing(editing, animated: animated)
  self.tableView.setEditing(editing, animated: animated)
}
1
2
3
func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
  return true
}
1
2
3
4
5
6
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
  if editingStyle == .Delete {
    self.searches.removeAtIndex(indexPath.row)
    tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
  }
}

Generic functions and protocols

1.这一节介绍泛型的函数和协议,之前一直使用的find方法就是一个泛型方法,这是一个全局方法,泛型参数C定义了domain参数,也间接定义了value参数的类型,且返回值也和C有关:

1
func find<C: Collection where C.GeneratorType.Element: Equatable> (domain: C, value: C.GeneratorType.Element) -> C.IndexType?

2.我们之前定义的有序字典的insert方法中这么使用了find(),没有指出C,其实这又是type interface的体现,通过第一个参数推断出了C的类型。

1
let existingIndex = find(self.array, key)!

3.那么GeneratorType是什么?上述的Collection协议,同时也遵从于SequenceType协议,如下,要实现该协议,必须有个typealias名为Generator,且遵从于GeneratorType协议,同时实现generate()方法返回Generator类型。

1
2
3
4
protocol SequenceType {
  typealias Generator : GeneratorType
  public func generate() -> Self.Generator
}

4.那么在自定义的有序数组上实验下SeqenceType,首先定义typealias名为Generator,使用AnyGenerator这个泛型类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
extension OrderedDictionary: SequenceType {
  typealias Generator = AnyGenerator<(KeyType, ValueType)>
  func generate() -> AnyGenerator<(KeyType, ValueType)> {
    var index = 0
    return anyGenerator {
      if index < self.array.count {
        let key = self.array[index++]
        return (key, self.dictionary[key]!)
      } else {
        return nil
      }
    }
  }
}

5.而在实现generate()方法中,通过调用了anyGenerator方法,该方法只有一个closure参数,所以使用了Trailing Closures技术,这个closure会在每次调用next()时调用,在closure中,你完成自己的遍历方法。

1
public func anyGenerator<Element>(body: () -> Element?) -> AnyGenerator<Element>

6.实现了SequenceType Protocol,你可以使用for-in来遍历字典,其实typealias Generator = AnyGenerator<(KeyType, ValueType)>这句可以删除,因为Swift从func generate() –> AnyGenerator<(KeyType, ValueType)>()返回值推断出了该类型。
7.实际上SequenceType Protocol就是一个泛型协议,只不过因为protocol不能使用<>关键字,而像Java和C#是可以的,原因也很简单,Protocols本身就是定义给class或struct实现的,这本身就是带有泛型的性质,Swift的思想就是protocol定义接口,而class和struct定义类型、泛型或其他。

6vvqnj09Z6