# Kotlin教程 - 6 函数式编程

什么是函数式编程?

就是用函数来解决问题,函数可以赋值给变量、函数可以作为参数传递给另外一个函数、函数可以作为返回值。

# 6.1 函数作为参数

# 1 函数赋值给变量

函数可以赋值给变量,看一下下面的代码:

fun compute(x: Int, y: Int): Int {
    // 定义一个函数
    return x + y
}

fun main() {
    val func = ::compute                // 将函数赋值给变量,注意函数名称后面没有括号
    val result = func(1, 2)       // 通过变量调用函数
    print(result)
}
1
2
3
4
5
6
7
8
9
10

将函数赋值给变量,函数名签名加 :: ,函数后面没有括号,有括号就是调用函数了。

函数赋值给变量后,可以通过变量调用函数。

那么函数是什么类型呢?

函数的类型是通过参数和返回值确定的,我们将上面的代码显式的写上类型。

fun compute(x: Int, y: Int): Int {
    // 定义一个函数
    return x + y
}

fun main() {
    val func: (Int, Int) -> Int = ::compute                // 将函数赋值给变量,注意函数名称后面没有括号
    val result = func(1, 2)       // 通过变量调用函数
    print(result)
}
1
2
3
4
5
6
7
8
9
10

上面 (Int, Int) -> Int 表示函数的类型,写上类型后,func 变量只能被赋值参数是两个 Int,返回值是 Int 的函数了。

# 2 匿名函数

匿名函数是一种没有显式命名的函数。一般定义完匿名函数会将函数赋值给一个变量。

举个栗子:

fun main() {
    // 定义一个匿名函数并赋值给变量sum
    val sum = fun(x: Int, y: Int): Int {
        return x + y
    }

    val result = sum(3, 4)  // 通过变量调用匿名函数
    println("Sum: $result")
}
1
2
3
4
5
6
7
8
9

上面定义了一个匿名函数,然后将函数赋值给变量 sum ,并通过变量来调用函数。

# 3 Lambda表达式

Lambda 表达式是一种轻量级的匿名函数,它允许你以更简洁的方式定义和传递功能。Lambda 表达式的一般语法如下:

{ 参数列表 -> 函数体 }
1

Lambda 表达式的关键部分是箭头 ->,它将参数列表和函数体分开,箭头前面是参数,后面是函数体。

lambda表达式的方法体不需要return语句,会自动返回函数体的最后一行语句的结果。

举个栗子:

fun main() {
    // 使用Lambda定义匿名函数
    val sum:(x: Int, y: Int) -> Int = {x, y ->
        println("${x} + ${y}")
        x + y									// 会自动返回最后一行语句的结果
    }

    val result = sum(3, 4)  // 通过变量调用匿名函数
    println("Sum: $result")
}
1
2
3
4
5
6
7
8
9
10

(x: Int, y: Int) -> Int 是函数的类型,参数的类型和返回值类型是放在类型中定义的。

= 号后面是 Lambda 表达式,参数名是放在函数中的。

如果匿名函数没有参数,该如何书写呢,如下:

fun main() {
    val getCount: () -> Int = {
        println("Hello, World!")
        42				// 会自动返回最后一行语句的结果
    }

    val count = getCount() // 通过变量调用匿名函数
    println(count)
}
1
2
3
4
5
6
7
8
9

如果匿名函数没有参数,Lambda 表达式则不需要 -> ,和代码块很像。

# 4 it 关键字

如果匿名函数只有一个参数,可以使用 it 关键字来表示参数名。如果参数是多个, it 关键字就不能用了。

举个栗子:

fun main() {
    val sayHello: (String) -> String = {		// 函数有一个参数
        "Hello, ${it}"
    }

    val content = sayHello("Doubi") // 通过变量调用匿名函数
    println(content)
}
1
2
3
4
5
6
7
8

# 5 函数类型推断

匿名函数的类型也支持类型推断,推断出参数类型和返回值类型。

首先,没有参数的匿名函数:

fun main() {
    // 没有参数的匿名函数
    val sayHello = {
        val name = "Doubi"
        "Hello, ${name}!"
    }

    val content = sayHello() // 通过变量调用匿名函数
    println(content)
}
1
2
3
4
5
6
7
8
9
10

在上面的代码中,定义了一个匿名函数赋值给 sayHello 变量,这里匿名函数没有参数,{} 中有点像代码块。

有参数的匿名函数的类型推断,当匿名函数有参数的时候,必须为参数指定参数类型。

fun main() {
    // 没有参数的匿名函数
    val sayHello = { name: String, age: Int ->
        "Hello, ${name}, I'm ${age} years!"
    }

    val content = sayHello("Doubi", 5) // 通过变量调用匿名函数
    println(content)
}
1
2
3
4
5
6
7
8
9

有参数的时候,需要写明参数的类型。

# 6 函数作为参数

函数可以作为参数传递给另外一个函数。

