函数式编程的魔法武器:Kotlin高阶函数和Lambda表达式

Kotlin作为一门函数式编程语言,强调函数的重要性,高阶函数和Lambda表达式则是Kotlin中的独门武器。本文旨在介绍Kotlin高阶函数、Lambda表达式的语法,以及它们在函数式编程中的应用,帮助读者更好地理解和运用函数式编程思想。

高阶函数

定义:

高阶函数是指可以接收其他函数作为参数,或者返回一个函数的函数。在许多编程语言中,高阶函数都是一种重要的编程方式,因为它们提供了更高的抽象层次,使得代码更加模块化、易于理解和维护。

下面是一个简单的例子,展示了如何在 Kotlin 中定义和使用高阶函数:

fun doOperation(x: Int, y: Int, operation: (Int, Int) -> Int): Int {
    return operation(x, y)
}

val result = doOperation(10, 5) { x, y -> x + y }
println(result) // 输出 15
复制代码

定义一个 doOperation 函数,它接受两个整数 xy,以及一个函数 operation。这个函数接受两个整数参数,并返回一个整数。我们可以将一个 Lambda 表达式作为 operation 参数传递给 doOperation 函数。

在这个例子中,将一个 Lambda 表达式 { x, y -> x + y } 传递给了 doOperation 函数,这个 Lambda 表达式表示将两个整数相加的操作。最后,doOperation 函数返回了相加后的结果 15,并打印了这个结果。

除了像上面例子中展示的那样,使用 Lambda 表达式作为函数参数外,Kotlin 还支持使用函数引用(function reference)来传递函数。例如,我们可以使用 :: 运算符引用一个已经定义好的函数,然后将它作为高阶函数的参数传递。例如:

fun add(x: Int, y: Int) = x + y

val result = doOperation(10, 5, ::add)
println(result) // 输出 15
复制代码

定义了一个名为 add 的函数,它接受两个整数参数,并返回它们的和。使用 :: 运算符引用这个函数,然后将它作为 doOperation 函数的参数传递。doOperation 函数将调用 add 函数,并返回相加后的结果 15

优势

  1. 代码重用

高阶函数可以接收其他函数作为参数,这样可以让代码变得更加灵活和可重用。例如,我们可以编写一个通用的排序函数,让它接收一个比较函数作为参数,这样就可以在不同的场景中使用它进行排序,而不需要重新编写排序算法的核心部分。

  1. 更好的抽象

高阶函数可以帮助我们更好地抽象出程序的核心逻辑,从而让代码更加清晰和易于理解。例如,我们可以使用高阶函数来定义一些常用的操作,比如对列表进行过滤、映射等操作,这样可以让代码更加简洁和可读。

  1. 可以避免代码重复

高阶函数可以让我们避免在代码中出现重复的逻辑。例如,我们可以将一些常用的代码块封装在一个高阶函数中,然后在需要的地方调用它。这样可以减少代码冗余,提高代码的可维护性。

  1. 更加灵活

高阶函数可以让我们编写更加灵活的代码。通过使用高阶函数,我们可以将不同的功能组合在一起,从而实现更加复杂的操作。例如,我们可以将一个函数作为另一个函数的参数,这样就可以实现函数的组合和嵌套。

常见的高阶函数

  1. map 函数:将集合中的每个元素都应用一个函数,并返回一个新的集合。
scssCopy code
val numbers = listOf(1, 2, 3, 4, 5)
val squaredNumbers = numbers.map { it * it }
println(squaredNumbers) // 输出 [1, 4, 9, 16, 25]
复制代码
  1. filter 函数:返回一个新的集合,其中包含满足给定条件的所有元素。
scssCopy code
val numbers = listOf(1, 2, 3, 4, 5)
val evenNumbers = numbers.filter { it % 2 == 0 }
println(evenNumbers) // 输出 [2, 4]
复制代码
  1. reduce 函数:将集合中的所有元素合并成一个值,具体的合并方式由指定的函数决定。
scssCopy code
val numbers = listOf(1, 2, 3, 4, 5)
val sum = numbers.reduce { acc, i -> acc + i }
println(sum) // 输出 15
复制代码
  1. fold 函数:与 reduce 函数类似,但是可以指定一个初始值。
