在 本系列的第一篇文章 中,介绍了如何避免对可选类型强制解包以及拯救“小马” 。在第二部分中,我们将会精简前文代码来让它看起来更有”雨燕风”( Swift-er : Swift编程语言风格),同时向你介绍 map()
和 flatMap()
方法。
今天这篇文章我们将要讨论数组的 map()
和 flatMap()
方法。
友情提示: 以下为 前文 留下的代码:
class ListItem {
var icon: UIImage?
var title: String = ""
var url: NSURL!
static func listItemsFromJSONData(jsonData: NSData?) -> [ListItem] {
guard let nonNilJsonData = jsonData,
let json = try? NSJSONSerialization.JSONObjectWithData(nonNilJsonData, options: []),
let jsonItems = json as? Array<NSDictionary>
else {
// 倘若JSON序列化失败,或者转换类型失败
// 返回一个空数组结果
return []
}
var items = [ListItem]()
for itemDesc in jsonItems {
let item = ListItem()
if let icon = itemDesc["icon"] as? String {
item.icon = UIImage(named: icon)
}
if let title = itemDesc["title"] as? String {
item.title = title
}
if let urlString = itemDesc["url"] as? String, let url = NSURL(string: urlString) {
item.url = url
}
items.append(item)
}
return items
}
}
本文的目标是使用更多“雨燕风”的模式和语法,使得代码看起来更棒并且简洁。
map()
是 Array
提供的方法,通过接收一个函数作为传入参数,对数组中每个元素进行函数变换得到新的结果值。这样只需要提供 X->Y
的映射关系,就能将数组 [X]
变换到新数组 [Y]
,而无需创建一个临时可变数组(注:即上面代码中的 items
变量)。
本例中,我们不再像之前一样用 for
来做循环,而是对 jsonItems
( JSON 数据:存储于类型为字典的数组中)使用 map()
方法,并传入一个变换函数(闭包),将每个 NSDictionary
类型数组元素转换成我们所需的 ListItem
实例:
return jsonItems.map { (itemDesc: NSDictionary) -> ListItem in
let item = ListItem()
if let icon = itemDesc["icon"] as? String {
item.icon = UIImage(named: icon)
}
if let title = itemDesc["title"] as? String {
item.title = title
}
if let urlString = itemDesc["url"] as? String, let url = NSURL(string: urlString) {
item.url = url
}
return item
}
这看起来只是个很小的改动,但是它让我们专注于怎样把 NSDictionary
转化成 ListItem
,毕竟这是解决问题的核心。更为重要的是,避免了像在 ObjC 里做的那样,新建一个中间数组。我们应该尽可能地避免这种情况发生。
目前代码还存在一个问题:即便输入的数据是不可用的,我们依然创建了一个 ListItem
实例(并返回添加到结果数组 jsonItems
当中)。所以,倘若某些 NSDictionary
是无效的,最终的输出数组中,就会添加一些毫无意义的 ListItem()
空实例。
更重要的是,我们仍在杀死一些小马 。当我们使用 NSURL!
时,代码允许我们创建那些没有 NSURL
的 ListItem()
实例(我们没有一个有效的 url
键值,所以访问 item.url
不起作用),当我们访问无效的 NSURL!
时,程序将会崩溃。
为了解决这个问题,我们对变换函数稍加修改,当输入值无效时,返回一个值为 nil
的 ListItem
,这比返回一个错误或无内容的 ListItem
更为合适。
return jsonItems.map { (itemDesc: NSDictionary) -> ListItem? in
guard …/* condition for valid data */… else { return nil }
let realValidItem = ListItem()
… /* fill the ListItem with the values */
return realValidItem
}
但是如果 jsonItems.map
里面传入的函数参数类型为 NSDictionary -> ListItem?
,最后我们得到的是一个 [ListItem?]
数组,那些原来是不可用 NSDictionary
的位置就被我们替换成了 nil
。比原来要好一些了,但还不够。
这个时候就轮到 flatMap()
来救场了。
flatMap()
与 map()
相似,但 flatMap()
用的是 T->U?
变换而不是 T->U
转化,而且倘若变换后的数组元素值为 nil
2 ,则不会被添加到最后的结果数组里面。
从语法上,你可以这么理解, flatMap
就是先使用 map
处理数组,接着将结果数组“压平”(顾名思义),也就是从输出数组里剔除值为 nil
的元素。
通过 flatMap
方法改写后的实例代码如下:
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 li = ListItem()
if let icon = itemDesc["icon"] as? String {
li.icon = UIImage(named: icon)
}
li.title = title
li.url = url
return li
}
现在我们只返回所有键都存在 3 并有效的 ListItem
对象(保证 NSURL
不为 nil
)。否则执行 guard
语句,返回 nil
值通知 flatMap
不要将这些无效元素添加到返回结果数组中。
这样做就更好更安全了,对吧?,我们解决了数据异常的问题,当有错误输入时候,避免了无效的 ListItem
项添加到数组当中。
我们仍然有很多工作要做,但是今天就先做这些吧(让我们为本系列文章的下一篇准备一下材料!)
在这篇文章里面,我们学到了怎么用 map
或者 flatMap
来替换掉 for
循环,确保即便输入数据是不可用的的情况下,输出数组也不会出问题。这确实已经算是很大的进步了。
在下一篇文章里,将介绍如何用结构体( struct
)重写 ListItem
类有助于探索 map
和 flatMap
的其它用法 – 尤其是在处理 Optionals
的时候。
同时,希望你花点时间来深入了解一下 map()
和 flatMap()
在数组上的应用,我知道你第一次学的时候可能觉得它们很复杂,但是一旦你学会了,你什么时候都会想用它们。
1: 请脑补一些作秀节目的用词
2: flatMap
还有其他一些作用。比如把一个二维数组变换为一维数组,比如,[[T]] -> [T]。但是在这里,我们只需要关注在如何使用 T->U? 的变换把 [T] 变成 [U]。
3: 注意到我们的代码中,允许 NSDictionary
没有 icon
键,这意味着我们认为一个 ListItem
可以不需要有任何 icon
。但是其他键是必须的。