fun plus(x: Int, y: Int): Int {
    // 定义一个加法的函数
    return x + y
}

fun multiply(x: Int, y: Int): Int {
    // 定义一个乘法的函数
    return x * y
}

fun calculate(x: Int, y: Int, func: (Int, Int) -> Int): Int {
    // 第三个参数是一个函数,在该函数中调用了这个函数
    return func(x, y)
}

fun main() {
    val result1 = calculate(1, 2, ::plus) // 传递三个参数,第三个参数是一个函数,使用第三个参数的函数来计算前两个参数
    println(result1)

    val result2 = calculate(1, 2, ::multiply)
    println(result2)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

在传递函数的时候,需要使用 ::函数名,表示的是函数的引用。

在上面的代码中,我们将两个数字和一个函数作为参数传递给了 calculate 函数,在 calculate 函数中,将两个数字作为参数,调用了作为参数的函数。

这样根据传入的函数的不同,实现了不同的逻辑,这是计算逻辑的传递,而不是数据的传递。

也可以使用 Lambda 表达式进行书写:

fun calculate(x: Int, y: Int, func: (Int, Int) -> Int): Int {
    // 第三个参数是一个函数,在该函数中调用了这个函数
    return func(x, y)
}

fun main() {
    val result1 = calculate(1, 2, {x: Int, y: Int -> x + y}) // 传递Lambda表达式
    println(result1)

    val result2 = calculate(1, 2, {x: Int, y: Int -> x * y})
    println(result2)
}
1
2
3
4
5
6
7
8
9
10
11
12

是不是简单很多。

# 7 函数作为参数的简写

如果一个函数的 Lambda 参数是最后一个参数,或者是唯一的参数,那么括住 Lambda 值的一对圆括号可以省略。

什么意思,举个例子:

fun calculate(func: (Int, Int) -> Int): Int {			// Lamabda匿名函数是唯一的参数
    // 调用参数
    return func(1, 2)
}

fun main() {
    val result1 = calculate{x: Int, y: Int -> x + y}		// 可以省略圆括号
    println(result1)
}
1
2
3
4
5
6
7
8
9

函数参数只有一个 Lambda匿名函数,调用的时候,可以省略 () ,直接跟 Lambda 表达式的 {}简直变态!!!

如果有多个参数,Lambda 参数是最后一个参数,可以写成如下:

fun calculate(x: Int, y: Int, func: (Int, Int) -> Int): Int {
    // 调用函数
    return func(x, y)
}

fun main() {
    val result = calculate(1, 2){x: Int, y: Int -> x + y}
    println(result)
}
1
2
3
4
5
6
7
8
9

在传递 Lambda 匿名函数的时候,可以将 Lambda 匿名函数放在函数参数 () 的后面。着实变态!!!

# 8 内联函数

在 JVM 上,Lambda表达式其实是通过生成一个匿名类实现,所以如果Lambda用在类似callback、方法参数较为合适,因为反正都需要生成一个匿名类,所以性能是差不多的。但是如果仅仅用作匿名函数,有点大材小用,因为不仅增加一个类而且还需要在使用的时候创建它的一个单例对象来调用。这样的实现方式与普通函数相比性能上就差了好多。

对于这种情况 Kotlin 提供了内联函数来解决性能上的影响,内联函数将函数的代码直接嵌入到调用它的地方,而不是在单独的函数定义中执行。因而避免的变量的内存分配,哪里需要使用Lambda,编译器就会将函数体复制粘贴到哪里。内联函数的目的是减少函数调用的开销,提高代码的执行效率。

需要注意:使用 Lambda的递归函数无法内联,因为会导致复制粘贴无限循环,编译会经过。

举个栗子:

inline fun calculate(x: Int, y: Int, func: (Int, Int) -> Int): Int {		// 定义内联函数
    // 调用函数
    return func(x, y)
}

fun main() {
    val result = calculate(1, 2) {x: Int, y: Int -> x + y}
    println(result)
}
1
2
3
4
5
6
7
8
9

内联函数的语法与普通函数的区别只是在函数的加个 inline 的标记,用于标记此函数数内联函数。

inline 修饰的函数使函数本身及其参数中的lambda参数都内联到使用处。

通过查看字节码,将字节码反编译成 Java 代码:

public static final void main() {
    byte x$iv = 1;
    int y$iv = 2;
    int $i$f$calculate = false;
    int var6 = false;
    int result = x$iv + y$iv;
    System.out.println(result);
 }
1
2
3
4
5
6
7
8

可以看到 main 函数中并没有调用 calculate 函数,而是将 calculate 函数逻辑赋值到 main 函数中。

在内联函数中,可以在类型是函数的参数前面使用 noinline 进行修饰,表示此参数不需要内联。

举个栗子:

inline fun calculate(x: Int, y: Int, noinline func: (Int, Int) -> Int): Int {
    // 调用函数
    return func(x, y)
}
1
2
3
4

需要注意的是,内联函数并不是在所有情况下都会提高性能。如果函数体非常复杂或者函数被频繁调用,那么内联可能会增加代码大小,反而降低性能。因此,在使用内联函数时需要权衡利弊,根据实际情况进行选择。

另外,Kotlin编译器默认会内联小函数,即函数体只有一行或者没有循环和条件语句的简单函数。对于大函数或者复杂的函数,编译器可能会自动忽略inline关键字。如果你想强制编译器内联某个函数,可以使用@inline注解,但需要确保该函数在编译时是可访问的。

# 6.2 函数作为返回值

# 1 函数作为返回值

函数除了可以作为参数,还可以作为返回值。

举个栗子:

fun returnFunc(): (String, Int) -> String {				// 定义一个函数,方法的返回值类型是一个函数类型
    // 返回函数
    return { name, age ->
        "Hello ${name}, I'm ${age} years old"
    }
}

fun main() {
    val func = returnFunc()         // 方法返回一个函数
    val content = func("Doubi", 5)  // 执行返回的函数
    println(content)
}
1
2
3
4
5
6
7
8
9
10
11
12

首先定义了一个函数,函数的返回值也是一个函数。返回的函数是 Lambda 表达式书写的匿名函数。

再举个栗子,返回一个没有参数,返回值是一个字符串的函数:

fun returnFunc(): () -> String {
    // 返回的是函数
    return { "Hello World!" }				// 这是一个函数
}

fun main() {
    val func = returnFunc()         // 方法返回函数
    val content = func()            // 执行方法
    println(content)
}
1
2
3
4
5
6
7
8
9
10

返回的函数是 Lambda 表达式书写的匿名函数。

# 2 闭包

什么是闭包?

闭包必须满足三个条件:

  • 在一个外部函数中有一个内部函数
  • 内部函数必须引用外部函数中的变量
  • 外部函数的返回值必须是内部函数

举个栗子:

fun outerFunc():() -> Unit {
    val a = 1

    return {        // 将内部函数返回
        print(a)    // 使用了外部函数的变量
    }
}

fun main() {
    var result = outerFunc() // 这里得到的结果是一个函数
    result(); // 执行内部函数,结果为:1
}
1
2
3
4
5
6
7
8
9
10
11
12

在上面外部函数 outerFunc 中返回了内部函数,内部函数中引用了外部函数的变量 a。这样就构成了闭包。

下面来讲解一下闭包的实际应用。

# 3 在函数之间共享数据

使用闭包可以在内部函数和外部函数之间共享数据。

举个栗子:

fun counter():() -> Int {
    var count = 0

    return {
        count += 1
        count
    }
}

fun main() {
    var c1 = counter(); // 创建了一个计数器
    println(c1()); // 输出 1
    println(c1()); // 输出 2

    var c2 = counter(); // 创建了第二个计数器
    println(c2()); // 输出 1
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

在上面的代码中,创建了闭包,用来实现了一个计数器的功能。

当我们调用外部函数 counter 就会创建一个计数器,当我们调用这个计数器的时候,也就是指定匿名内部类的函数的时候,count 的值就会加1,由于我们每次调用counter函数时都会创建一个新的闭包,因此我们可以创建多个计数器,每个计数器都是独立的。

可以看到闭包可以用于封装私有状态,没有使用全局的变量,避免变量暴露在全局作用域中,实现更好的封装和数据隐藏。

执行结果:

1

2

1

其实,我们也可以创建一个计数器的类,在类中定义一个count属性,也可以实现上面的功能,例如:

class Counter {
    var count = 0
    fun increment(): Int {
        count += 1
        return count
    }
}

fun main() {
    // 创建计数器
    val counter1 = Counter()
    val counter2 = Counter()
    println(counter1.increment()) // 输出 1
    println(counter2.increment()) // 输出 1
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

一般情况下,类实现方式更适合需要维护的数据结构较为复杂的情况,比如需要使用多个属性和方法来处理数据。而闭包实现方式更适合只需要共享一些简单数据的情况。

# 4 作为回调函数

回调函数指的是将一个函数作为参数传递给另外一个函数,然后在这个函数中执行这个参数函数。回调函数通常用于异步编程中,可以在事件发生后回调执行,以完成一些特定的任务。例如:在实际的功能实现中,我们经常会做一些耗时的操作,例如网络请求,发起网络请求后,继续执行后面的代码,待服务器返回结果后,在通过回调函数的方式返回结果。

这里举一个回调函数的列子:

fun plus(a: Int, b: Int, callback: (Int) -> Unit) {
    // callback参数就是一个函数
    val result = a + b
    callback(result) // 执行callback函数将结果返回
}

fun main() {
    plus(10, 20 ) { 	// 调用函数,传入一个匿名函数作为参数
        println(it)   // 匿名函数只有一个参数,可以使用it
    }
}
1
2
3
4
5
6
7
8
9
10
11

调用 plus 函数的时候,传入了一个函数,得到计算结果后,通过调用传入的函数,将结果传递给匿名函数的参数。