Hi,SwiftGG 翻译组启用了新的域名:swiftgg.team今后翻译组的各项活动将会在新域名下开展,不要错过哦!

作者:Olivier Halligon,原文链接,原文日期:2015-10-11
译者:ray16897188;校对:Prayer;定稿:numbbbbb

系列文章地址:

在本系列之前的文章中我们学到了如何使用mapflatMap来操作数组(arrays)。今天我们继续研究如何对可选类型(Optionals)以及很多其他类型使用mapflatMap

数组 vs. 可选类型

回顾一下,学完前面的文章后我们已经知道,Array<T>对应的map()flatMap()函数签名是:

// 作用在Array<T>上的方法
map( transform: T -> U ) -> Array<U>
flatMap( transform: T -> Array<U> ) -> Array<U>

意思是你可以用一个给定的transform: T->U将一个元素类型是T的数组转换成一个元素类型是U的数组。对Array<T>调用map( transform: T->U )方法就会返回一个Array<U>,就这么简单。

嗯,不出意外,对于Optional<T>来说,map()flatMap()的函数签名十分类似:

// 作用在Optional<T>上的方法
map( transform: T -> U ) -> Optional<U>
flatMap( transform: T -> Optional<U> ) -> Optional<U>

是不是很像?

作用在可选类型上的 map()

那么map方法到底对Optional<T>类型(也叫做T?)做了什么?

其实很简单:和作用在Array<T>上的一样,map方法将Optional<T>中的内容取出来,用指定的transform: T->U方法做出转换,然后把结果包装成一个新的Optional<U>

如果细想一下,这和Array<T>.map做的事情十分相似:这个方法对Array<T>(与之相应的是Optional<T>)中的每个元素使用transform函数转换,并将转换过的值封装在一个新的Array<U>中(与之相应的是Optional<U>),作为结果返回。

回到我们的例子

那么这对我们一直在做的示例代码有什么帮助?

我们最新版代码中,有一个String?类型的itemDesc["icon"],我们当时想把它转换成一个UIImage;但是UIImage(named:)要求传入一个String型的参数,而不是String?型,所以我们需要在可选型中确实有值时(非nil)将内部的String值传入。

一种解决方案是使用可选绑定(Optional Binding):

let icon: UIImage?
if let iconName = itemDesc["icon"] as? String {
icon = UIImage(named: iconName)
} else {
icon = nil
}

但是对于一个如此简单的操作来说代码量太大。

之前的一个例子中我们用了另外一种(很不优雅的)方式,使用nil-联合操作符??

let iconName = itemDesc["icon"] as? String
let icon = UIImage(named: iconName ?? "")

这么做是可以,但是之所以能够成功,是因为当iconNamenil时,我们实际上是使用了UIImage(named: "")的初始化方法,这个初始化方法在传入空字符串时,会返回nil。但是这样的解决办法不是很好,因为我们是依赖于该初始化方法的特性(传入空字符串时,会返回nil)来实现的。

来用 map 吧

那么为什么不用map呢?本质上,我们是想要在Optional<String>不是nil的时候将其解包,把里面的值转换成一个UIImage对象然后把这个UIImage返回,这不就是一个绝佳的用例么?

试试看:

let iconName = itemDesc["icon"] as? String
item.icon = iconName.map { imageName in UIImage(named: imageName) }

等会儿…. 编译不通过。能猜出为什么吗?

哪儿有问题?

上面的代码中的问题是UIImage(named: …)也返回一个可选类型:如果对给定的name没有相应的图片,就不能创建出一个UIImage,所以这种情况下该初始化方法为可失败的(failable),并返回nil,是完全合理的。

于是问题就在于我们给map的这个闭包用一个String作为参数而返回…一个UIImage?类型——因为图片的初始化方法是可失败的,会返回nil。再看一下map方法的签名,它想要的是一个T->U类型的闭包,这个闭包会返回一个U?类型。我们的例子中,U代表UIImage?的话,整个map表达式会返回一个U?类型,也就是…一个UIImage??类型…是的,一个双重可选类型,吓死宝宝了!

flatMap() 来帮忙了

flatMap()map类似,但是做的是一个T->U?的转换(不是T->U),它把结果“扁平化(顾名思义)”成一个单重的可选类型。这恰恰就是我们所需要的!

