《Kotlin核心编程》 ——2.3.6 Lambda是语法糖

举报
华章计算机 发表于 2020/02/21 20:44:12 2020/02/21
【摘要】 本节书摘来自华章计算机《Kotlin核心编程》 —— 书中第2章,第2.3.6节,作者是水滴技术团队 。

2.3.6 Lambda是语法糖

提到Lambda表达式,也许你听说过所谓的Lambda演算。其实这是两个不同的概念,Lambda演算和图灵机一样,是一种支持理论上完备的形式系统,也是理解函数式编程的理论基础。古老的Lisp语言就是基于Lambda演算系统而来的,在Lisp中,匿名函数是重要的组成部分,它也被叫作Lambda表达式,这就是Lambda表达式名字的由来。所以,相较Lambda演算而言,Lambda表达式是更加简单的概念。你可以把它理解成简化表达后的匿名函数,实质上它就是一种语法糖。

我们先来分析下代码清单2-2中的filterCountries方法的匿名函数,会发现:

fun(country:Country)显得比较啰唆,因为编译器会推导类型,所以只需要一个代表变量的country就行了;

return关键字也可以省略,这里返回的是一个有值的表达式;

模仿函数类型的语法,我们可以用->把参数和返回值连接在一起。

因此,简化后的表达就变成了这个样子:

countryApp.filterCountries(countries, {

    country ->

    country.continient == "EU" && country.population > 10000

})

是不是非常简洁?这个就是Lambda表达式,它与匿名函数一样,是一种函数字面量。我们再来讲解下Lambda具体的语法。现在用Lambda的形式来定义一个加法操作:

val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }

由于支持类型推导,我们可以采用两种方式进行简化:

val sum = { x: Int, y: Int -> x + y }

或者是:

val sum: (Int, Int) -> Int = { x, y -> x + y }

现在来总结下Lambda的语法:

一个Lambda表达式必须通过{}来包裹;

如果Lambda声明了参数部分的类型,且返回值类型支持类型推导,那么Lambda变量就可以省略函数类型声明;

如果Lambda变量声明了函数类型,那么Lambda的参数部分的类型就可以省略。

此外,如果Lambda表达式返回的不是Unit,那么默认最后一行表达式的值类型就是返回值类型,如:

val foo = { x: Int ->

    val y = x + 1

    y // 返回值是 y

}

>>> foo(1)

2

Lambda看起来似乎很简单。那么再思考一个场景,如果用fun关键字来声明Lambda表达式又会怎么样?如代码清单2-3所示。

代码清单 2-3

image.png

1.单个参数的隐式名称

首先,也许你在it这个关键字上停留了好几秒,然后依旧不明其意。其实它也是Kotlin简化Lambda表达的一种语法糖,叫作单个参数的隐式名称,代表了这个Lambda所接收的单个参数。这里的调用等价于:

listOf(1, 2, 3).forEach { item -> foo(item) }

默认情况下,我们可以直接用it来代表item,而不需要用item->进行声明。

其次,这行代码的结果可能出乎了你的意料,执行后你会发现什么也没有。为什么会这样?这一次,我们必须要借助IDE的帮助了,以下是把foo函数用IDE转化后的Java代码:

@JvmStatic

@NotNull

public static final Function0 foo(final int var0) {

    return (Function0)(new Function0 () {

        // $FF: synthetic method

        // $FF: bridge method

        public Object invoke() {

            this.invoke();

            return Unit.INSTANCE;

        }

 

        public final void invoke () {

            int var1 = var0;

            System.out.print(var1);

        }

    });

}

以上是字节码反编译的Java代码,从中我们可以发现Kotlin实现Lambda表达式的机理。

2. Function类型

Kotlin在JVM层设计了Function类型(Function0、Function1……Function22)来兼容Java的Lambda表达式,其中的后缀数字代表了Lambda参数的数量,如以上的foo函数构建的其实是一个无参Lambda,所以对应的接口是Function0,如果有一个参数那么对应的就是Function1。它在源码中是如下定义的:

package kotlin.jvm.functions

 

interface Function1<in P1, out R> : kotlin.Function<R> {

    fun invoke(p1: P1): R

}

可见每个Function类型都有一个invoke方法,稍后会详细介绍这个方法。

设计Function类型的主要目的之一就是要兼容Java,实现在Kotlin中也能调用Java的Lambda。在Java中,实际上并不支持把函数作为参数,而是通过函数式接口来实现这一特性。所以如果我们要把Java的Lambda传给Kotlin,那么它们就必须实现Kotlin的Function接口,在Kotlin中我们则不需要跟它们打交道。在第6章我们会介绍如何在Kotlin中调用Java的函数式接口。

神奇的数字—22

也许你会问一个问题:为什么这里Function类型最大的是Function22?如果Lambda的参数超过了22个,那该怎么办呢?

虽然22个参数已经够多了,然而现实中也许我们真的需要超过22个参数。其实,在Scala的设计中也遵循了22个数字的设计,这似乎已经成了业界的一种惯例。然而,这个22的设计也给Scala开发者带来了不少麻烦。所以,Kotlin在设计的时候便考虑到了这种情况,除了23个常用的Function类型外,还有一个FunctionN。在参数真的超过22个的时候,我们就可以依靠它来解决问题。更多细节可以参考https://github.com/JetBrains/kotlin/blob/master/spec-docs/function-types.md。

3. invoke方法

代码清单2-3中的foo函数的返回类型是Function0。这也意味着,如果我们调用了foo(n),那么实质上仅仅是构造了一个Function0对象。这个对象并不等价于我们要调用的过程本身。通过源码可以发现,需要调用Function0的invoke方法才能执行println方法。所以,我们的疑惑也迎刃而解,上述的例子必须如下修改,才能够最终打印出我们想要的结果:

fun foo(int: Int) = {

    print(int)

}

>>> listOf(1, 2, 3).forEach { foo(it).invoke() } // 增加了invoke调用

 123

也许你觉得invoke这种语法显得丑陋,不符合Kotlin简洁表达的设计理念。确实如此,所以我们还可以用熟悉的括号调用来替代invoke,如下所示:

>>> listOf(1, 2, 3).forEach { foo(it)() }

123


【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。