Kotlin 真香之 Lambda 表达式
函数类型与函数实例化
在kotlin中,函数与数据同样是一等公民,这意味着它们可以存储在变量与数据结构中、作为参数传递给其他高阶函数以及从其他高阶函数返回,可以像操作任何其他非函数值一样操作函数。
1 | val square = { item: Int -> item * item } |
那么如果函数与数据同样是一等公民,那么数据拥有的类型
,实例
,函数是不是也同样拥有呢?答案是肯定的。
函数类型
函数也是拥有类型的,不过不同于Int
、String
这些大家耳熟能详的,而是以(Type1,Type2,...)->Type
这种形式来定义的。例如上文中提到的square,它的类型就是(Int)->Int
。
1 | val square: (Int) -> Int = { item: Int -> |
我们知道数据类型是可以继承的,以满足我们个性化的定制需求,函数类型也可以吗?当然!
1 | class Square : (Int) -> Int { |
函数类型也可以定义在函数签名之中,我们看看这样的例子,参数action的类型就是函数类型:
1 | fun doWithInt(item: Int, action: (Int) -> Int): Int { |
函数实例
那么函数有了类型,就一定有实例,就像Int可以有3,String可以有HelloWorld
。我们来看看函数实例有什么样的要求。
首先已经声明的函数,都是具体的实例,无论是在class内部,还是属性等等。来看看例子,其中::
表示创建对方法或者class的引用。
1 | fun main() { |
除了前面的方式以为,还有另两种常用的实例化方法。一是lambda表达式、另一个是匿名函数。
先看看匿名函数的方法,匿名函数与普通函数的申明,区别仅仅在有没有方法名字上:
1 | val square = fun(item: Int): Int { |
另一个就是本文的重点lambda表达式
,其语法也很简单:
{ param1 : Type1, param2 : Type2 , … -> body }
1.永远在大括号内。
2.参数两边不能有小括号。如果没有参数,可以留白,同时->也可以去掉。
3.方法体如果有多行,直接换行即可。
1 | val square = { item:Int -> |
Lambda表达式想对于匿名函数提供了更多便利的地方,大家尽量使用lambda表达式。
1.it代替单个参数
在很多情况下,参数可能只有一个,在lambda表达式中就可以用it来代替,从而简化语法。
1 | val square1:(Int)->Int = { |
2.lambda表达式作为最后一个参数
在Kotlin中有一个约定:如果函数的最后一个参数是函数,那么作为相应参数传入的lambda表达式可以放在圆括号之外,这种语法也叫做拖尾lambda表达式。
下面两种doWithInt的调用是等价的。
1 | fun doWithInt(item: Int, action: (Int) -> Int): Int { |
SAM转换
前面我们讨论kotlin中的lambda很是欢畅,但到了Java这一侧就不那么愉快了。毕竟Java8中的lambda表达式只是闭包和invokeDynamic拼凑起来的四不像,Java8并没有函数这种类型,更别说kotlin对接的Java平台还只是Java7。
SAM = Single Abstract Method,即唯一抽象方法!
但兽人永不为奴,啊,呸,是kotlin无敌,针对SAM给我们提供了一个很甜的语法糖。我们来看看下面这个例子:
1 | var listener = View.OnClickListener { |
详细分析一下,View.OnClickListener的官方函数是java的,申明如下:
1 | /** |
Java这端需要的是OnClickListener匿名类的实例,而前面代码中提供的还只是lambda表达式。两者是不同的东西呀,那是怎么回事呢?这里需要提出一个SAM构造器的概念。
FunctionInterfaceName { lambda_function }
这是专门为Java提供的语法糖,用于将lambda表达式转换成相应的匿名类的实例。
同样,还可以在函数调用中使用SAM转换,由于类型是可以推断出来的,前面的FunctionInterfaceName甚至可以省略,下面两种写法是等价的。
1 | fun doWithView(view: View) { |
我们扩展一下,如果是kotlin会怎样呢?首先我们申明一个kotlin的接口。
1 | interface KotlinInterface { |
接着我们和前面Java一样,试图通过SAM构造器的方式,来创建匿名内部类的实例,情况会怎样?
1 | // this code will leads to compile error. |
这里居然报错了!!!
聪明的读者已经想到了,Kotlin中如果使用这种语法,是在调用KotlinInterface的构造函数,由于这是一个接口,并没有对应的构造函数,所以就报错了。那这里就奇怪了,Java中是SAM构造器语法,到Kotlin中这就是调用构造函数?答案确实是这样的,这是专门为Java提供的语法糖!
所以问题的关键是,为什么Kotlin要做这个限制呢?原因在于kotlin拥有更高阶的能力—高阶函数。对于Java而言,由于没有函数这个类型,对于SAM,只能是构造一个匿名对象,变相地绕过语言层面的限制。但kotlin不一样,其函数本身就是一等公民,没必要舍近求远。针对OnClickListener,Kotlin更愿意这样来申明:
1 | class View { |
带Receiver的lambda
结合前面的文章,我们知道定义扩展函数,这个语法同样适用于lambda表达式。那么既然有了扩展函数,为什么还要给lambda也提供同样的能力呢?
假设,我们构建一个 json 的DSL (Domain Specific Language),首先定义了一个生成键值对的方法。
1 | class JsonBuilder { |
这里关键在于it,对于DSL而言,如果每个地方都需要调用it的话,会很别扭。前面提到扩展函数,可以用this,或者省略,来调用Receiver的方法。如果我们给Receiver扩展一个lambda表达式,那就可以省略it了。
1 | class JsonBuilder { |
那么为什么我们非得省略it这个了?是为了DSL更加美观,让使用者不必明白kotlin的相关语法细节。
我们来实现一个极简版本的jsonDSL(-先不考虑JSON数组的情况-),看看会变成什么样子。
1 | import java.lang.StringBuilder |
最后会输出
如果每次调用key,value方法都需要加上it,就达不到DSL的目的了。
这里给大家一个GitHub上,大神写的kotlinDSL的Demo,GitHub-rybalkinsd/kohttp:Kotlin DSL http client,语法还是相当舒服的。
文档信息
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)
- 发表日期:2019年09月15日
- 社交媒体:weibo.com/woaitqs
- Feed订阅:www.woaitqs.cc/atom.xml