书籍链接:《Pro Swift》 (链接需要梯子才能打得开)。
函数式编程是一个很大的话题,在此不详细描述。大家可以自行了解。在这一章节我们主要看看函数式编程在Swift中的应用。
为了更好的理解函数式编程的好处,我们先看看这个五个原则:
- 函数在Swift中属于一等数据类型,就像整型和字符串一样,函数也可以被创建、复制和传递。
- 函数可以作为参数传入另外一个函数。
- 为了让函数可以重用,传入一样的参数,应该返回一样的结果,并且不会产生任何副作用。
- 优先使用不可变的数据类型,尽量不要使用函数来改变可变的变量。
- 因为函数不产生副作用,并且变量是不可变的,所以在函数里面尽可能减少程序中跟踪的状态。
一、函数式编程在Swift中的应用
1. map
定义:把容器中的元素取出,然后放到closure中运行,最后把返回的结果放到一个新的容器中。Swift中的数组、字典和集合都有这个方法。
多看几个例子:
// 转换成大写
let fruits = ["Apple", "Cherry", "Orange", "Pineapple"]
let upperFruits = fruits.map { $0.uppercased() }
// 转换成对分数的描述
let scores = [100, 80, 85]
let formatted = scores.map { "Your score was \($0)" }
// 转换成对分数的是否通过
let scores = [100, 80, 85]
let passOrFail = scores.map { $0 > 85 ? "Pass" : "Fail" }
// 转换成是否在范围内
let position = [50, 60, 40]
let averageResults = position.map { 45...55 ~= $0 ? "Within average" : "Outside average" }
// 求平方根
let numbers: [Double] = [4, 9, 25, 36, 49]
let result = numbers.map(sqrt)
其实这些方法我们都可以使用for
循环完成,但是使用map()
会更简洁些。要注意的是: map()
会遍历每个元素,而在for
循环中,我们可以控制是否break
。
2. Optional map
我们刚刚说了对map()
的定义是把容器中的元素取出,然后放到closure中运行,最后把返回的结果放到一个新的容器中。因为Optional也是属于容器,只有一个元素的容器,所以我们也可以在Optional类型中使用map()
:
let i: Int? = 10
let j = i.map { $0 * 2 }
print(j) // Optional(20)
如果i
是nil
,那么map()
返回nil
。
我们再看一个例子:
func fetchUsername(id: Int) -> String? {
if id == 1989 {
return "Taylor Swift"
} else{
return nil
}
}
我们来使用上面的方法:
// 如果不使用map
let username = fetchUsername(id: 1989)
let formattedUsername = username != nil ? "Welcome, \(username!)!" : "Unknown user"
print(formattedUsername)
// 使用map
var username: String? = fetchUsername(id: 1989)
let formattedUsername = username.map { "Welcome, \($0)!" } ?? "Unknown user"
print(formattedUsername)
可以看到,不使用map()
的话,要先判断username
是否为nil
,如果不为nil
,还要进行强制unwrap;而使用map
则简单很多。
3. forEach
map()
和forEach()
很类似,都是会遍历容器中的每一个元素;不同之处在于,1)map()
会返回新的容器,而forEach()
返回空,2)forEach()
在执行的时候,是按照数组元素的顺序执行的;而为了提高性能,map()
是没有顺序的。
例子:
["Taylor", "Paul", "Adele"].forEach { print($0) }
forEach()
的内部实现实际是使用了for
循环:
public func forEach(_ body: (Iterator.Element) throws -> Void) rethrows {
for element in self {
try body(element)
}
}
4. flatMap
先来看一个map()
的例子:
let albums: [String?] = ["Fearless", nil, "Speak Now", nil, "Red"]
let result = albums.map { $0 }
print(result)
// [Optional("Fearless"), nil, Optional("Speak Now"), nil, Optional("Red")]
因为albums
的元素类型是String?
,在map()
直接返回$0
,所以得到的result
全是Optional
类型。
但是如果我们使用flatMap()
的话:
let albums: [String?] = ["Fearless", nil, "Speak Now", nil, "Red"]
let result = albums.flatMap { $0 }
print(result)
// ["Fearless", "Speak Now", "Red"]
result
变成了[String]
类型,所有Optional
和nil
都去掉了。
我们再看另外一个例子:
let labels = view.subviews.flatMap { $0 as? UILabel }
上面这行代码的作用:把subviews
中属于UILabel
类型的找出来,并转成UILabel
。
所以说我们可以对flatMap()
这么理解:,先用map()
把一个容器变成另外一个容器,然后再把nil
去掉。
4. filter()
filter()
是比较容易理解的,就是把符合我们传入的closure条件的元素找出来,然后组成一个新的容器。
例如下面的例子,把偶数找出来:
let fibonacciNumbers = [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
let evenFibonacci = fibonacciNumbers.filter { $0 % 2 == 0 }
4. reduce()
reduce()
把容器的所有元素,根据我们传入的closure,最终合并成一个值。这个函数需要两个变量,第一个是初始值,第二个是closure。
例如下面的例子,求数组元素的和:
let numbers = [1, 2, 3, 4]
let numberSum = numbers.reduce(0, { sum, number in
return sum + number
})
// numberSum == 10
当numbers.reduce
在运行的时候,最开始的求和结果是我们传入的0
,然后用第一个元素1
放到closure运行,这时sum
是0
,number
就是第一个元素1
,所以结果就是0 + 1
;然后用第二个元素2
放到closure运行,这时sum
就是上次的运算结果1
,number
就是第二个元素2
,结果就是1 + 2
;以此类推,最后结果是10
。
我们还可以简写成,因为+
本身是一个实现了加法的方法,而方法可以看作是closure作为参数:
let numbers = [1, 2, 3, 4]
let numberSum = numbers.reduce(0, +)
1) 使用reduce()
返回一个Bool
类型的值
有时候我们可能会遇到这种情况:判断数组内的元素是否都满足某个条件。这时我们用reduce()
就可以轻松实现。
例如,判断一组人名的长度是否都大于4:
let names = ["Taylor", "Paul", "Adele"]
let longEnough = names.reduce(true) { $0 && $1.count > 4 }
print(longEnough)
最终返回false
。
不过要注意的是,如果数组中有1000个人名,在第二个元素就已经不满足条件,就没必要再判断下去了。如果使用reduce()
的话,就不管是否已经有不满足条件的元素,都会遍历完所有元素,会做一些无用功。所以在实际开发中,我觉得还是优先使用这个方法,如果确实会影响性能,在考虑其他办法。
2) 使用reduce()
查找一个值
例如,查找一组人名中最长的名字:
let names = ["Taylor", "Paul", "Adele"]
let longest = names.reduce("") { $1.count > $0.count ? $1 : $0 }
但是我们也可以使用max()
:
let longest = names.max { $1.count > $0.count }
这个例子中,reduce()
会多做一次比较,因为我们给了一个初始值;而max()
返回一个Optional类型。实际开发中我们可以根据情况,选择一个比较好的方案。
5. sort()
如果数组中存储的是基本数据类型,例如字符串和数字类型,我们可以直接用没有参数的sort()
(默认是从小到大排序的):
let scoresString = ["100", "95", "85", "90", "100"]
let sortedString = scoresString.sorted()
print(sortedString)
// ["100", "100", "85", "90", "95"]
因为scoresString
是字符串数组,所以Swift根据字符串的每一个字符从小到大排列,就造成了这个结果:["100", "100", "85", "90", "95"]
。但事实上这不是我们想要的结果,因为从常识来说,100
肯定是最大的。所以我们要先把他转为数字类型的数组:
let scoresInt = scoresString.flatMap { Int($0) }
let sortedInt = scoresInt.sorted()
print(sortedInt)
我们使用了flatMap()
把[String]
转为[Int]
,如果是用map()
的话,就会转成[Int?]
,不方便我们后续使用,因为[Int?]
是没有sort()
方法的,[Int]
才有。
如果我们使用map()
的话,需要使用有closure参数的sort()
,代码会多很多:
let scoresInt = scoresString.map { Int($0) }
let sortedInt = scoresInt.sorted {
if let unwrappedFirst = $0, let unwrappedSecond = $1 {
return unwrappedFirst < unwrappedSecond
} else {
return false
}
}
1) 对复杂的数据排序
如果我们的数组元素类型是自定义的类型,例如struct
类型,并且有不同的属性,这种情况下,我们要告诉Swift如何进行排序。有以下两种方式:
- 使用有参数的
sort()
- 实现
Comparable
协议后,使用没有参数的sort()
我们举个例子,有一个Person
结构:
struct Person {
var name: String
var age: Int
}
let taylor = Person(name: "Taylor", age: 26)
let paul = Person(name: "Paul", age: 36)
let justin = Person(name: "Justin", age: 22)
let adele = Person(name: "Adele", age: 27)
let people = [taylor, paul, justin, adele]
使用第一种方法排序:
let sortedPeople = people.sorted { $0.name < $1.name }
但是我们更推荐使用第二种方法:实现Comparable
协议,因为这可以让排序的逻辑更集中。如果我们要在多个地方进行排序,使用sort()
即可,不需要多次调用有参数的sort()
:
struct Person: Comparable {
var name: String
var age: Int
static func <(lhs: Person, rhs: Person) -> Bool {
return lhs.name < rhs.name
}
static func ==(lhs: Person, rhs: Person) -> Bool {
return lhs.name == rhs.name && lhs.age == rhs.age
}
}
我们就可以直接调用无参数的sort()
进行排序:
let sortedPeople = people.sort()
另外Comparable
是继承于Equatable
的,所以我们也同时解决了比较的问题。
2) 反向排序
如果我们使用有参数的sort()
,直接把<
改为>
即可:
let sortedString = scoresString.sorted {
if let first = Int($0), let second = Int($1) {
return first > second
} else {
return false
}
}
print(sortedString)
Swift还有一个自带的reversed()
方法:
let names = ["Taylor", "Paul", "Adele", "Justin"]
let sorted = names.sorted().reversed()
print(sorted)
上面的sorted
等于ReverseRandomAccessCollection<Array<String>>(_base: ["Adele", "Justin", "Paul", "Taylor"])
,他其实是一个lazy
数组,存储了原来的数组和reverse函数。因为数组是一个值类型,所以Swift不必真实的把原来的数组反向排序。如果我们想要一个真实的数组,需要通过Array的初始化函数来创建:
let sortedArray = Array(names.sorted().reversed())
print(sortedArray)
6. 函数连续调用
也就是说多个函数连在一起调用。例如下面的例子:
let london = (name: "London", continent: "Europe", population: 8_539_000)
let paris = (name: "Paris", continent: "Europe", population: 2_244_000)
let lisbon = (name: "Lisbon", continent: "Europe", population: 530_000)
let rome = (name: "Rome", continent: "Europe", population: 2_627_000)
let tokyo = (name: "Tokyo", continent: "Asia", population: 13_350_000)
let cities = [london, paris, lisbon, rome, tokyo]
let biggestCities = cities
.filter { $0.population > 2_000_000 }
.sorted { $0.population > $1.population }
.prefix(upTo: 3)
.map { "\($0.name) is a big city, with a population of \($0.population)" }
.joined(separator: "\n")
print(biggestCities)
// Tokyo is a big city, with a population of 13350000
// London is a big city, with a population of 8539000
// Rome is a big city, with a population of 2627000
先用filter()
找出人口大于2_000_000
(_
是为了让读者更好的读这个数);再用sorted()
从大到小排序,再用prefix()
取出前三个,再用map()
转换成对这个城市的描述,最后用joined()
把他们结合在一起,成为一个字符串。
上面这种多个方法连在一起的写法,在RxSwift中无处不在。
7. 复合函数
有时候我们开发过程中,可能会遇到这样的情况:
let foo = functionC(functionB(functionA()))
为了让代码更好看些,可能会改成这个样子:
let a = functionA()
let b = functionB(a)
let foo = functionC(b)
现在我们用另外一种更好的方式来解决这个问题,自定义一个运算符>>>
,最终把代码改写成:
let foo = functionA >>> functionB >>> functionC
关于运算符的问题,在【《Pro Swift》阅读笔记】05 - 方法有讲到,大家可以先去看看这个。
下面看看如何自定义运算符>>>
:
precedencegroup CompositionPrecedence {
associativity: left
}
infix operator >>>: CompositionPrecedence
func >>> <T, U, V>(lhs: @escaping (T) -> U, rhs: @escaping (U) -> V) -> (T) -> V {
return { rhs(lhs($0)) }
}
我们用到了三个泛型:T
、U
和V
,需要两个方法类型的操作数lhs
和rhs
,其中lhs
接受T
类型的参数并返回U
类型,rhs
接受U
类型的参数并返回V
类型。整个运算符返回一个方法类型:接受T
类型的参数并返回V
类型,所以方法的实现是return { rhs(lhs($0)) }
。另外,因为两个closure不是马上调用,所以都要用@escaping
。
下面我来看一个实际的例子,先生成一个随机的Int
,再求这个随机数的所有因子并组成数组,最后把所以因子组成一个String
:
func generateRandomNumber(max: Int) -> Int {
let number = Int(arc4random_uniform(UInt32(max)))
print("Using number: \(number)")
return number
}
func calculateFactors(number: Int) -> [Int] {
return (1...number).filter { number % $0 == 0 }
}
func reduceToString(numbers: [Int]) -> String {
return numbers.reduce("Factors: ") { $0 + String($1) + " " }
}
如果我们用之前的方法,会这么做:
let result = reduceToString(numbers: calculateFactors(number: generateRandomNumber(max: 100)))
print(result)
如果是使用>>>
运算符:
let combined = generateRandomNumber >>> calculateFactors >>> reduceToString
print(combined(100))
明显使用>>>
运算符会更清晰易读:生成随机数;计算因子;生成字符串。而且我们合并好的方法combined
可以重复使用。另外我们还可以用>>>
拼接更多的方法。
8. lazy方法
在有些方法调用的时候,我们也可以使用lazy
:
因为我们使用了lazy
,当我们调用前两个print
的时候,Swift才会去执行filter()
,filter()
的closure调用了2
万次,每个print
调用1
万。因为lazyFilter
只有在numbers
的每个元素都处理完成之后才能确定下来,而后面两个print
,当我们访问lazyMap[5000]
时,才会去运行map()
,并且只运行一次,其他没有访问的会忽略。
如果不使用lazy
的情况如下:
如果不使用lazy
,lazyFilter
和lazyMap
在创建的时候就会去执行filter()
和map()
,所有他们的closure都是运行1
万次。后面再print
的时候就不会调用filter()
和map()
了,因为lazyFilter
和lazyMap
在前面已经创建好了。
是否要使用lazy
,我们要根据实际情况来看。
二、函子和单子
在这里简单介绍下函子和单子的概念。
函子:实现了map()
方法的容器。也不一定要把这个方法称为map()
,只要这个方法实现了把A容器转换成B容器。我们经常用的数组和Optional都是函子。
单子:实现了flatMap()
的函子,也就是同时实现了map()
和flatMap()
的容器。数组和Optional也都是单子。
完
有任何问题,欢迎大家留言!
欢迎加入我管理的Swift开发群:536353151
,本群只讨论Swift相关内容。
原创文章,转载请注明出处。谢谢!