swift思维 第四部分:map all the things!

原文地址

Array vs. Optional

所以,作为一个提醒,我们在前面的文章中了解到,Array的函数map()和flatmap:

map( transform: T -> U ) -> Array
flatMap( transform: T -> Array ) -> Array

这意味着,给定一个变换:T-> U的用户可将T的数组转换为U的数组,只需使用(变换:T-> U)在你的Array中,它会返回一个数组Array

好了,这并不奇怪,map()和flatmap的返回值(Optional)是如此的相似:

map( transform: T -> U  ) -> Optional<U>
flatMap( transform: T -> Optional<U> ) -> Optional<U>

map() on Optionals

那么,map方式是如何返回Optional类型的(T?)?

嗯,这很简单 :与Array同样的方法,它内部是Optional的,使用transform:T->U来进行转换,并且把结果包装起来形成新的Optional

你想想看,这非常像Array.map所做的:它可以转换每个内在值(resp.Optional)并且返回一个新的Array(U)(resp.Optional)包含着转换过的新值。

回到我们的例子

那么,怎样才能应用这一切在代码中呢?

在上个版本的代码中,我们有一个itemDesc[“icon”]做了隐式转换(String?),并且我们想要把它最终转化成为UIImage;但是UIImage(named:)使用String作为参数,而不是String?(Optional)类型,所以我们需要使用一个方法的内部值String,并且仅仅当可选变量不为nil的情况。

一个解决办法是使用Optional绑定来做到这一点:

1
2
3
4
5
6
7
8

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

但是,对于这样一个简单的操作,我们写了太多行。

在前面的例子中,我们使用了(非常不优雅)替代解决方案,采用了nil,合并运算符??:

1
2
3
4

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

上面的代码能够工作,仅因为如果iconName是nil,我们将欺骗的UIImage的初始化和使用UIImage(name:” “),这确实会返回一个nil的图像。但是,这样并不能让人感觉干净整洁,我们有点滥用初始化,使其在这里工作。

让我们使用map

那么,为什么不使用map?的确,我们想要解包我们的Optional如果它不为空的话,转换内部值为UIImage并且返回UIIamge,那么究竟怎么使用呢?

我们试一试:

1
2
3
4

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

等等,上面的代码不能编译。你能猜出是为什么吗?

发生了什么?

问题出在UIImage(name:…)方法返回了一个可选类型:如果name没有值,那么它将无法创建UIImage,这样就使得这个初始化是失败的,在这种情况下将返回nil。

因此,这里的问题是,我们给闭包映射一个String,并返回……一个UIImage?类型 –初始化失败并且返回nil的类型。并且如果你再次检查map内部,他将要形成T(imageName) -> U(UIImage(name: ImageName))的闭包,并且返回U?。所以在这种情况下,U相当于UIImage?并且如果整个map表达式返回U?那么最终将返回的将是UIImage??,是的,两个Optional,oh boy!

flatMap()来救援

flatMap与map相似,但是它使用T->U?转换(而不是T->U),并且它是”flattens”的它的结果仅有一级的optional。这正是我们期望的!

1
2
3
4

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

因此,这里就是flatMap实际所做的:

1、如果iconName为nil,flatMap将直接返回nil(但是类型为UIImage?)
2、如果iconName不为nil,flatMap适用于转换内部值,因此试图使用String创建UIImage,并且返回结果–如果已经是UIImage?因此其可以是nil,如果UIImage初始化失败的话。

总之,item.icon只会有一个非空值,如果 itemDesc[“icon”]as? String 为非空并且UIImage(named:imageName)初始化成功。

这样感觉好多了,比欺骗初始化的??更加符合习惯。

使用 init闭包

说远一点,上面也可以使用一个更紧凑的编码方式,如Xcode中7现在通过类型的.init属性公开这些类的构造函数。

这意味着,UIImage.init实际上已经是一个函数,它接受一个字符串,返回一个UIImage?因此,我们可以直接使用它作为参数传递给我们的flatMap,没必要把它放在闭包里!

1
2
3
4

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

好吧,我觉得这是难以阅读和个人更喜欢使用一个明确的闭包在这里,为了使代码更清晰,更加明确。但是,这是个人喜好的问题而已,这是一件好事,你知道这是可能的!

我们最终的swift代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

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代码

花一点时间来与ObjC代码比较,我们的WIFT代码。我们从那里适应了相当多的东西!

如果你仔细观察ObjC与swift的代码,你就会意识到,swift的代码行数不会明显地缩短,但它的方式更安全。

特别是,关于swift,我们学会了,guard?,try?和as?它迫使我们必须检查一切不确定的类型,那些ObjC代码不会操心的和会导致崩溃的潜在问题。因此,也许它们的代码一样大小,但ObjC代码的更加危险!

结论

有了这个系列文章,我希望你明白,你不应该尝试把ObjC代码直接转化为swift。相反,尝试重新思考你的代码,重构它。通常更好的方式是从一个clean的状态开始重写你的代码时考虑到swift的特性,而不是试图直接翻译ObjC代码。

我不是说这很容易。改变你的思维方式,当你已经习惯了ObjC,并使用其模式和方法来编写代码还可能需要一些时间。但swift肯定更好。