scssCopy code
val numbers = listOf(1, 2, 3, 4, 5)
val sum = numbers.fold(0) { acc, i -> acc + i }
println(sum) // 输出 15
复制代码
  1. forEach 函数:对集合中的每个元素执行指定的操作,没有返回值。
scssCopy code
val numbers = listOf(1, 2, 3, 4, 5)
numbers.forEach { println(it) } // 依次输出 1、2、3、4、5
复制代码
  1. groupBy 函数:根据指定的键将集合分组,返回一个 Map 对象。
cssCopy code
data class Person(val name: String, val age: Int)
val people = listOf(
    Person("Alice", 20),
    Person("Bob", 22),
    Person("Charlie", 20),
    Person("David", 25)
)
val groupedPeople = people.groupBy { it.age }
println(groupedPeople) // 输出 {20=[Person(name=Alice, age=20), Person(name=Charlie, age=20)], 22=[Person(name=Bob, age=22)], 25=[Person(name=David, age=25)]}
复制代码
  1. flatMap 函数:对集合中的每个元素应用一个函数,并将结果合并成一个新的集合。
javaCopy code
val words = listOf("hello", "world", "kotlin")
val chars = words.flatMap { it.toList() }
println(chars) // 输出 [h, e, l, l, o, w, o, r, l, d, k, o, t, l, i, n]
复制代码
  1. sortedBy 函数:按照指定的排序规则对集合进行排序,并返回一个新的集合。
kotlinCopy code
data class Person(val name: String, val age: Int)
val people = listOf(
    Person("Alice", 20),
    Person("Bob", 22),
    Person("Charlie", 20),
    Person("David", 25)
)
val sortedPeople = people.sortedBy { it.age }
println(sortedPeople) // 输出 [Person(name=Alice, age=20), Person(name=Charlie, age=20), Person(name=Bob, age=22), Person(name=David, age=25)]
复制代码
  1. takeWhile 函数:返回集合中从开始位置开始的连续元素,直到遇到第一个不满足给定条件的元素。
scssCopy code
val numbers = listOf(1, 2, 3, 4, 5)
val result = numbers.takeWhile { it < 3 }
println(result) // 输出 [1, 2]
复制代码
  1. any 函数:判断集合中是否存在满足给定条件的元素,返回一个布尔值。
scssCopy code
val numbers = listOf(1, 2, 3, 4, 5)
val result = numbers.any { it % 2 == 0 }
println(result) // 输出 true
复制代码
  1. let

let 函数接收一个lambda表达式,其返回值为lambda表达式中最后一行的结果,let 函数的参数则被传递到lambda表达式中。该函数主要用于避免空指针异常,简化对可空对象的判空操作。

kotlinCopy code
val str: String? = "Hello World"
str?.let { 
    // str不为null的情况下才会执行下面的语句
    println(it.length) // 输出 11
}
复制代码
  1. also

also 函数和 let 函数很像,但是它返回的是调用对象本身,而不是最后一行的结果。also 函数的参数同样会传递到lambda表达式中。

kotlinCopy code
val list = mutableListOf<Int>()
list.also {
    // 对list进行一些操作,返回的是list本身
    it.add(1)
    it.add(2)
}.also {
    // 进一步操作list,返回的还是list本身
    it.add(3)
}
println(list) // 输出 [1, 2, 3]
复制代码
  1. with

with 函数不是扩展函数,而是一个独立函数,它接收两个参数:一个对象和一个lambda表达式。lambda表达式中的代码可以直接访问该对象的属性和方法,从而省去了在每个语句中重复引用对象的麻烦。

kotlinCopy code
data class Person(val name: String, var age: Int)

val person = Person("Tom", 20)
with(person) {
    age += 1
    println("My name is $name and I'm $age years old.")
} // 输出 "My name is Tom and I'm 21 years old."
复制代码
  1. run

run 函数和 let 函数非常相似,它也是接收一个lambda表达式,返回的是lambda表达式中最后一行的结果,但它和 let 的区别在于它的调用对象可以通过 this 关键字访问,而不是通过lambda表达式的参数。

kotlinCopy code
val str: String = "Hello World"
val result = str.run {
    // 这里可以通过 this 访问 str 对象
    println(this) // 输出 "Hello World"
    length
}
println(result) // 输出 11
复制代码
  1. apply

