在上一章,咱们学习了不可变性和并发。在这一章,咱们将学习高阶函数和闭包。前端
若是你尚未阅读过第一部分和第二部分,能够点击这里阅读:react
高阶函数是能够接受将函数做为输入参数,也能够接受将函数做为输出结果的一类函数。很酷吧?android
可是为何有人想要那样作呢?ios
让咱们看一个例子。假设我想压缩一堆文件。我想用两种压缩格式来作 — ZIP 或者 RAR 格式。若是用传统的 Java 来实现,一般会使用 策略模式。git
首先,建立一个定义策略的接口:github
public interface CompressionStrategy {
void compress(List<File> files);
}
复制代码
而后,像如下代码同样实现两种策略:编程
public class ZipCompressionStrategy implements CompressionStrategy {
@Override public void compress(List<File> files) {
// Do ZIP stuff
}
}
public class RarCompressionStrategy implements CompressionStrategy {
@Override public void compress(List<File> files) {
// Do RAR stuff
}
}
复制代码
在运行时,咱们就可使用任意一种策略:后端
public CompressionStrategy decideStrategy(Strategy strategy) {
switch (strategy) {
case ZIP:
return new ZipCompressionStrategy();
case RAR:
return new RarCompressionStrategy();
}
}
复制代码
使用这种方式有一堆的代码和须要遵循的格式。bash
其实咱们所要作的只是根据不一样的变量实现两种不一样的业务逻辑。因为业务逻辑不能在 Java 中独立存在,因此必须用类和接口去修饰。闭包
若是可以直接传递业务逻辑,那不是很好吗?也就是说,若是能够把函数看成变量来处理,那么可否像传递变量和数据同样轻松地传递业务逻辑?
这正是高阶函数的功能!
如今,从高阶函数的角度来看这同一个例子。这里我要使用 Kotlin ,由于 Java 8 的 lambdas 表达式仍然包含了咱们想要避免的 一些建立函数接口的方式 。
fun compress(files: List<File>, applyStrategy: (List<File>) -> CompressedFiles){
applyStrategy(files)
}
复制代码
compress
方法接受两个参数 —— 一个文件列表和一个类型为 List<File> -> CompressedFiles
的 applyStrategy
函数。也就是说,它是一个函数,它接受一个文件列表并返回 CompressedFiles
。
如今,咱们调用 compress
时,传入的参数能够是任意接收文件列表并返回压缩文件的函数。:
compress(fileList, {files -> // ZIP it})
compress(fileList, {files -> // RAR it})
复制代码
这样代码看起来干净多了。
因此高阶函数容许咱们传递逻辑并将代码看成数据处理。
闭包是能够捕捉其环境的函数。让咱们经过一个例子来理解这个概念。假设给一个 view 设置了一个 click listener,在其方法内部想要打印一些值:
int x = 5;
view.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View v) {
System.out.println(x);
}
});
复制代码
Java 里面不容许咱们这样作,由于 x
不是 final 的。在 Java 里 x
必须声明为 final,因为 click listener
可能在任意时间执行, 当它执行时 x
可能已经不存在或者值已经被改变,因此在 Java 里 x
必须声明为 final
。Java 强制咱们把这个变量声明为 final,其实是为了把它设置成不可变的。
一旦它是不可变的,Java 就知道无论 click listener 何时执行,x
都等于 5
。这样的系统并不完美,由于 x
能够指向一个列表,尽管列表的引用是不可变的,其中的值却能够被修改.
Java 没有一个机制可让函数去捕捉和响应超过它做用域的变量。Java 函数不能捕捉或者涵盖到它们环境的变化。
让咱们尝试在 Kotlin 中作相同的事。咱们甚至不须要匿名内部类,由于在 Kotlin 中函数是「一等公民」:
var x = 5
view.setOnClickListener { println(x) }
复制代码
这在 Kotlin 中是彻底有效的。Kotlin 中的函数都是闭包。他们能够跟踪和响应其环境中的更新。
第一次触发 click listener 时, 会打印 5
。若是咱们改变 x
的值好比令 x = 9
,再次触发 click listener ,此次会打印9
。
闭包有不少很是好的用例。不管什么时候,只要你想让业务逻辑响应环境中的状态变化,那就可使用闭包。
假设你在一个按钮上设置了点击 listener, 点击按钮会弹出对话框向用户显示一组消息。若是没有闭包,则每次消息更改时都必须使用新的消息列表而且初始化新的 listener。
有了闭包,你能够在某个地方存储消息列表并把列表的引用传递给 listener,就像咱们上面作的同样,这个 listener 就会一直展现最新的消息。
**闭包也能够用来完全替换对象。**这种用法常常出如今函数式编程语言的编程实践中,在那里你可能须要用到一些 OOP(面向对象编程)的编程方法,可是所使用的语言并不支持。
咱们来看个例子:
class Dog {
private var weight: Int = 10
fun eat(food: Int) {
weight += food
}
fun workout(intensity: Int) {
weight -= intensity
}
}
复制代码
我有一条狗在喂食时体重增长,运动时体重减轻。咱们能用闭包来描述相同的行为吗?
fun main(args: Array<String>) {
dog(Action.feed)(5)
}
val dog = { action: Action ->
var weight: Int = 10
when (action) {
Action.feed -> { food: Int -> weight += food; println(weight) }
Action.workout -> { intensity: Int -> weight -= intensity; println(weight) }
}
}
enum class Action {
feed, workout
}
复制代码
dog
函数接受一个 Action
参数,这个 action 要么是给狗喂食,要么是让它去运动。当在 main
中调用 dog(Action.feed)(5)
,结果将是 15
。 dog
函数接受了一个 feed
动做,并返回了另一个真正去给狗喂食的函数。若是把 5
传递给这个返回的函数,它将把狗狗的体重增长到 10 + 5 = 15
并打印出来。
因此结合闭包和高阶函数,咱们没有使用 OOP 就有了对象。
可能你在真正写代码的时候不会这样作,可是知道能够这样作也是蛮有趣的。确实,闭包被称为可怜人的对象。
在许多状况下,相比于 OOP 高阶函数让咱们能够更好地封装业务逻辑,咱们能够将它们当作数据同样传递。闭包捕获其周围环境,帮助咱们有效地使用高阶函数。
在下一部分,咱们将学习如何以函数式的方法去处理错误。
若是你喜欢这篇文字,能够点击下面的 按钮。我通知了他们每个人,我也感激他们每个人。
感谢 Abhay Sood 和 s0h4m.
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖Android、iOS、React、前端、后端、产品、设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。