swift思维 第二部分:array的map方法

翻译自 今天这篇文章主要讲解map和flatmap在Arrays中的应用

以前的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

class ListItem {
var icon: UIImage?
var title: String = ""
var url: NSURL!

static func listItemsFromJSONData(jsonData: NSData?) -> [ListItem] {
guard let nonNilJsonData = jsonData,
let 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 {
// If we failed to unserialize the JSON or that JSON wasn't an NSArray,
// then bail early with an empty array
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()介绍

map()是Array中的一个方法,它包含一个函数来作为参数,这就解释了怎么样把数组的每个元素转换成新的。这个方法仅通过解释如何转换X->Y来允许转换一个数组[x]为[Y],而不需要创建一个临时的可变数组。(关于这一段,需要了解函数式编程的相关内容,简单来说,函数作为处理”值”而存在,我们输入一个值,会得到另一个值。而数据类型作为对值的封装,不仅包含值,还包含相关的属性和方法。这里把array这个数据类型而不是值通过map()函数进行处理,我们通过这个过程,拿到中间状态NSDictionary来自己”组装”函数,最后,函数将帮我们返回另一种数据类型ListItem。详情见这篇博文以及原文

因此,在我们的例子中,我们利用这个特性代替for循环,我们可以使用jsonItems(包含几组NSDictionary的JSON数组)的map方法–将每个字典替换成ListItem实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

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做的那样)。如果可能的话,我们要始终避免可变(mutable)状态。

数据损坏

另一个需要解决的问题是,目前我们所使用的代码仍然会创建一个ListItem(并将它包含在最后的数组中)即使我们输入了不正确的数据。因此,如果其中的一些NSDictionary是无效的,我们的数组最终还会包含一些空的ListItem()对象,实际上它们在真正的意义上是不存在的。

更重要的是,我们仍然 killing some ponies 🐴 因为我们还在使用NSURL!并且我们的代码还是允许我们创建一个没有NSURL的ListItem(item.url不会受到影响,如果我们没有一个有效的“URL”键),如果我们试图访问这些无效的NSURL! 我们的代码将崩溃。

为了解决这个问题,我们可以返回一个nil的ListItem,如果输入的参数是无效的。这比一个会引起错误的或者空的ListItem更合适。

1
2
3
4
5
6
7
8

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
}

但是,如果我们要使用新的NSDictionary - >ListItem?与jsonItems.map变换,它会产生一个[ListItem?],其中可能包含包含nil,我们仍然输入了无效的NSDictionary项。虽然这样做比以前好,但仍然不是非常实用的处理方法。

使用flatMap()

该flatMap()派上用场了。

flatMap()类似map(),但它采用的是T-> U?转换,而不是T-> U,如果该转换的返回值是nil,那么不加此项到输出数组。

语义上,flatMap和map很像,就像map消除nil值的扁平化结果。

运用这一原理,我们给出了下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

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
}

现在我们只返回一个真正的ListItem如果所有的键是我们所需要的和有效的(包括NSURL,我们保证它不是空的)。否则(使用guard声明),我们将返回nil,告诉flatMap不要添加无效的元素到返回的数组。

这是更好的而且更安全的,对吗?我们消除了数据损坏和错误数据带来的风险,以及无效的ListItem元素带来的风险。

结论

我们仍然有很多工作要做,但是今天,这将是所有(让我们保留一些东西到本系列的下一篇文章!)

因此,在这一篇里,我们学会了如何更换一个for循环到map或flatMap,我们要使我们的代码保证一点,避免不一致的输出信号的产生当我们的输入数据是无效的。这是一个相当不错的改进了。

在下篇文章中,我们将看到如何将ListItem转化为struct,并且探索map和flatMap的其他用途 - 尤其是Optionals。

在此期间,我们需要时间来探索map()和flatMap()的效率。我知道他们在第一次使用时是可怕的或复杂的,但一旦你掌握它,你将会想在任何地方使用它们!