apply 函数和 also 函数也很相似,它同样返回调用对象本身,但是它不接受lambda表达式的参数,而是在lambda表达式中直接操作调用对象。

data class Person(var name: String, var age: Int)

val person = Person("Tom", 20)
person.apply {
    name = "Jerry"
    age = 21
}
println(person.name) // 输出 "Jerry"
println(person.age) // 输出 21

复制代码

最常使用的五个高阶函数的使用条件

it REe.png

不得不知道的东西

kotlin 中提到高阶函数总是和以下几个概念或者用法一同出现,当然,你可以不知道这些用法或者概念,因为你可以不知道所以不用,但是你要阅读别人的代码,不能保证别人不用,这也是一些不愿意学习kotlin代码的老兵常说的可读性差,嗯,是可读性差吗?我感觉是X差。

都有什么呢?

  1. Lambda 表达式的语法:介绍 Lambda 表达式的语法,包括 Lambda 表达式的参数、函数体和返回值。
  2. 函数引用的使用:介绍函数引用的使用方法,包括使用 :: 运算符引用已经定义好的函数,并将其作为高阶函数的参数传递。
  3. Kotlin 中常用的函数式编程操作:介绍 Kotlin 中常用的函数式编程操作,包括:映射、过滤、折叠、遍历、分组、扁平化等。
  4. Kotlin 中其他函数式编程相关的特性:介绍 Kotlin 中其他函数式编程相关的特性,包括:局部函数、嵌套函数、尾递归函数、函数类型、函数类型参数等。
  5. Kotlin 中高阶函数的应用场景:介绍 Kotlin 中高阶函数的应用场景,包括:函数式编程、事件驱动编程、依赖注入、反射等。

其实很多都是概念性的东西,没有看过相关概念的时候你已经在使用了,今天就来归类一下你使用的东西是什么。

高阶函数总是和Lambda

Lambda 表达式是 Kotlin 中实现高阶函数的一种方式。高阶函数是指能够接收其他函数作为参数或将函数作为返回值的函数。而 Lambda 表达式则是一种可以被传递和操作的匿名函数。

在 Kotlin 中,Lambda 表达式可以直接作为高阶函数的参数,这使得高阶函数的调用更加简洁和灵活。使用 Lambda 表达式,我们可以避免在调用高阶函数时重复定义一个新的函数。

简单介绍一下kotlin中的Lambda,

在 Kotlin 中,Lambda 表达式的语法是使用花括号 {} 包裹参数列表和代码块,中间用 -> 连接。具体语法如下:

{ 参数列表 -> 代码块 }
复制代码

其中,参数列表可以为空,也可以包含一个或多个参数,每个参数都包含一个参数名和类型,用冒号 : 分隔。代码块可以包含一条或多条语句,如果只有一条语句,则可以省略花括号。Lambda 表达式的返回值类型可以自动推导出来,也可以使用 return 关键字显式指定返回值。

Kotlin Lambda 表达式的示例:

  1. 不带参数的 Lambda 表达式
val sayHello = { println("Hello, world!") }
sayHello()
复制代码
  1. 带一个参数的 Lambda 表达式
val double = { n: Int -> n * 2 }
println(double(5))
复制代码
  1. 带多个参数的 Lambda 表达式
val sum = { a: Int, b: Int -> a + b }
println(sum(3, 5))
复制代码
  1. 带返回值的 Lambda 表达式
val greater = { a: Int, b: Int -> if (a > b) a else b }
println(greater(3, 5))
复制代码
  1. 带类型推导的 Lambda 表达式
val list = listOf(1, 2, 3, 4, 5)
val evenList = list.filter { it % 2 == 0 }
println(evenList)
复制代码

写时稍微注意的地方

  1. 如果 Lambda 表达式的参数列表只有一个参数,可以省略参数名,并用 it 代替参数名。例如:

    val list = listOf(1, 2, 3)
    list.forEach { println(it) }
    复制代码
  2. 如果 Lambda 表达式的参数列表为空,可以用空括号表示。例如:

    val action = { println("Hello") }
    复制代码
  3. 如果 Lambda 表达式的主体只有一条语句,可以省略花括号。例如:

    val sum = { x: Int, y: Int -> x + y }
    复制代码
  4. Lambda 表达式可以有多个语句,这时需要用花括号包裹起来,并用 return 关键字返回结果。例如:

    val max = { x: Int, y: Int ->
        val temp = if (x > y) x else y
        temp
    }
    复制代码
  5. Lambda 表达式可以定义在函数外部,并且可以被命名和赋值。例如:

    val myLambda: (Int) -> Int = { x -> x * x }
    复制代码

