[Kotlin] 关于lambda,你想知道的都在这里

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语言的推广工作。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343