如何:在Swift中映射,缩小和过滤

由Reinder de Vries撰写于2017年5月3日在App Development中

Swift的编程功能Map,Reduce和Filter可能难以挑战。 特别是,如果您总是编写for循环来解决迭代问题!

Map,Reduce和Filter函数来自函数编程领域。 在Swift中,您可以使用Map,Reduce和Filter遍历诸如ArrayDictionary集合类型,而无需使用for循环。

在构建应用程序时,通常使用过程式面向对象的编程方式。 函数式编程是不同的:它仅处理函数。 没有变量,没有“状态”,没有for循环-只是函数。

Swift编程语言非常适合于函数式编程。 但是,您无需严格地编写功能代码,只需采用功能编程中的概念(例如Map,Reduce和Filter)即可帮助您学习如何更好地编写代码。

Map,Reduce和Filter通常被称为高阶函数 ,因为这些函数将一个函数(一个闭包)作为输入,而将返回函数作为输出。 严格来说,Swift将在使用高阶函数时返回操作(即映射数组)的结果,而“纯”函数语言将返回函数的集合。

如果您在想:“伙计,我不需要功能编程或数据处理,因为我的应用程序不需要这样做!” —不要在这里停下来。

在最近的应用程序项目中,我多次使用过Map,Reduce和Filter:

  • 过滤成本/收入值,使其达到一定阈值,然后再将其包含在折线图中
  • 将成千上万的电影评分降低为一个平均评分
  • 在此Photo App模板中的标签上映射一些操作(小写,删除“#”)

您当然可以通过for循环解决所有这些问题,但是您会发现使用map,reduce和filter函数可以使代码更简洁,可读性和性能更高。

在本指南中,您将学习如何使用地图,缩小和过滤功能。 您将对集合类型 (如Array执行这些功能。

以下是简要概述:

  • map函数遍历集合中的每个项目,并对集合中的每个元素执行操作
  • reduce函数循环遍历集合中的每个项目,并将它们组合为一个值
  • filter函数遍历集合中的每个项目,并返回仅包含满足包含条件的项目的集合

换句话说:

  • map功能将功能应用于集合中的每个项目。 考虑将一组值“映射”到另一组值。
  • reduce函数将一个集合变成一个值。 可以认为它实际上是将多个值减少为一个值。
  • filter函数仅返回通过if -statement条件且仅当该条件导致true的值的数组。

有趣的事实: MapReduce是基本的大数据处理概念,其中对集合并行执行密集操作。 一个示例是将一本书的页面概括为一个单词(映射),然后将这些单词存储在字母框中(减少)。

跳至相关章节:

  • 使用地图功能
  • 使用减少功能
  • 使用过滤功能
  • 链接图,缩小和过滤
  • 结论

准备? 我们走吧!

成为专业的iOS开发人员

开始使用iOS 12和Swift 4

注册我们的iOS开发课程“ 零到App Store”,以学习Swift 4的iOS开发,并开始您的专业iOS生涯。

了解更多 免费开始

map 函数遍历集合中的每个项目,并对集合中的每个元素进行操作。 它返回操作所应用到的结果项的数组。

让我们看一个例子。 假设您要转换为华氏温度,并在摄氏有一系列温度。

您可以使用for循环:

 设celcius = [-5.0、10.0、21.0、33.0、50.0] 
var fahrenheit:[Double] = []表示摄氏{
华氏度+ = [值*(9/5)+ 32]
}打印(华氏度)
//输出:[23.0、50.0、69.8、91.4、122.0]

尽管代码可以正常工作,但它不是最有效的。 您需要一个可变的“帮助程序”变量fahrenheit来存储计算的转换,同时还要进行转换,而转换本身需要3行代码。

使用map函数签出代码示例:

 设celcius = [-5.0、10.0、21.0、33.0、50.0] 
let华氏= celcius.map {$ 0 *(9/5)+ 32}
打印(华氏度)
//输出:[23.0、50.0、69.8、91.4、122.0]

这里会发生什么?

  • 首先,定义一个常量celcius ,其类型为Array of Double ,并使用一些随机的Celcius值进行初始化。
  • 其次,在常数celcius上调用函数map 。 该函数有一个参数,即Closure ,它从Celcius转换为Fahrenheit。
  • 最后,结果被打印出来。

让我们仔细看一下闭包 。 如果您以前使用过闭包,那么您会认识到简写的闭包语法 。 这是编码闭包的一种较短方法,并且省去了闭包的大部分语法。

基本上,您只能使用输入,如$0和方括号{ }$0是闭包的第一个输入参数, {}表示闭包的开始和结束。 如果闭包具有多个参数,则第二个参数为$1 ,第三个参数$1 $2 ,以此类推。

这是“扩展”代码示例的样子:

  let celcius = [-5.0,10.0,21.0,33.0,50.0] let fahrenheit = celcius.map({(value:Double)-> Double in 
返回值*(9/5)+ 32
})print(华氏度)

实际的map函数调用及其关闭是这样的:

  ... = celcius.map({(value:Double)-> Double in 
返回值*(9/5)+ 32
})

如您所见, map函数在数组celcius上调用。 这个map函数接受一个参数:闭包。

闭包的第一部分以{开头,表示此闭包具有一个Double类型的参数value ,并且应返回Double类型的值。 以return开头的闭包主体只是将Celcius的结果返回华氏转换。

如果将简写闭合语法与上面的扩展代码进行比较,则会看到:

  • 省略函数括号() ,因为您可以在函数调用的最后一个参数为闭包时将其省略。
  • 可以省略() -> in部分,因为Swift可以推断您使用一个Double参数作为输入,并且期望返回Double 。 现在您已经省略了value变量,可以使用缩写$0
  • 还可以省略return语句,因为无论如何该闭合都将返回表达式的结果。

即使上面的代码示例使用Double类型,您也不限于这些类型。 map函数的结果类型可以与您输入的map函数类型不同,并且还可以在Dictionary上使用map

这是来自Photo App模板的更复杂的示例:

  ... 
regex.matches(in:self,options:[],range:NSRange(location:0,length:self.characters.count))。map {
string.substring(with:$ 0.range).replacingOccurrences(of:“#”,with:“”).lowercased()
}
...

在此示例中,将映射正则表达式的结果,删除#字符,然后将字符串转换为小写。

这就是您的map功能!

reduce 函数循环遍历集合中的每个项目,并将它们组合为一个值。 可以认为它实际上是将多个值减少为一个值。

Reduce功能可能是最难理解的Map,Reduce,Filter。 您如何从一组价值转变为一个价值?

一些例子:

  • 创建多个值的总和,即3 + 4 + 5 = 12
  • 连接字符串集合,即["Zaphod", "Trillian", "Ford"] = "Zaphod, Trillian, Ford"
  • 平均一组值,即(7 + 3 + 10) / 3 = 7/3 + 3/3 + 10/3 = 6.667

在数据处理中,您可以想象出很多情况,例如像这样的简单操作会派上用场。 像以前一样,您可以使用for循环解决任何这些问题,但是reduce会更短,更快。

这是您使用reduce

 让值= [3,4,5] 
让sum = values.reduce(0,+)
打印(总和)
//输出:12

函数reduce带有两个参数,一个初始值和一个闭包。 如您在上面的示例中看到的,您可以使用诸如+-*的简单表达式替换此闭包。

同样,您也可以提供自己的闭包,以及简化的闭包语法,如下所示:

 让值= [7.0、3.0、10.0] 
让avg:Double = values.reduce(0.0){$ 0 +($ 1 / Double(values.count))}
打印(平均)
//输出:6.667

在上面的示例中,您正在计算三个值7、3、10的平均值。 因为所有类型都需要匹配,所以values数组, avg常量和values.count的类型均为Double

重要的是在此处注意,用于reduce函数的闭包有两个参数。 第一个是先前减少的结果,即函数的输出,用$0表示。 第二个是要减少的当前项,即当前迭代的输入,用$1表示。

如果您“扩展”前面的示例,闭包的输入将变得更加清晰,例如:

 让值= [7.0、3.0、10.0] 
让avg = values.reduce(0.0,{(结果:Double,item:Double)-> Double in
返回结果+(item / Double(values.count))
})
打印(平均)
//输出:6.667

在上面的示例中,您可以看到两个闭包参数resultitem ,均为Double类型。 这也清楚了为什么reduce函数需要一个初始值-这是循环的第一次迭代的result

减少可能很难把握。 您正在对两个操作数(例如A + B执行一个运算,但是怎么可能只有一个结果呢? 在集合中的所有项目中,哪些项目用作操作数?

看一下这个:

  3 + 4 + 5 + 6 
((3 + 4)+ 5)+ 6
3 + 4 = 7
7 + 5 = 12
12 + 6 = 18

在上面的示例中,您使用+运算符添加数字列表。 在第二行上,您可以看到执行顺序,即首先执行3 + 4 。 在接下来的几行中,您可以看到3 + 4的结果被加到第三项5 ,结果为12 ,用作12 + 6的左侧操作数,结果为18

在Swift代码中,是这样的:

 让值= [3、4、5、6] 
让sum = values.reduce(0,+)
打印(总和)
//输出:18

在上面的示例中,第一次迭代需要初始值为00 + 3 = 3

filter 函数循环遍历集合中的每个项目,并返回仅包含满足包含条件的项目的集合。 这就像将if-语句应用于集合,而只获取那些将条件传递回来的语句。

看看这个例子:

  let值= [11、13、14、17、21、33、22] 
让偶数= values.filter {$ 0%2 == 0}
打印(偶数)
//输出:[14,22]

在上面的示例中,您要从偶数中选择数字。

表达式$0 % 2 == 0使用余数运算符%来确定$0是否可被2整除。 如果操作的余数为0 ,则数字必须为偶数。

  5%2 == 0 
//输出:false16%2 == 0
//输出:true

5/2将1作为余数,而16/2将0作为余数,因此16必须是偶数。

您还可以看到上面的表达式返回Bool ,因此是truefalse 。 您可以假设filter的闭包需要返回Bool ,然后仅返回通过条件的值数组,即返回true

这是前面的示例“扩展”,仅包括奇数

  let值= [11、13、14、17、21、33、22] 
让偶数= values.filter({(value:Int)-> Bool in
返回值%2!= 0
})打印(偶数)
//输出:[11、13、17、21、33]

在示例中,闭包应该返回一个布尔值,用-> Bool表示。 它提供了一个参数(集合中的项目),并返回value % 2 != 0

您可以结合使用Map,Reduce和Filter功能吗? 你当然可以!

假设我们有一班学生,每个学生都知道他们的出生年份。 您想对2000年或之后出生的所有学生的年龄求和。

这是您的操作方式:

 让年= [1989、1992、2003、1970、2014、2001、2015、1990、2000、1999] 
let sum = years.filter({$ 0> = 2000})。map({2017-$ 0})。reduce(0,+)
打印(总和)
//输出:52

上面的代码示例使用chaining ,您可以在其中链接函数,并将一个函数的结果用作另一个函数的输入。

换句话说,在filter函数的结果数组上调用map函数,在map函数的结果数组上调用reduce函数。

代码本身非常简单:

  • 首先,您将任意出生年份的数组声明为整数。
  • 然后,使用filter功能过滤该数组。 如果该值大于或等于2000 ,则它将包含在结果数组中。 在此必须注意,结果的大小小于年的大小,因为不包括1989这样的值!
  • 然后,使用map功能对每个项目执行操作,计算学生在2017年的年龄。
  • 最后,使用+操作将集合减小为一个值,创建所有年龄的总和。

成为专业的iOS开发人员

开始使用iOS 12和Swift 4

注册我们的iOS开发课程“ 零到App Store”,以学习Swift 4的iOS开发,并开始您的专业iOS生涯。

了解更多 免费开始

现在,如果要使用for循环编写所有代码,该怎么办? 您至少需要3个for循环,1个if语句和3套辅助集合变量。 使用Map,Reduce和Filter,您只需两行代码即可完成操作,而不会损失可读性和性能。 太棒了!

Map,Reduce和Filter函数在许多编程语言(包括Java,JavaScript,Ruby和PHP)中具有同样强大的实现。 学习如何使用函数式编程概念来处理数据可能是一个挑战,但它无疑会节省您的时间,并使您的代码更简洁,可读性和性能更高。

学到更多? 以下是有关Map,Reduce和Filter的Apple官方文档:

  • 地图(_:)
  • 降低(_:_:)
  • 过滤(_:)