声明高阶函数
高阶函数: 就是以另一个函数作为参数或者返回值的函数。 在 kotlin
中,函数可以用 lambda
或者函数引用来表示。因此,任何以 lambda
或者函数引用作为参数或返回值的函数,或者两者都满足的函数都是高阶函数。
函数类型
我们先来看一个例子:
// 通过类型推导,需要显式指定参数类型
val sum = { x: Int, y: Int -> x + y }
val action = { println(42) }
在这个例子中,编译器通过类型推导知道 sum
和 action
具有函数类型,显式类型声明是这样的:
// 有两个 Int 类型的参数和 Int 返回值的函数
val sum: (Int, Int) -> Int = { x, y -> x + y }
// 没有参数和返回值的函数
val action: () -> Unit = { println(42) }
声明函数类型,需要将函数参数类型放在括号中,紧接着是一个箭头和函数的返回类型。
在 lambda
表达式 { x, y -> x + y }
中省略了参数类型,因为已经在函数类型的变量声明中指定了。
注意:参数一定是用括号括起来的,如果没有参数,也要有括号。Unit
在函数类型声明中是不能省略掉的。
就像其他方法一样,函数类型的返回值也可以标记为可空类型:
var canReturnNull: (Int, Int) -> Int? = { _, _ -> null }
也可以定义一个函数类型可空的变量,需要将整个函数类型的定义包含在括号内并在括号后面添加一个问号,如下:
var funOrNull: ((Int, Int) -> Int)? = null
注意上述两者的微妙区别,一个是返回值可空,一个是函数类型可空。
其实上述我们声明函数类型的时候,没有指定参数名,可以为函数类型声明中的函数指定参数名,比如:
// 给函数类型的参数指定了名字
fun performRequest(url: String, callback: (code: Int, content: String) -> Unit) {
/*..*/
}
fun testPerformRequest() {
val url = "http://test.com"
// 使用定义的名字
performRequest(url, { code, content -> /*..*/ })
// 改变参数的名字
performRequest(url, { x, y -> /**/ })
}
参数名称不会影响类型的匹配,当你声明一个 lambda
时,不必使用和函数类型声明中一样的参数名称,但命名会提升代码可读性并且能用于 IDE
的代码补全。
调用作为参数的函数
我们实现一个任意操作数字 2
和 3
的函数:
fun twoAndThree(operation: (Int, Int) -> Int) {
// 调用函数类型的参数
print(operation(2, 3))
}
twoAndThree { x, y -> x + y }
twoAndThree { x, y -> x * y }
调用作为参数的函数和调用普通函数的语法是一样的:把括号放在函数名后,并把参数放在括号内。
接下来,我们来实现基于 String
类型的 filter
函数,它和作用于集合的泛型版本的原理是相似的:
fun String.filter(predicate: (Char) -> Boolean): String {
val sb = StringBuilder()
forEach { if (predicate(it)) sb.append(it) }
return sb.toString()
}
println("ab.id390lj".filter { it in 'a'..'z' }) //abidlj
filter
函数以一个判断式作为参数,判断式的类型是一个函数,以字符作为参数并返回 Boolean
类型的值。
在 Java 中使用函数类
其背后的原理是: 函数类型被声明为普通的接口,一个函数类型的变量是 FunctionN
接口的一个实现。kotlin
定义了一系列的接口,这些接口对应于不同参数数量的函数:Function0<R>
(没有参数的函数)、 Function1<P1,R>
(一个参数的函数)等等。每个接口定义了一个 invoke
方法,调用这个方法就会执行函数,这是一个 operator
标记的约定方法,当用类似 operation(2, 3)
的方式调用函数时,实际上是调用 operation.invoke(2,3)
的方式,两种方式的调用是等价的。 invoke
方法包含了 lambda
函数体,也就是说,lambda
的代码其实变成 invoke
方法的代码了。
在 Java
中可以很简单的调用使用了函数类型的 kotlin
函数,Java 8
的 lambda
会被自动转换为函数类型的值:
// kotlin
fun processTheAnswer(f: (Int) -> Int) {
println(f(42))
}
// Java
HeightPraticeKt.processTheAnswer(number -> number + 1); //43
在旧版的 Java
中 ,可以传递一个实现了函数接口的 invoke
方法的匿名类的实例:
// 单抽象接口 Function1
HeightPraticeKt.processTheAnswer(new Function1<Integer, Integer>() {
@Override
public Integer invoke(Integer integer) {
return integer+1;
}
});
在 Java
中可以很容易地使用 kotlin
标准库中以 lambda
作为参数的扩展函数,只是没有 kotlin
中看起来那么直观 —— 必须显式地传递一个接收者对象作为第一个参数。
public static void callKotlinMethod() {
List<String> strings = new ArrayList<>();
strings.add("42");
// Java 8 之前
CollectionsKt.forEach(strings, new Function1<String, Unit>() {
@Override
public Unit invoke(String s) {
System.out.println(s);
return Unit.INSTANCE;
}
});
// Java 8 之后,用 lambda 的方式
CollectionsKt.forEach(strings, s -> {
System.out.println(s);
return Unit.INSTANCE;
});
}
因为 kotlin
中 Unit
类型是有值的,所以在 Java
中需要显式地返回它。
函数类型的参数默认值和 null 值
声明函数类型的参数的时候可以指定参数的默认值,我们看一下之前实现过的 joinToString
函数:
@JvmOverloads
fun <T> Collection<T>.joinToString(
separator: String = ",",
prefix: String = "(",
postfix: String = ")"
): String {
val builder = StringBuilder(prefix)
for ((index, element) in withIndex()) {
if (index > 0) builder.append(separator)
// 使用 toString 方法将对象转换为字符串
builder.append(element)
}
builder.append(postfix)
return builder.toString()
}
这个实现已经很灵活了,但是它总是使用 toString
方法将对象转换为字符串,而不能打印其他内容,为了解决这个问题,可以定义一个函数类型的参数并用一个 lambda
作为它的默认值:
@JvmOverloads
fun <T> Collection<T>.joinToString(
separator: String = ",",
prefix: String = "(",
postfix: String = ")",
transform: (T) -> String = { it.toString() } // 默认实现
): String {
val builder = StringBuilder(prefix)
for ((index, element) in withIndex()) {
if (index > 0) builder.append(separator)
// 使用函数类型的参数
builder.append(transform(element))
}
builder.append(postfix)
return builder.toString()
}
val list = listOf("fanda", "liuhang")
println(list.joinToString()) //(fanda,liuhang)
println(list.joinToString { it.toUpperCase() }) //(FANDA,LIUHANG)
println(list.joinToString(separator = "!", transform = { it.toUpperCase() })) //(FANDA!LIUHANG)
注意:这是一个泛型函数,它有一个类型参数 T
表示集合中的元素的类型, lambda
将接收这个类型的参数。声明函数类型的默认值只需要把 lambda
作为值放在 =
号后面即可。
除了默认实现的方式来达到选择性地传递,另一种选择是声明一个参数为可空的函数类型,然后对参数进行判空处理:
fun foo(callback: (() -> Unit)?) {
if (callback != null) {
callback()
}
// 这种方式也可以调用
callback?.invoke()
}
上述函数的两种调用是等价的,因为函数类型是一个包含 invoke
方法的接口的具体实现,作为一个普通方法,invoke
可以通过安全调用语法被调用。
用第二种方式重写 joinToString
函数如下:
// 终极版本,用扩展函数的方式实现并带有 lambda 参数,但带 null 默认值
@JvmOverloads
fun <T> Collection<T>.joinToString(
separator: String = ",",
prefix: String = "(",
postfix: String = ")",
transform: ((T) -> String)? = null // 声明一个函数类型可空的参数
): String {
val builder = StringBuilder(prefix)
for ((index, element) in withIndex()) {
if (index > 0) builder.append(separator)
// 安全调用,Elvis 运算符
var result = transform?.invoke(element) ?: element.toString()
builder.append(result)
}
builder.append(postfix)
return builder.toString()
}
返回函数的函数
从函数中返回另一个函数并没有将函数作为参数传递那么常用,但它仍然非常有用。想象一下程序中的一段逻辑可能会因为程序的状态或者其他条件而产生变化——比如说,运输费用的计算依赖于选择的运输方式。可以定义一个函数用来选择恰当的逻辑变体并将它作为另一个函数返回。
enum class Delivery { STANDARD, EXPEDITED }
class Order(val itemCount: Int)
fun getShippingCostCalculator(delivery: Delivery): (Order) -> Double {
if (delivery == Delivery.EXPEDITED) {
return { order -> 6 + 2.1 * order.itemCount }
}
return { order -> 1.2 * order.itemCount }
}
fun testGetShippingCostCalculator() {
// 根据不同的枚举类型,返回指定的函数
val calculator = getShippingCostCalculator(Delivery.EXPEDITED)
// 调用函数获取数据
println("Shopping costa ${calculator(Order(3))}")
}
声明一个返回另一个函数的函数,需要指定一个函数类型作为返回类型。上述函数返回一个以 Order
作为参数并返回一个 Double
类型的值的函数。要返回一个函数,需要写一个 return
表达式,再跟上一个 lambda
、 一个成员引用或其他的函数类型的表达式,比如一个(函数类型的)局部变量。
通过 lambda 去除重复代码
函数类型和 lambda
表达式一起组成了一个创建可重用代码的好工具。我们来看一个分析网站访问的例子,SiteVisit
类用于保存每次访问的路径、 持续时间和操作系统,不同的操作系统使用枚举类型来表示:
enum class OS { WINDOWS, LINUX, MAC, IOS, ANDROID }
data class SiteVisit(val path: String, val duration: Double, val os: OS)
val log = listOf(
SiteVisit("/", 34.0, OS.WINDOWS),
SiteVisit("/", 22.0, OS.MAC),
SiteVisit("/login", 12.0, OS.WINDOWS),
SiteVisit("/signup", 8.0, OS.IOS),
SiteVisit("/", 16.3, OS.ANDROID)
)
如果你需要显示来自 windows
机器的平均访问时间,可以用 average
函数来完成:
val averageWindowsDuration = log.filter { it.os == OS.WINDOWS }.map (SiteVisit::duration).average()
println(averageWindowsDuration) //23.0
现在假设你要计算来自 MAC
用户的相同数据,为了避免重复,可以将平台类型抽象为一个参数。
// 作为扩展函数,将平台类型抽为参数
fun List<SiteVisit>.averageDurationFor(os: OS) = filter { it.os == os }.map(SiteVisit::duration).average()
println(log.averageDurationFor(OS.MAC))
这样调用起来非常方便,可以对不同的列表查询不同平台类型的平均数据。但是,如果你想对移动平台的数据感兴趣呢?那么上述扩展方法无法使用,需要跟之前一样,这样调用:
fun getMobileAverage() {
val averageWindowsDuration = log.filter { it.os == OS.ANDROID || it.os == OS.IOS }.map(SiteVisit::duration).average()
// val averageWindowsDuration = log.filter { it.os in setOf(OS.IOS, OS.ANDROID) }.map(SiteVisit::duration).average()
println(averageWindowsDuration) //12.15
}
如果我们需要对更多的字段进行判断呢?比如查看 IOS
平台的指定路径的平均时间,也可以实现,如下:
fun getMobilePathAverage() {
val averageWindowsDuration = log.filter { it.os == OS.IOS && it.path == "/signup" }.map(SiteVisit::duration).average()
println(averageWindowsDuration) //12.15
}
此时,你会发现有很多重复的代码,每次有一个新的判断式,都需要重写之前的代码,而仅仅是改一个条件。那么我们使用 lambda
来重构一下,把重复的代码抽出来,把需要更改的代码变成函数的函数类型的参数:
// 作为扩展函数,将判断式抽为函数类型的参数
fun List<SiteVisit>.averageDuration(predicate: (SiteVisit) -> Boolean) {
filter(predicate).map(SiteVisit::duration).average()
}
println(log.averageDuration { it.os == OS.MAC })
println(log.averageDuration { it.os == OS.MAC && it.path == "/signup" })
现在,你不需要重复之前的代码了,只需要传入你想要的判断式即可。
一些广为人知的设计模式可以函数类型和 lambda
表达式进行简化。比如策略模式。没有 lambda
表达式的情况下,你需要声明一个接口,并为没一种可能的策略提供实现类,使用函数类型,可以用一个通用的函数类型来描述策略,然后传递不同的 lambda
表达式作为不同的策略。
内联函数: 消除 lambda 带来的运行时开销
lambda
表达式会被正常编译成匿名类。这表示每调用一次 lambda
表达式,一个额外的类就会被创建。并且如果 lambda
捕捉了某个变量,那么每次调用的时候都会创建一个新的对象。这会带来运行时的额外开销,导致使用 lambda
比使用一个直接执行相同代码的函数效率更低。
但是,如果使用 inline
修饰符标记一个函数,在函数被使用的时候编译器并不会生成函数调用的代码,而是使用函数实现的真实代码替换每一次的函数调用,这样就能避免额外开销。
内联函数如何运作
当一个函数被声明为 inline
时,它的函数体是内联的——换句话说,函数体会被直接替换到函数被调用的地方,而不是被正常调用。来看一个例子以便理解生成的最终代码。
// 内联函数
inline fun <T> synchronized(lock: Lock, action: () -> T) : T{
lock.lock()
try {
return action()
}finally {
lock.unlock()
}
}
synchronized(ReentrantLock()){
println(42)
}
这个函数用于确保一个共享资源不会并发地被多个线程访问,函数锁住一个 Lock
对象,执行代码块,然后释放锁。
上述的例子会被编译器转换成如下的代码:
Lock lock$iv = (Lock)(new ReentrantLock());
lock$iv.lock();
try {
// 被内联的 lambda 体代码
byte var3 = 42;
System.out.println(var3);
} finally {
lock$iv.unlock();
}
可以看到,lambda
表达式和 synchronized
函数的实现都被内联了。 声明为 inline
的函数,在函数被调用时,编译器并不会生成函数调用的代码,而是使用函数实现的真实代码替换该次的函数调用,函数类型的参数也不会被转换成一个实现了函数接口的匿名类。
注意:在调用内联函数的时候也可以传递函数类型的变量作为参数:
class LockOwner(val lock: Lock) {
fun runUnderLock(body: () -> Unit) {
synchronized(lock, body)
}
}
在这种情况下,lambda
的代码在内联函数被调用的时候是不可用的,因此并不会被内联,只有 synchronized
的函数体被内联了,上述的 runUnderLock
函数会被编译器编译成如下类似的代码:
// 函数类型的参数被转换成接口参数
public final void runUnderLock(@NotNull Function0 body) {
Lock lock$iv = this.lock;
lock$iv.lock();
try {
// body 没有被内联,因为在调用的地方还没有 lambda
Object var4 = body.invoke();
} finally {
lock$iv.unlock();
}
}
如果不同的位置使用同一个内联函数,但是用的不同的 lambda
,那么内联函数会在每一个被调用的位置被分别内联。内联函数的代码会被拷贝到使用它的不同地方,并把不同的 lambda
替换到其中。
内联函数的限制
如果 lambda
参数被调用,这样的代码能被容易地内联。但如果 lambda
参数在某个地方被保存起来,以便后面可以继续使用,lambda
表达式的代码将不能被内联,因为必须要有一个包含这些代码的对象存在。一般来说,参数如果被直接调用或者作为参数传递给另外一个 inline
函数,它是可以被内联的。否则,编译器会禁止参数被内联并给出错误信息 Illegal usage of inline-parameter
。
例如,许多作用于序列的函数会返回一些类的实例,这些类代表对应的序列操作并接收 lambda
作为构造方法的参数。以下是 Sequence.map
函数的定义:
public fun <T, R> Sequence<T>.map(transform: (T) -> R): Sequence<R> {
return TransformingSequence(this, transform)
}
map
函数没有直接调用作为 transform
参数传递进来的函数。而是将这个函数传递给一个类的构造方法,构造方法将它保存在一个属性中。为了支持这一点,作为 transform
参数传递的 lambda
需要被编译成标准的非内联的表示法,即一个实现了函数接口的匿名类。
如果一个函数期望两个或更多 lambda
参数,可以选择只内联其中一些参数。这样是有道理的,因为一个 lambda
可能会包含很多代码或者以不允许内联的方式使用。接收这样的非内联 lambda
的参数,可以用 noinline
修饰符来标记它:
// params1 被内联, params2 被标记不需要内联
inline fun foo(params1: () -> Unit, noinline params2: () -> Unit) {
}
注意: 虽然在 Java
中可以调用绝大部分内联函数,但这些调用并不会被内联,而是被编译成普通的函数调用。
内联集合操作
大部分标准库中的集合函数都带有 lambda
参数,相比于使用标准库函数,直接实现这些操作不是更高效吗?例如,让我们来比较以下两个代码中用来过滤一个人员列表的方式:
data class Person(val name: String, val age: Int)
val personList = listOf(Person("fanda", 38), Person("liuhang", 22))
// 输出 [Person(name=fanda, age=38)]
println(personList.filter { it.age > 30 })
前面的代码不用 lambda
表达式也可以实现:
val result = mutableListOf<Person>()
for (person in people) {
if (person.age <= 30) result.add(person)
}
println(result)
在 Kotlin
中,filter
函数被声明为内联函数。最终,第一种实现所产生的字节码和第二种实现所产生的字节码大致是一样的。你可以很安全地使用符合语言习惯的集合操作,Kotlin
对内联函数的支持让你不必担心性能问题。
想象一下现在你连续调用 filter
和 map
两个操作:
println(personList.filter { it.age > 30 }.map(Person::name))
filter
和 map
函数都被声明为 inline
函数,所以它们的函数体会被内联,因此不会产生额外的类或者对象。但是上面的代码却创建了一个中间集合来保存列表过滤的结果,由 filter
函数生成的代码会向这个集合添加元素,而由 map
函数生成的代码会读取这个集合。
如果有大量集合元素需要处理,中间集合的运行开销将成为不可忽视的问题,这时可以在调用链后加上一个 asSquence
调用,用序列来替代集合。但正如你在前面看到的,用来处理序列的 lambda
没有被内联。每一个中间序列被表示成把 lambda
保存在其字段中的对象,而末端操作会导致由每一个中间序列调用组成的调用链被执行。
小结:即便序列上的操作是惰性的,你不应该总是试图在集合操作的调用链后加上 asSquence
。这只在处理大量数据的集合时有用,小的集合可以用普通的集合操作处理。
决定何时将函数声明成内联
不要以为将函数标记为内联的,就能让代码运行得更快,使用 inline
关键字只能提高带有 lambda
参数的函数的性能,其他的情况需要额外的度量和研究。
inline
虽然可以有效减少函数运行时开销(包含减少匿名类的创建),但这是基于将标记的的函数拷贝到每一个调用点来达成的,因此,如果函数体的代码过多,会增大字节码的大小。考虑到 JVM
本身已经提供了强大的内联支持:它会分析代码的执行,并在任何通过内联能够带来好处的时候将函数调用内联。还有一点就是 Kotlin
的内联函数在 Java
调用时并没有其内联的作用。最终,我们应该谨慎考虑添加 inline
,只将一些较小的,并且需要嵌入调用方的函数标记内联,将那些与 lambda
参数无关的代码抽取到一个独立的非内联函数中。
使用内联 lambda 管理资源
lambda
可以去除重复代码的一个常见模式是资源管理:先获取一个资源,完成一个操作,然后释放资源。这里的资源可以表示很多不同的东西:一个文件、 一个锁、 一个数据库事务等。实现这个模式的标准做法是使用 try/finally
语句。资源在 try
代码块被获取,在 finally
代码块中释放。
kotlin
标准库定义了一个叫作 withLock
的函数,实现了跟 Java
中 synchronized
一样的功能,是 Lock
接口的扩展函数,来看一下如何使用它:
ReentrantLock().withLock {
// 这里是在加锁的情况下完成的操作
println(42)
}
这是 kotlin
库中 withLock
函数的定义:
fun <T> Lock.withLock(action: () -> T): T {
lock()
try {
return action()
}finally {
unlock()
}
}
文件是另一种使用这种模式的常见资源类型。 Java 7
甚至为这种模式引入了特殊的语法: try-with-resource
语句,下面是 Java
中使用这种模式的示例:
public static String readFirstLineFormFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
在 kotlin
中没有等价的语法,但是标准库中有一个叫作 use
的函数,有类似的功能:
fun readFirstLineFormFile(path: String): String {
BufferedReader(FileReader(path)).use { br -> return br.readLine() }
}
use
函数是一个扩展函数,接收 lambda
作为参数,这个方法调用 lambda
并且确保资源被关闭,无论 lambda
正常执行还是抛出异常。use
被标记为内联函数,不会引发任何性能开销。
高阶函数中的控制流
把一个 return
语句放在 lambda
中进行返回,会是什么样的情况呢?
lambda 中的返回语句:从一个封装的函数返回
我们分别用普通的函数调用方式和带 lambda
表达式的函数调用方式来对以下代码进行演示:
fun lookForAlice() {
val personList = listOf(Person("Alice", 29), Person("fanda", 18))
for (p in personList) {
if (p.name == "Alice") {
// 正常的函数返回
return println("Found!")
}
}
println("Alice is not found !")
}
fun lookForAliceForEach() {
val personList = listOf(Person("Alice", 29), Person("fanda", 18))
personList.forEach {
if (it.name == "Alice") {
// 在 lambda 中返回
return println("Found!")
}
}
println("Alice is not found !")
}
// 输出
Found!
Found!
上述代码的结果是一样的,代码的实现是等价的。如果你在 lambda
中使用 return
关键字,它会从调用 lambda
的函数中返回,并不只是从 lambda
中返回,这样的 return
语句叫作非局部返回。在上述例子中,return
不只是从 forEach
的 lambda
表达式中返回,还从 lookForAliceForEach
函数中返回了。
注意: 只有在以 lambda
作为参数的函数是内联的才能从更外层的函数返回,否则是不允许使用 return
语句的。这很好理解,内联的函数会把代码拷贝到被调用的位置,return
就能直接在外层的函数中返回了。而非内联的函数的 lambda
可能会被保存到变量中,以便在函数返回后可以继承使用,return
语句就没有意义了,所以不允许添加 。
从 lambda 返回:使用标签返回
也可以在 lambda
表达式中使用局部返回。lambda
中的局部返回跟 for
循环中的 break
表达式相似。它会终止 lambda
的执行,并接着从 lambda
的代码处执行。要区分局部返回和非局部返回,要用到标签。想从一个 lambda
表达式处返回你可以标记它,然后在 return
关键字后面引用这个标签。
fun lookForAliceForEach() {
val personList = listOf(Person("Alice", 29), Person("fanda", 18))
personList.forEach label@{ // 给 lambda 加上标签
if (it.name == "Alice") return@label //从标签返回
}
// 标签返回之后,会执行这里的代码
println("Alice is not found !")
}
要标记一个 lambda
表达式,在 lambda
的花括号之前放一个标签名(可以是任何标识符),接着放一个 @
符号。要从一个 lambda
返回,在 return
关键字后放一个 @
符号,接着放标签名。
如果你不想写标签,可以使用默认的标签(使用 lambda
作为参数的函数的函数名):
fun lookForAliceForEachNoLable() {
val personList = listOf(Person("Alice", 29), Person("fanda", 18))
personList.forEach {
if (it.name == "Alice") {
//从标签返回,使用 lambda 作为参数的函数的函数名
return@forEach
}
}
// 标签返回之后,会执行这里的代码
println("Alice is not found !")
}
注意: 显式指定了标签,就不能用默认的标签,两者只能用其一,而且标签数量只能是一个。
如果你给带接收者的 lambda
指定标签,就可以通过对应的带有标签的 this
表达式访问它的隐式接收者,比如:
fun testApplyThis() {
println(StringBuffer().apply sb@{ //显式指定标签
listOf(1,2,3).apply {
// 这里引用了外层的 StringBuffer 对象
this@sb.append(this.toString())
}
})
}
和 return
表达式中使用标签一样,可以显式地指定 lambda
表达式的标签,也可以使用函数名作为标签。
匿名函数:默认使用局部返回
局部返回的语法相当冗长,如果一个 lambda
包含多个返回语句会变得笨重,可以用另一种可选的语法来传递代码块:匿名函数。先来看一个例子:
// 匿名函数的方式
fun lookForAliceForEachNoName() {
val personList = listOf(Person("Alice", 29), Person("fanda", 18))
personList.forEach(fun(p) {
if (p.name == "Alice") return
println("${p.name} is not Alice")
})
}
// 标签的方式
fun lookForAliceForEachWithLable() {
val personList = listOf(Person("Alice", 29), Person("fanda", 18))
personList.forEach {
if (it.name == "Alice") return@forEach
println("${it.name} is not Alice")
}
}
使用匿名函数取代 lambda
表达式,所以需要放在括号内。匿名函数看起来跟普通函数相似,除了它的函数名和参数类型被省略之外。我们再看一个例子:
// 代码块函数体表示,要指定返回类型
personList.filter(fun(p): Boolean {
return p.age < 30
})
// 表达式函数体表示,不用显式指定返回类型
personList.filter(fun(p) = p.age < 30)
匿名函数和普通函数有相同的指定返回值类型的规则。
注意:在匿名函数中,不带标签的 return
表达式会从匿名函数返回,而不是从外层函数返回。规则很简单:return
从最近使用 fun
关键字声明的函数返回,因为 lambda
表达式没有使用 fun
,所以 lambda
的 return
从最外层函数返回。
注意:尽管匿名函数看起来跟普通函数很相似,但它其实就是 lambda
表达式的另一种语法形式而已。关于 lambda
表达式如何实现,以及内联函数中如何被内联同样适用于匿名函数。
小结
- 函数类型可以让你声明一个持有函数引用的变量、参数或者函数返回值。
- 高阶函数以其他函数作为参数或者返回值,可以用函数类型作为函数参数或者返回值的类型来创建这样的函数。
- 内联函数被编译后,它的字节码连同传递给它的
lambda
的字符码会被插入到调用函数的代码中,这使得函数调用相比于直接编写相同的代码,不会产生额外的运行时开销。 - 高阶函数能够让组件内的不同部分的代码重用,也可以让你构建功能强大的通用库。
- 内联函数可以让你使用非局部返回。
- 匿名函数给
lambda
表达式提供了另一种可选的语法,用不同的规则来解析return
表达式。可以在需要编写有多个退出点的代码块的时候使用它们。