在Kotlin中,Lambdas无处不在。我们在代码中看到它们。它们在文档和博客文章中被提及。写作、阅读或学习Kotlin的时候,很难不很快碰到lambdas的概念。
但到底什么_是_lambdas?
如果你是该语言的新手,或者没有仔细研究过lambdas本身,那么这个概念有时可能会让人困惑。
在这篇文章中,我们将深入研究Kotlin的lambdas。我们将探讨它们是什么,它们的结构是怎样的,以及它们可以在哪里使用。在本篇文章结束时,你应该对Kotlin中的lambda有一个完整的理解,以及如何在任何类型的Kotlin开发中务实地使用它们。
什么是Kotlin lambda?
让我们从正式的定义开始。
lambdas是一种_函数字面_,意味着它们是一种没有使用fun 关键字的函数定义,并立即作为表达式的一部分使用。
因为lambdas没有使用fun 关键字来命名或声明,所以我们可以自由地将它们分配给变量或作为函数参数传递。
Kotlin中lambdas的例子
让我们看一下几个例子来帮助说明这个定义。下面的片段演示了在变量赋值表达式中使用两个不同的lambdas。
val lambda1 = { println("Hello Lambdas") }
val lambda2 : (String) -> Unit = { name: String ->
println("My name is $name")
}
复制代码
在这两种情况下,等号右边的所有东西都是lambda。
让我们看看另一个例子。这个片段演示了使用lambda作为函数参数的情况。
// create a filtered list of even values
val vals = listOf(1, 2, 3, 4, 5, 6).filter { num ->
num.mod(2) == 0
}
复制代码
在这种情况下,调用.filter 后的所有内容都是lambda。
有时,lambdas可能会让人感到困惑,因为它们可以用不同的方式来写和使用,使人难以理解某个东西是否是lambda。这方面的一个例子可以在下一个片段中看到。
val vals = listOf(1, 2, 3, 4, 5, 6).filter({ it.mod(2) == 0 })
复制代码
这个例子显示了前一个例子的另一个版本。在这两种情况下,一个lambda被传递给filter() 函数。我们将在这篇文章中讨论这些差异背后的原因。
Kotlin lambda不是什么
现在我们已经看到了一些关于lambdas_是_什么的例子,指出一些关于lambdas_不是_什么的例子可能会有所帮助。
lambdas不是类或函数体。看一下下面的类定义。
class Person(val firstName: String, val lastName: String) {
private val fullName = "$firstName $lastName"
fun printFullName() {
println(fullName)
}
}
复制代码
在这段代码中,有两组大括号,看起来非常像lambdas。类的主体包含在一组{ } ,而printFullName() 方法的实现包含在一组{ } 中的方法主体。
虽然这些看起来像lambdas,但它们并不是。我们将在接下来的讨论中详细解释,但基本的解释是,这些实例中的大括号并不代表函数表达式;它们只是语言基本语法的一部分。
下面是最后一个例子,说明lambda不是什么。
val greeting = if(name.isNullOrBlank()) {
"Hello you!"
} else {
"Hello $name"
}
复制代码
在这个片段中,我们又一次有两组大括号。但是,条件语句的主体并不代表一个函数,所以它们不是lambdas。
现在我们已经看到了一些例子,让我们仔细看看lambda的正式语法。
了解基本的lambda语法
我们已经看到,lambdas可以用几种不同的方式来表达。然而,所有的lambdas都遵循一套特定的规则,作为Kotlin的lambda表达式语法的一部分进行详细说明。
该语法包括以下规则。
- lambdas总是被大括号所包围
- 如果lambda的返回类型不是
Unit,那么lambda主体的最终表达式将被视为返回值。 - 参数声明放在大括号内,可以有可选的类型注释
- 如果有一个参数,它可以在lambda主体中使用隐式
it。 - 参数声明和lambda主体必须用a来分隔。
->
虽然这些规则概述了如何编写和使用lambda,但如果没有例子,它们本身就会令人困惑。让我们看看一些说明这种λ表达式语法的代码。
声明简单的lambdas
我们可以定义的最简单的lambda是这样的。
val simpleLambda : () -> Unit = { println("Hello") }
复制代码
在这种情况下,simpleLambda 是一个不需要参数的函数,并返回Unit 。因为没有参数类型需要声明,而且返回值可以从lambda主体中推断出来,我们可以进一步简化这个lambda。
val simpleLambda = { println("Hello") }
复制代码
现在我们依靠Kotlin的类型推理引擎来推断:simpleLambda 是一个不需要参数并返回Unit 的函数。Unit 的返回是通过以下事实推断出来的:λ主体的最后一个表达式,即对println() 的调用,返回Unit 。
声明复杂的lambdas
下面的代码片段定义了一个lambda,它接收两个String 参数并返回一个String 。
val lambda : (String, String) -> String = { first: String, last: String ->
"My name is $first $last"
}
复制代码
这个lambda是粗略的。它包括所有可选的类型信息。_第一个_和_最后一个_参数都包括它们明确的类型信息。该变量也明确地定义了λ所表达的函数的类型信息。
这个例子可以用几种不同的方式来简化。下面的代码显示了两种不同的方式,通过依赖类型推理,可以使lambda的类型信息不那么明确。
val lambda2 = { first: String, last: String ->
"My name is $first $last"
}
val lambda3 : (String, String) -> String = { first, last ->
"My name is $first $last"
}
复制代码
在lambda2 这个例子中,类型信息是从lambda本身推断出来的。参数值被明确地注释为String 类型,而最终表达式可以被推断为返回一个String 。
对于lambda3 ,该变量包括类型信息。正因为如此,lambda的参数声明可以省略显式的类型注释;first 和last 都将被推断为String 类型。
调用lambda表达式
一旦你定义了一个lambda表达式,你如何调用这个函数来实际运行lambda主体中定义的代码?
与Kotlin中的大多数事情一样,我们有多种方法来调用lambda。请看下面的例子。
val lambda = { greeting: String, name: String ->
println("$greeting $name")
}
fun main() {
lambda("Hello", "Kotlin")
lambda.invoke("Hello", "Kotlin")
}
// output
Hello Kotlin
Hello Kotlin
复制代码
在这个片段中,我们定义了一个lambda,它将接受两个Strings ,并打印一个问候语。我们能够以两种方式调用该lambda。
在第一个例子中,我们调用lambda,就像我们调用一个命名的函数一样。我们在变量name 上加上括号,并传递适当的参数。
在第二个例子中,我们使用了一个函数类型可用的特殊方法invoke() 。
在这两种情况下,我们都得到了相同的输出。虽然你可以使用任何一个选项来调用你的lambda,但直接调用lambda而不使用invoke() ,会导致更少的代码,并更清楚地传达了调用定义函数的语义。
从lambda返回值
在上一节中,我们简要地谈到了从lambda表达式中返回值。我们证明了λ的返回值是由λ主体中的最后一个表达式提供的。无论是返回一个有意义的值还是返回Unit ,都是如此。
但是如果你想在lambda表达式中拥有多个返回语句呢?在编写普通的函数或方法时,这种情况并不少见;lambdas是否也支持这种多返回的概念?
是的,但它并不像在lambda中添加多个返回语句那样简单明了。
让我们来看看我们可能期望在lambda表达式中实现的明显的多重返回。
val lambda = { greeting: String, name: String ->
if(greeting.length < 3) return // error: return not allowed here
println("$greeting $name")
}
复制代码
在一个普通的函数中,如果我们想提前返回,我们可以添加一个return ,在函数运行到完成之前返回。然而,在λ表达式中,以这种方式添加一个return ,会导致编译器错误。
为了达到预期的结果,我们必须使用所谓的限定返回。在下面的片段中,我们更新了之前的例子,以利用这个概念。
val lambda = greet@ { greeting: String, name: String ->
if(greeting.length < 3) return@greet
println("$greeting $name")
}
复制代码
这段代码有两个关键变化。首先,我们通过在第一个大括号前添加greet@ ,来标记我们的lambda。第二,我们现在可以引用这个标签,并使用它来从我们的lambda返回到外部的调用函数。现在,如果greeting < 3 是true ,我们将提前从我们的 lambda 返回,并且不会打印任何东西。
你可能已经注意到,这个例子没有返回任何有意义的值。如果我们想返回一个String 而不是打印一个String 呢?这个限定返回的概念还适用吗?
同样,答案是肯定的。在制作我们的标签return ,我们可以提供一个明确的返回值。
val lambda = greet@ { greeting: String, name: String ->
if(greeting.length < 3) return@greet ""
"$greeting $name"
}
复制代码
如果我们需要有两个以上的返回,同样的概念也可以应用。
val lambda = greet@ { greeting: String, name: String ->
if(greeting.length < 3) return@greet ""
if(greeting.length < 6) return@greet "Welcome!"
"$greeting $name"
}
复制代码
请注意,虽然我们现在有多个return 语句,但我们仍然没有为我们的最终值使用一个明确的return 。这一点很重要。如果我们在lambda表达式主体的最后一行添加一个return ,我们会得到一个编译器错误。最后的返回值必须总是隐式返回。
与lambda参数一起工作
我们现在已经看到了在lambda表达式中使用参数的许多用法。编写lambdas的大部分灵活性来自于与参数一起工作的规则。
声明lambda参数
让我们从简单的情况开始。如果我们不需要向我们的lambda传递任何东西,那么我们就不需要为lambda定义任何参数,就像下面这段话一样。
val lambda = { println("Hello") }
复制代码
现在,让我们假设我们想给这个lambda传递一个问候语。我们将需要定义一个单一的String 参数。
val lambda = { greeting: String -> println("Hello") }
复制代码
注意,我们的lambda已经在几个方面发生了变化。我们现在在大括号内定义了一个greeting 参数,并且用一个-> 操作符来分隔参数声明和lambda的主体。
因为我们的变量包括参数的类型信息,我们的λ表达式可以简化。
val lambda: (String) -> Unit = { greeting -> println("Hello") }
复制代码
lambda中的greeting 参数不需要指定String 的类型,因为它是由变量赋值的左边推断出来的。
你可能已经注意到,我们根本就没有使用这个greeting 参数。这种情况有时会发生。我们可能需要定义一个接收参数的lambda,但是因为我们不使用它,所以我们想直接忽略它,这样可以节省我们的代码,并从我们的心理模型中去除一些复杂性。
为了忽略或隐藏未使用的greeting 参数,我们可以做几件事。在这里,我们通过完全删除它来隐藏它。
val lambda: (String) -> Unit = { println("Hello") }
复制代码
现在,尽管lambda本身没有声明或命名这个参数,但并不意味着它不是函数签名的一部分。要调用lambda ,我们仍然需要向函数传递一个String 。
fun main() {
lambda("Hello")
}
复制代码
如果我们想忽略这个参数,但仍然包括它,以便更清楚地知道有信息被传递给lambda调用,我们有另一个选择。我们可以用下划线来替换未使用的lambda参数的名称。
val lambda: (String) -> Unit = { _ -> println("Hello") }
复制代码
虽然这在用于一个简单的参数时看起来有点奇怪,但当有多个参数需要考虑时,这可能是相当有帮助的。
访问lambda参数
我们如何访问和使用传递给lambda调用的参数值?让我们回到我们先前的一个例子。
val lambda: (String) -> Unit = { println("Hello") }
复制代码
我们如何更新我们的lambda以使用将被传递给它的String ?为了达到这个目的,我们可以声明一个名为String 的参数并直接使用它。
val lambda: (String) -> Unit = { greeting -> println(greeting) }
复制代码
现在,我们的lambda将打印任何传递给它的东西。
fun main() {
lambda("Hello")
lambda("Welcome!")
lambda("Greetings")
}
复制代码
虽然这个lambda非常容易阅读,但它可能比一些人想写的更冗长。因为这个lambda只有一个参数,而且这个参数的类型可以被推断出来,所以我们可以用it 这个名字来引用传入的String 值。
val lambda: (String) -> Unit = { println(it) }
复制代码
你很可能见过Kotlin代码引用一些没有明确声明的it 参数。这是Kotlin的常见做法。当参数值代表什么非常清楚时,请使用it 。在很多情况下,即使使用隐含的it ,代码也比较少,但最好还是给lambda参数命名,这样阅读代码的人更容易理解。
处理多个lambda参数
到目前为止,我们的例子都是使用单个参数值传递给lambda的。但如果我们有多个参数呢?
值得庆幸的是,大部分相同的规则仍然适用。让我们更新我们的例子,同时接受一个greeting 和一个thingToGreet 。
val lambda: (String, String) -> Unit = { greeting, thingToGreet ->
println("$greeting $thingToGreet")
}
复制代码
我们可以命名这两个参数,并在lambda中访问它们,就像使用单个参数一样。
如果我们想忽略一个或两个参数,我们必须依靠下划线命名惯例。对于多参数,我们不能省略参数的声明。
val lambda: (String, String) -> Unit = { _, _ ->
println("Hello there!")
}
复制代码
如果我们只想忽略其中一个参数,我们可以自由地混合和匹配带有下划线的命名惯例的参数。
val lambda: (String, String) -> Unit = { _, thingToGreet ->
println("Hello $thingToGreet")
}
复制代码
用lambda参数进行结构重组
解构让我们把一个对象分解成代表原始对象的数据片段的单个变量。Map 这在某些情况下是非常有用的,比如从一个key 和value 条目中提取。
在lambdas中,当我们的参数类型支持时,我们可以利用重构。
val lambda: (Pair<String, Int>) -> Unit = { pair ->
println("key:${pair.first} - value:${pair.second}")
}
fun main() {
lambda("id123" to 5)
}
// output
// key:id123 - value:5
复制代码
我们将一个Pair<String, Int> 作为参数传递给我们的lambda,在这个lambda中,我们必须首先通过引用Pair 来访问这对属性的first 和second 。
通过重构,我们可以定义两个参数:一个是first 属性,一个是second 属性,而不是声明一个参数来代表传递的Pair<String, Int> 。
val lambda: (Pair<String, Int>) -> Unit = { (key, value) ->
println("key:$key - value:$value")
}
fun main() {
lambda("id123" to 5)
}
// output
// key:id123 - value:5
复制代码
这样我们就可以直接访问key 和value ,这样可以节省代码,也可以减少一些心理上的复杂性。当我们关心的是底层数据时,不需要引用包含的对象就少了一件需要考虑的事情。
关于解构的更多规则,无论是变量还是lambdas,请查看官方文档。
访问闭包数据
我们现在已经看到了如何处理直接传递给lambdas的值。然而,lambda也可以从其定义之外访问数据。
lambdas可以从其范围之外访问数据和函数。这些来自外部范围的信息就是lambda的_闭包_。lambda可以调用函数,更新变量,并以它需要的方式使用这些信息。
在下面的例子中,lambda访问了一个顶层属性currentStudentName 。
var currentStudentName: String? = null
val lambda = {
val nameToPrint = currentStudentName ?: "Our Favorite Student"
println("Welcome $nameToPrint")
}
fun main() {
lambda() // output: Welcome Our Favorite Student
currentStudentName = "Nate"
lambda() // output: Welcome Nate
}
复制代码
在这种情况下,对lambda() 的两次调用会产生不同的输出。这是因为每次调用都会使用currentStudentName 的当前值。
将lambdas作为函数参数传递
到目前为止,我们一直在把lambdas分配给变量,然后直接调用这些函数。但是如果我们需要把我们的lambda作为另一个函数的参数来传递呢?
在下面的例子中,我们定义了一个名为processLangauges 的高阶函数。
fun processLanguages(languages: List<String>, action: (String) -> Unit) {
languages.forEach(action)
}
fun main() {
val languages = listOf("Kotlin", "Java", "Swift", "Dart", "Rust")
val action = { language: String -> println("Hello $language") }
processLanguages(languages, action)
}
复制代码
processLanguages 函数接收一个List<String> ,同时也接收一个函数参数,该参数本身接收一个String ,并返回Unit 。
我们给我们的action 变量分配了一个lambda,然后在调用processLanguages 时将action 作为一个参数。
这个例子证明了我们可以将一个存储有lambda的变量传递给另一个函数。
但是如果我们不想先赋值变量呢?我们可以直接将一个lambda传递给另一个函数吗?可以,而且这也是常见的做法。
下面的代码段更新了我们之前的例子,将lambda直接传递给processLanguages 函数。
fun processLanguages(languages: List<String>, action: (String) -> Unit) {
languages.forEach(action)
}
fun main() {
val languages = listOf("Kotlin", "Java", "Swift", "Dart", "Rust")
processLanguages(languages, { language: String -> println("Hello $language") })
}
复制代码
你会看到,我们不再有action 这个变量。我们是在将lambda作为参数传递给函数调用的地方定义它的。
现在,这有一个问题。对processLanguages 的调用结果很难阅读。在函数调用的括号内定义一个lambda,对于我们的大脑来说,在阅读代码时需要解析大量的句法噪音。
为了帮助解决这个问题,Kotlin支持一种特殊的语法,被称为尾随羔羊法语法。这种语法规定,如果一个函数的最终参数是另一个函数,那么λ可以_在_函数调用的括号_外_传递。
这在实践中是什么样子的呢?这里有一个例子。
fun main() {
val languages = listOf("Kotlin", "Java", "Swift", "Dart", "Rust")
processLanguages(languages) { language ->
println("Hello $language")
}
}
复制代码
请注意,对processLanguages 的调用现在只有一个值传到了括号里,但现在在这些括号后面直接有一个lambda。
在Kotlin标准库中,这种尾随的lambda语法的使用极为普遍。
看一下下面的例子。
fun main() {
val languages = listOf("Kotlin", "Java", "Swift", "Dart", "Rust")
languages.forEach { println(it) }
languages
.filter { it.startsWith("K")}
.map { it.capitalize() }
.forEach { println(it) }
}
复制代码
对forEach,map,和 filter 的每一个调用都是利用了这种后置的lambda语法,使我们能够在括号外传递lambda。
如果没有这种语法,这个例子看起来会更像这样。
fun main() {
val languages = listOf("Kotlin", "Java", "Swift", "Dart", "Rust")
languages.forEach({ println(it) })
languages
.filter({ it.startsWith("K")})
.map({ it.capitalize() })
.forEach({ println(it) })
}
复制代码
虽然这段代码在功能上与前面的例子相同,但随着小括号和大括号的增加,它开始显得更加复杂。因此,作为一般规则,在函数的括号外将lambdas传递给该函数,可以提高Kotlin代码的可读性。
在Kotlin中使用lambdas进行SAM转换
我们一直在探索lambdas作为在Kotlin中表达功能类型的一种手段。我们可以利用lambdas的另一种方式是在执行Single Access Method(或SAM)转换时。
什么是SAM转换?
如果你需要提供一个具有单一抽象方法的接口实例,SAM转换让我们使用一个lambda来表示该接口,而不是必须实例化一个新的类实例来实现该接口。
考虑一下下面的情况。
interface Greeter {
fun greet(item: String)
}
fun greetLanguages(languages: List<String>, greeter: Greeter) {
languages.forEach { greeter.greet(it) }
}
fun main() {
val languages = listOf("Kotlin", "Java", "Swift", "Dart", "Rust")
greetLanguages(languages, object : Greeter {
override fun greet(item: String) {
println("Hello $item")
}
})
}
复制代码
greetLanguages 函数需要一个Greeter 接口的实例。为了满足需求,我们创建一个匿名类来实现Greeter ,并定义我们的greet 行为。
这样做很好,但它有一些缺点。它要求我们声明和实例化一个新的类。语法很冗长,使得我们很难跟踪函数的调用。
通过SAM转换,我们可以简化这个问题。
fun interface Greeter {
fun greet(item: String)
}
fun greetLanguages(languages: List<String>, greeter: Greeter) {
languages.forEach { greeter.greet(it) }
}
fun main() {
val languages = listOf("Kotlin", "Java", "Swift", "Dart", "Rust")
greetLanguages(languages) { println("Hello $it") }
}
复制代码
请注意,现在对greetLanguages 的调用更容易阅读。这里没有冗长的语法,也没有匿名类。这里的lambda现在正在进行SAM转换,以表示Greeter 的类型。
也注意到对Greeter 接口的改变。我们在接口中加入了fun 关键字。这标志着该接口是一个功能接口,如果你试图增加一个以上的公共抽象方法,编译器将给出一个错误。这就是让这些函数式接口轻松实现SAM转换的法宝。
如果你要创建一个只有一个公共抽象方法的接口,可以考虑把它变成一个函数式接口,这样你在处理这个类型的时候就可以利用lambdas了。
总结
希望这些例子能够帮助我们了解什么是lambdas,如何定义它们,以及如何使用它们来使你的Kotlin代码更具表现力和可理解性。
The postA complete guide to Kotlin lambda expressionsappeared first onLogRocket Blog.


















![[02/27][官改] Simplicity@MIX2 ROM更新-一一网](https://www.proyy.com/wp-content/uploads/2020/02/3168457341.jpg)



![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)