let iconName = itemDesc["icon"] as? String
item.icon = iconName.flatMap { imageName in UIImage(named: imageName) }

实际中flatMap做了如下工作:

  • 如果iconNamenil的话,它就直接返回nil(但返回类型还是UIImage?)
  • 如果iconName不是nil,它就把transform作用到iconName的实际的值上,尝试用这个String创建一个UIImage并将结果返回——结果本身已经是一个UIImage?类型,因此如果UIImage初始化方法失败的话,返回结果就是nil

简而言之,item.icon只会在itemDesc["icon"] as? String非空、并且UIImage(named: imageName)初始化方法成功的情况下才是一个非空值。

和使用??欺骗初始化方法相比,这么做更好,更地道。

把 init 当闭包来用

更进一步,由于现在 Xcode 7 可以通过类型的.init属性暴露该类型的构造器(constructors),上面的代码还能写的更加紧凑。

这意味着UIImage.init本质上就已经是一个接收String并返回UIImage?的方法了,所以我们可以把它直接当成参数来调用flatMap,不用把它再包进一个闭包里!

let iconName = itemDesc["icon"] as? String
item.icon = iconName.flatMap(UIImage.init)

哇哦!太魔幻了!

好了,有人说这么写很难读懂,为了让代码更明了更清晰,在这里还是更喜欢用一个显式闭包。但是这只是关乎个人偏好,并且知道这么做可行也是好事。

最终的Swift代码

下面就是将本课所学应用到之前代码里的样子:

struct ListItem {
var icon: UIImage?
var title: String
var url: NSURL

static func listItemsFromJSONData(jsonData: NSData?) -> [ListItem] {
guard let jsonData = jsonData,
let json = try? NSJSONSerialization.JSONObjectWithData(jsonData, options: []),
let jsonItems = json as? Array<NSDictionary> else { return [] }

return jsonItems.flatMap { (itemDesc: NSDictionary) -> ListItem? in
guard let title = itemDesc["title"] as? String,
let urlString = itemDesc["url"] as? String,
let url = NSURL(string: urlString)
else { return nil }
let iconName = itemDesc["icon"] as? String
let icon = iconName.flatMap { UIImage(named: $0) }
return ListItem(icon: icon, title: title, url: url)
}
}
}

回头看一眼我们的 ObjC 代码

花一点儿时间比较一下我们最终的 Swift 代码和最开始的ObjC代码。我们着实改了很大一部分内容。

如果你仔细看一下 ObjC 和 Swift 代码,会发现 Swift 的代码量并不是那么少(ObjC 是 5+15 LoC1,对比 Swift 的 19 LoC),但是安全性高了太多

尤其是我们使用的guardtry?as?会迫使我们去检查所有类型是否都如所期,ObjC 代码不会关心这些,因此可能崩溃💣💥。所以虽然代码量相当,但 ObjC 代码更危险。

结论

通过本系列文章,我希望你能够意识到:不要尝试将你的 ObjC 代码直译成 Swift。相反,你要去重新考虑一下你的代码,重新想象一下你的代码。从一个空白状态开始,脑中秉持着 Swift 的理念重写你的代码总会比你把 ObjC 代码直译过来要好。

我没说过这是件容易的事儿。当你已经习惯了用 ObjC 写代码,熟悉了它的模式和写代码的方式的话,做出思维上的改变会需要一些时间。但是这绝对有更多的好处。


以上就是 Swift 编程思想系列的最后一部分内容2。现在你要开始为新 Swift 项目发狂了,把 Swift 编程思想全部贯彻到你的脑海中。

祝用 Swift 编程愉快,并且…
MAP 一切,FLATMAP 一切!


  1. Lines of Codes,指有多少行代码
  2. 我会马上发布一个收尾文章,留一个关于Monads的口风,并把这个系列真正完结。别愁,之后还有很多关于 Swift 的文章会陆续发布。

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

文章目录
  1. 1. 数组 vs. 可选类型
  2. 2. 作用在可选类型上的 map()
  3. 3. 回到我们的例子
  4. 4. 来用 map 吧
  5. 5. 哪儿有问题?
  6. 6. flatMap() 来帮忙了
  7. 7. 把 init 当闭包来用
  8. 8. 最终的Swift代码
  9. 9. 回头看一眼我们的 ObjC 代码
  10. 10. 结论