从Java语言转到Kotlin,最让人头疼的问题恐怕就是lambda表达式了。
lambda,准确的中文翻译是:匿名函数。
不过,在Kotlin语言中本身就有匿名函数的概念,为了区分,我们姑且把它叫做Lambda表达式。
对于Java程序员来说,这是一个比较新的概念。而在计算机领域,这其实是一个非常普遍的概念。在C++11,OC,Java8,Python等语言中均有相应实现。
一起来简单看一下其它语言关于lambda表达式的实现!
C++
Java语言的老祖宗C++11标准已经开始支持使用lambda表达式了!
语法:[ capture ] ( params) mutable exception attribute -> ret { body }
这是一个完整的lambda表达式语法,capture表示捕获的外部变量列表,mutable修饰说明lambda表达式内部代码是否可以修改捕获的外部变量的值。exception表示lambda表达式抛出的异常,和函数声明类似。
#include <functional>
int main() {
auto sum = [] (int x, int y) { return x + y; };
std::cout << "sum(3, 4) = " << sum(3, 4) << std::endl;
// lambda表达式本身就是一个函数。因此,声明可以用函数接收,like this:
std::function<int (int, int)> sum1 = [] (int x, int y) { return x + y; };
std::cout << "sum1(3, 4) = " << sum1(3, 4) << endl;
}
可以看到,C++在实现lambda表达式上面显得有点中规中矩,基本上就是将函数名去掉,再完整Copy。估计它老人家年纪大了,也懒得动了。不过,它的这种实现恰好可以作为lambda表达式实现的范本。可以说,这是lambda表达式实现最完整、灵活性也最高的版本。
C++ 灵活性... LOL
OC
OC语言中,是我最早看到lambda表达式实现的地方。OC其实也是一门古老的语言,像C++一样提供了lambda表达式实现,并且实现的更早。在OC语言中,它叫做block,中文翻译为代码块。一起来看一下它的实现:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
int(^sum) (int, int) = ^(int x, int y) {
return x + y;
};
NSLog(@"sum(3, 4) = %i", sum(3, 4));
}
return 0;
}
Java8
在Java8语言标准中,Oracle官方终于提供了lambda表达式的实现。
语法: ( params) -> { expressions; }
package com.company;
import java.util.function.BiFunction;
public class Main {
public static void main(String[] args) {
BiFunction<Integer, Integer, Integer> sum = (Integer x, Integer y) -> {
return x + y;
};
System.out.println("sum(3, 4) = " + sum.apply(3, 4));
}
}
PS:这里需要说明一点的是,虽然我没有声明函数式接口,就完成了lambda表达式的声明。这并不代表大家在使用的过程中不需要使用函数式接口,在Java8语言中,只提供了少量的函数式接口可以使用,这里恰好可以实现我的简单需求,这其实是一个小小的tricks。在大多数使用场景中,你依然需要先声明一个函数式接口。
什么?你还不知道什么叫做函数式接口,赶紧去看官方文档吧!
有人说,这样的设计不是掩耳盗铃吗?
-_- || ,是的,不得不说,这是Java8 lambda表达式设计的一大遗憾!
好啦,接下来,我们开始本文的重点,Kotlin语言lambda表达式的相关知识!
基础知识
废话不多说,我们直接开始。Kotlin语言中,lambda表达式的完整语法如下:
{ params -> expressions }
params表示参数列表,expressions表示具体实现,可以是单行语句,也可以是多行语句。
作为Java语言的近亲,lambda表达式的语法和Java8非常接近。不过,由于Kotlin语言天然支持函数式编程的特性。声明lambda表达式不需要显式声明函数式接口,显得优雅了许多。
来看一下实现上述同样功能,Kotlin语言的实现:
val sum = { x: Int, y: Int -> x + y; };
print("sum(3, 4) = ${sum(3, 4)}")
类型推导
lambda表达式常常和类型推导一起使用,刚开始使用lambda表达式的时候,总是会遇到到底该不该使用具体类型声明的疑惑。其实,这并不是一个难题,在思考类型推导的时候,注意以下两点即可:
1)声明lambda表达式:如果在左边定义中,已经写了具体的类型声明,后面的实现就可以不用。反之,实现中则需要具体的声明。这里,可能有人会问,如果有多个参数,我一部分在定义中声明,一部分在实现中声明,是否可以?LOL,亲爱的,你觉得呢?
2)使用lambda表达式:基本不用,某些特殊情况可能需要。
val sum: (x: Int, y: Int) -> Int = { x, y -> x + y }
这里要注意一个非常的特殊的lambda表达式的写法:如果一个lambda表达式只有一个参数,这个参数可以使用it指代,注意只能使用it指代,不能使用其它的单词代替,这点要谨记!看下面的例子:
val condition: (x: Int) -> Boolean = { x -> x > 0 }
// 等价于(注意:这里只能用it,不能使用其它单词)
val condition: (x: Int) -> Boolean = { it > 0 }
这在控制流中使用比较广泛,关于控制流的使用,大家如果觉得有必要讲解一下,在文章最后评论告诉我。
Closure(闭包)
敲黑板!!!
这是大家非常容易混淆的概念,我们常常把lambda表达式也称为闭包,这其实无可厚非?但这真的是一个概念吗?
其实并不是,这里要先提一个新的概念capture,这个单词大家在看C++例子的时候已经见到过了。中文意思是捕获,是指闭包使用非自己作用域的外部变量。闭包这个概念与capture息息相关,简单来说,可以用一句话概括:
如果lambda表达式访问了它的作用域外部的变量,这个lambda表达式加上它访问的外部变量一起就构成了闭包
var sum = 0
ints.filter { it > 0 }.forEach {
sum += it
}
print(sum)
从某种层面来说,这个概念并不影响使用,但了解它的意思有利于更深层次理解lambda表达式。
lambda表达式的问题
同函数不一样,Kotlin版本lambda表达式中不允许使用return关键字。return关键字在解决多分支语句代码优化方面有很好的作用。有人说,如果我一定要使用return关键字怎么办,目前比较好的替代方案是:匿名函数
大部分情况下,匿名函数和lambda表达式几乎可以通用,推荐使用lambda表达式。两者唯一的区别就是上面提到的匿名函数可以使用return关键字,而lambda表达式不行。一起来看一下匿名函数的简单用法:
// 这是一个完整函数
fun sum(x: Int, y: Int): Int {
return x + y
}
// 转化为匿名函数
fun(x: Int, y: Int): Int {
return x + y
}
匿名函数因为没有函数名称,不能直接使用,需要使用一个变量接收。然后,调用使用这个变量间接调用这个匿名函数。
看到这里,细心的同学一定会有所疑问,看这里:
// 这里用一段伪代码模拟http请求
fun httpRequest(url: String, onSuccess: (code: Int)->Unit) {
...
}
// 在调用的时候,我们需要在回调中对Code进行处理
httpRequest(url = "http://www.youngfeng.com?id=xxx",
onSuccess = { code ->
if(code == 1) {
....
return@httpRequest
}
})
在这段代码中,明显在闭包中使用了return关键字,你为什么说不可以使用呢?
囧... 是的,这里的确使用了return关键字,可是这里的return并不表示闭包逻辑退出,而是退出整个httpRequest函数。换而言之,如果闭包是服务于某个函数的,在其中的确可以使用return关键字,但这表示退出函数逻辑,而闭包本身是不能使用return关键字进行逻辑分支的。
提问:这里Kotlin语言设计lambda表达式不允许使用return关键字你认为是否是一个缺陷呢?
这个问题文章最后我们再做讨论
尾随闭包
在上文的讲解中,我们忽略了一个非常重要的概念:尾随闭包。这个概念在实际开发中经常使用,初次接触这个概念的Java程序员会感觉到非常的不适应,需要一段时间的磨合后才能慢慢习惯这种写法。别急,且听我慢慢分解。
lambda表达式可以作为函数参数使用,如果一个函数的最后一个参数恰好是一个lambda表达式,lambda表达式可以写到括号的外面。
使用尾随闭包后,上面的表达式可以这样表示:
httpRequest(url = "http://www.youngfeng.com?id=xxx") { code ->
if(code == 1) {
....
return@httpRequest
}
}
对于这样一种表达方式,Java阵营的同学常常会有这样一种疑惑:通常来说,括号外面应该是函数的定义,这样的表达不会和定义混淆吗?
其实是不会的,注意看上面,这里是函数的调用,而非定义。如果你看到一个函数在调用的时候,表达式写到了括号的外面,就表示它一定是一个尾随闭包实现。而如果是函数定义,就没什么可说的了!
尽管如此,对于这样的一种表达方式,依然要在平时编码过程中不断使用,不要去排斥它。否则,很难驾轻就熟。
总结
这篇文章从以下几个方面介绍了关于lambda表达式的相关知识:
- 基础知识(语法:{ params -> expressions }
- 灵活运用类型推导
- 闭包与lambda表达式的区别
- 闭包的“缺陷”(不能使用return关键字)
- 尾随闭包
答疑解惑
a)使用lambda表达式是否容易造成代码可阅读性变差?
使用lambda表达式带来的一个最直观的问题就是:变量的类型有点难以预测,如果命名不规范将需要查看源码才知道具体变量的意思。从这个层面来说,的确带来一定的阅读问题,但只要养成良好的命名习惯,这个问题是可以避免的。这个问题对于使用弱类型语言(如JS)编码的同学来说,根本不是一个问题!
b)lambda表达式和函数是什么关系?针对这两者应该如何取舍?
注意:lambda表达式和函数其实完全是一回事,lambda表达式可以理解为一个没有函数声明的匿名函数。因此,在使用的过程中,如果你需要一个不需要声明就使用的函数,使用lambda表达式将是一个不二的选择。在Android应用开发中,lambda表达式通常用在回调场景中。
c)在Java8语言中,lambda表达式是可以明确使用return关键字的,而Kotlin语言中,lambda表达式却不能使用return关键字。这是一个设计缺陷吗?
我认为不是!注意:Kotlin语言是一门支持类型推导的语言,通过对闭包上下文的分析,我们可以推断出哪一部分是作为返回值return的,只是在主观阅读上缺少了一个明显的return关键字而已,但这并不影响使用!
附加题
看完了所有关于lambda表达式的介绍,一起来做一道题,检验一下学习效果。
fun f1(): ()->Unit {
var x = 0
return fun() {
x ++
println("x = $x")
}
}
fun main(args: Array<String>) {
val closure = f1()
closure()
closure()
closure()
}
这道题是JavaScript语言中非常经典的闭包例子,把它放到Kotlin语言中,请大家先不要用编译器去编译,在文章下方评论告诉我,运行这段代码,输出是什么?为什么?
欢迎加入Kotlin交流群
如果你也喜欢Kotlin语言,欢迎加入我的Kotlin交流群: 329673958 ,一起来参与Kotlin语言的推广工作。