《Advanced Swift》笔记1:数组变形(一) —— map 和 flatMap

mapflatMap是Swift数组中的两个高阶函数,他们能很方便的对数组内的所有元素进行操作,然后返回一个新的数组。本文将探索这两个函数的实际使用以及其内部实现。

map

在objective-c中,如果我们要对一个数组里的每个元素进行操作,我们都是通过写for循环来遍历数组,然后在再对元素进行操作。比如我们要对一个数组中的所以元素做平方,并返回新的数组,我们可以这样写:

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

NSArray *array = @[@(1), @(3), @(5), @(6)];
NSMutableArray *sauared = [NSMutableArray array];
for (NSNumber *number in array) {
NSInteger num = number.integerValue;
[sauared addObject:@(num * num)];
}

NSLog(@"%@", sauared);

// 最后的输出为:
2016-07-31 15:20:52.497 Array[9945:5153603] (
1,
9,
25,
36
)

在Swift中,为数组提供了一个map方法来做这件事,因此上面的代码在Swift中可以这样写:

1
2
3
let array = [1, 2, 3]
let sauared = x.map{$0 * $0}
// sauared = [1, 4, 9]

使用map方法的优势很明显:

  • 代码会变得非常简洁,一句代码就能搞定,代码量变少了,也意味着出错的概率变小了。
  • 语义更加明确,也就是代码可读性更好。通过map这个方法名,我们一眼就知道这个方法要做什么,而如果是通过for循环,我们必须看完整个上下文的代码的才知道在做什么。
  • 在Swift中使用map还有一个优点就是能使用let把新的数组定义为不可变量。如果是用for循环,因为要在循环中逐个添加新的数组元素,因此新的数组必须要用var来定义。这样也提高了代码的安全性。

map 的内部实现

map函数的内部实现原理也非常简单,就是把for循环中的代码用一个泛型函数封装起来了。源码地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public func map<T>(
_ transform: @noescape (Iterator.Element) throws -> T
) rethrows -> [T] {
// TODO: swift-3-indexing-model - review the following
let count: Int = numericCast(self.count)
if count == 0 {
return []
}

var result = ContiguousArray<T>()
result.reserveCapacity(count)

var i = self.startIndex

for _ in 0..<count {
result.append(try transform(self[i]))
formIndex(after: &i)
}

_expectEnd(i, self)
return Array(result)
}

我们也可以这样简单地实现:

1
2
3
4
5
6
7
8
9
10
extension Array {
func my_map<U>(transform: Element -> U) -> [U] {
var result: [U] = []
result.reserveCapacity(self.count)
for x in self {
result.append(transform(x))
}
return result
}
}

flatMap

flatMapmap很像,都是对数组中每个元素进行操作转换,但flatMap会做两件额外的事,一是将结果数组展平,如果结果数组中的元素也是一个数组,就会将这个数组展平;二是会把数组中的nil过滤,返回一个不包含nil的数组。这两个额外的操作,其实是分别对应flatMap的两个方法, 因此在一个flatMap方法中这两个额外操作是不能同时执行的。

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
extension SequenceType {
/// Returns an `Array` containing the concatenated results of mapping
/// `transform` over `self`.
///
/// s.flatMap(transform)
///
/// is equivalent to
///
/// Array(s.map(transform).flatten())
///
/// - Complexity: O(*M* + *N*), where *M* is the length of `self`
/// and *N* is the length of the result.
@warn_unused_result
public func flatMap<S : SequenceType>(transform: (Self.Generator.Element) throws -> S) rethrows -> [S.Generator.Element]
}

extension SequenceType {
/// Returns an `Array` containing the non-nil results of mapping
/// `transform` over `self`.
///
/// - Complexity: O(*M* + *N*), where *M* is the length of `self`
/// and *N* is the length of the result.
@warn_unused_result
public func flatMap<T>(@noescape transform: (Self.Generator.Element) throws -> T?) rethrows -> [T]
}

从文档中我们可以看到第一个flatMap的调用者中的元素其实是一个数组(SequenceType),返回的是一个完全展开的一次数组,通过实际代码,我们可以看出其中的区别:

1
2
3
4
5
let array = [[1, 2, 3, 4], [5, 6, 7]]
let mapArray = array.map{$0.map{$0 * $0}}
// mapArray = [[1,4,9,16],[25,36,49]]
let flatMapArray = array.flatMap{$0.map{$0 * $0}}
// flatMapArray = [1,4,9,16,25,36,49]

文档的解释是第一个flatMap函数相当于Array(s.map(transform).flatten())

第二个flatMap方法则会返回一个不带nil的数组,示例如下:

1
2
3
4
5
let nilArray: [Any?] = [1, 2, nil, 3]
let mapArray = nilArray.map{$0}
// mapArray = [1,2,nil,3]
let flatMapArray = nilArray.flatMap{$0}
// flatMapArray = [1,2,3]

小思考:下面的代码各会返回什么结果

1
2
3
4
5
6
7
8
9
10
> let sArray: [[Int?]] = [[1, 2, 3, nil, 4]]
> sArray.flatMap{$0}
>
> let array = [[1, 2, 3], 4, [5, 6]]
> array.flatMap{$0}
>
> let array: [Any?] = [[1, 2, 3], 4, [5, 6], nil]
> array.flatMap{$0}
>
>

flatMap 的内部实现

第一个flatMap源码地址

1
2
3
4
5
6
7
8
9
10

public func flatMap<SegmentOfResult : Sequence>(
_ transform: @noescape (${GElement}) throws -> SegmentOfResult
) rethrows -> [SegmentOfResult.${GElement}] {
var result: [SegmentOfResult.${GElement}] = []
for element in self {
result.append(contentsOf: try transform(element))
}
return result
}

从源码中我们可以发现,flatMap方法之所以能将数组展开,关键是在添加结果是使用的是appendContentsOf方法,这个方法能把数组中的元素取出,放入一个新数组中。

第二个flatMap源码地址

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

public func flatMap<ElementOfResult>(
_ transform: @noescape (${GElement}) throws -> ElementOfResult?
) rethrows -> [ElementOfResult] {
var result: [ElementOfResult] = []
for element in self {
if let newElement = try transform(element) {
result.append(newElement)
}
}
return result
}

第二个flatMap方法内部则使用了一个if let判断来过滤nil元素。

坚持原创技术分享,您的支持将鼓励我继续创作!