函数引用的使用

函数引用是指将一个函数作为参数传递或者将其赋值给一个变量或者属性的方式。函数引用可以使代码更加简洁、易于阅读和维护。

常见的引用方法

  1. 引用顶层函数:
fun main() {
    val numbers = listOf(1, 2, 3, 4, 5)
    val evenNumbers = numbers.filter(::isEven)
    println(evenNumbers)
}

fun isEven(number: Int) = number % 2 == 0
复制代码

使用 :: 操作符引用了 isEven 函数,并将其传递给了 filter 函数。这样做可以使代码更加简洁,不需要定义一个 lambda 表达式来过滤偶数。

  1. 引用对象的成员函数:
data class Person(val name: String, val age: Int)

fun main() {
    val people = listOf(Person("Alice", 29), Person("Bob", 31))
    val names = people.map(Person::name)
    println(names)
}
复制代码

使用 :: 操作符引用了 Person 类的 name 属性,并将其传递给了 map 函数。这样做可以使代码更加简洁,不需要定义一个 lambda 表达式来提取人名。

  1. 引用构造函数:
data class Person(val name: String, val age: Int)

fun main() {
    val createPerson = ::Person
    val person = createPerson("Alice", 29)
    println(person)
}
复制代码

使用 :: 操作符引用了 Person 类的构造函数,并将其赋值给了一个变量。然后我们使用这个变量来创建一个 Person 对象。

Kotlin 中常用的函数式编程操作

Kotlin 作为一门支持函数式编程风格的编程语言,提供了许多常用的函数式编程操作,高阶函数就是其中之一,还有哪些呢?这么问谁都会懵逼,但是你看下,

  • Lambda 表达式 有用过吗
  • map\fold\filter等等有用过吗

其实都是大家使用过的,接下来看一下几个特有名词。

  1. 映射 (map)

映射操作可以将一个集合中的每个元素都应用一个函数,并将结果存储到一个新的集合中。在 Kotlin 中,可以使用 map() 函数来实现这个操作。

val numbers = listOf(1, 2, 3, 4, 5)
val squaredNumbers = numbers.map { it * it }
println(squaredNumbers) // [1, 4, 9, 16, 25]
复制代码
  1. 过滤 (filter)

过滤操作可以根据一个条件从一个集合中选择元素,并将结果存储到一个新的集合中。在 Kotlin 中,可以使用 filter() 函数来实现这个操作。

val numbers = listOf(1, 2, 3, 4, 5)
val evenNumbers = numbers.filter { it % 2 == 0 }
println(evenNumbers) // [2, 4]
复制代码
  1. 折叠 (reduce)

折叠操作可以将一个集合中的元素聚合成一个值。在 Kotlin 中,可以使用 reduce() 函数来实现这个操作。

val numbers = listOf(1, 2, 3, 4, 5)
val sum = numbers.reduce { acc, n -> acc + n }
println(sum) // 15
复制代码
  1. 遍历 (forEach)

遍历操作可以对一个集合中的每个元素都应用一个函数。在 Kotlin 中,可以使用 forEach() 函数来实现这个操作。

kotlinCopy code
val numbers = listOf(1, 2, 3, 4, 5)
numbers.forEach { println(it) }
复制代码
  1. 分组 (groupBy)

分组操作可以根据一个属性将一个集合中的元素分组,并将结果存储到一个 Map 中。在 Kotlin 中,可以使用 groupBy() 函数来实现这个操作。

data class Person(val name: String, val age: Int)

val people = listOf(
    Person("Alice", 20),
    Person("Bob", 25),
    Person("Charlie", 20),
    Person("David", 30)
)

val peopleByAge = people.groupBy { it.age }
println(peopleByAge) // {20=[Person(name=Alice, age=20), Person(name=Charlie, age=20)], 25=[Person(name=Bob, age=25)], 30=[Person(name=David, age=30)]}
复制代码
  1. 扁平化 (flatten)

扁平化操作可以将一个集合中的嵌套集合的元素展开,并将结果存储到一个新的集合中。在 Kotlin 中,可以使用 flatten() 函数来实现这个操作。

val nestedNumbers = listOf(listOf(1, 2), listOf(3, 4), listOf(5, 6))
val flattenedNumbers = nestedNumbers.flatten()
println(flattenedNumbers) // [1, 2, 3, 4, 5, 6]
复制代码

哈哈,你可能会说了,你个糟老头子坏的很,这不是我都用过吗?

第一:我不是糟老头子

第二:名词知道的越多,面试逼格就越高,而逼格的高低直接决定你money的多少

Kotlin 中其他函数式编程相关的特性

局部函数、嵌套函数、尾递归函数、函数类型、函数类型参数等,其中尾递归函数你可以插个眼,等我后面专门做一个分享。

  1. 局部函数(Local Function)是在函数内部定义的函数,只能在该函数内部调用。局部函数的作用是将复杂的函数拆分成多个小函数,使代码更易于理解和维护。
fun main() {
    fun printMessage(message: String) {
        println(message)
    }

    printMessage("Hello World")
}
复制代码
  1. 嵌套函数(Nested Function)是在一个函数内部定义的函数,与局部函数不同的是,嵌套函数可以在其外层函数和其他嵌套函数中调用。
fun main() {
    fun printMessage(message: String) {
        fun addExclamationMark(message: String): String {
            return "$message!"
        }
        println(addExclamationMark(message))
    }

    printMessage("Hello World")
}
复制代码
  1. 尾递归函数(Tail Recursive Function)是指在函数的最后一步调用自身,并且不再进行任何操作。这种函数可以通过编译器的优化来避免栈溢出。
tailrec fun factorial(n: Int, acc: Int = 1): Int {
    return if (n == 0) acc else factorial(n - 1, acc * n)
}
复制代码
  1. 函数类型(Function Type)是指将函数作为一种类型来使用。函数类型由参数类型和返回值类型组成,可以在变量声明、函数参数、返回值等地方使用。
val sum: (Int, Int) -> Int = { a, b -> a + b }
复制代码
  1. 函数类型参数(Function Type Parameter)是指将函数类型作为另一个函数的参数。函数类型参数可以使用lambda表达式或者函数引用来进行传递。
fun <T, R> List<T>.map(transform: (T) -> R): List<R> {
    val result = mutableListOf<R>()
    for (item in this) {
        result.add(transform(item))
    }
    return result
}

val numbers = listOf(1, 2, 3, 4, 5)
val doubledNumbers = numbers.map { it * 2 }
复制代码

Kotlin 中高阶函数的应用场景

  1. 函数式编程(Functional Programming):函数式编程的核心思想是将函数看作是一等公民,可以像其他数据类型一样传递、赋值和返回。高阶函数在函数式编程中被广泛使用,例如 map、filter、reduce 等常用的函数,它们可以将函数作为参数进行传递,实现对集合等数据结构的操作。
  2. 事件驱动编程(Event-driven Programming):事件驱动编程是一种基于事件和回调函数的编程模式。高阶函数可以作为回调函数,在事件触发时被调用。例如在 Android 中,OnClickListener 就是一个高阶函数,用于处理 View 的点击事件。
  3. 依赖注入(Dependency Injection):依赖注入是一种通过将依赖关系从一个对象传递给另一个对象来实现松耦合的编程技术。高阶函数可以用于依赖注入中的回调函数,例如在 Android 中,通过调用 setOnClickListener 方法,将一个函数作为参数传递给 View,实现对 View 的点击事件的处理。
  4. 反射(Reflection):反射是一种在运行时动态获取和操作对象的技术。Kotlin 中的高阶函数可以与反射结合使用,例如通过调用 KClass 的函数 findFunction,可以在运行时获取函数的引用,进而调用该函数。

总结

通过本文,我们深入探讨了Kotlin中高阶函数和Lambda表达式的语法和使用,以及它们在函数式编程中的重要性和应用场景。我们学习了一些常用的函数式编程操作,如map、filter、reduce等,了解了函数式编程的特性。