作用域函数

  Kotlin 标准库包含几个函数,它们的唯一目的是在对象的上下文中执行代码块。当对一个对象调用这样的函数 并提供一个 lambda 表达式时,它会形成一个临时作用域。在此作用域中,可以访问该对象而无需其名称。这些 函数称为作用域函数。共有以下五种:let 、run 、with 、apply 以及 also

  这些函数基本上做了同样的事情:在一个对象上执行一个代码块。不同的是这个对象在块中如何使用,以及整个 表达式的结果是什么

  下面是作用域函数的典型用法

Person("Alice", 20, "Amsterdam").let {
println(it)
it.moveTo("London")
it.incrementAge()
println(it)
}

  如果不使用 let 来写这段代码,就必须引入一个新变量,并在每次使用它时重复其名称

val alice = Person("Alice", 20, "Amsterdam")
println(alice)
alice.moveTo("London")
alice.incrementAge()
println(alice)

  作用域函数没有引入任何新的技术,但是它们可以使你的代码更加简洁易读。

  由于作用域函数的相似性质,为你的案例选择正确的函数可能有点棘手。选择主要取决于你的意图和项目中使 用的一致性。下面我们将详细描述各种作用域函数及其约定用法之间的区别

区别

  由于作用域函数本质上都非常相似,因此了解它们之间的区别很重要。每个作用域函数之间有两个主要区别:

    — 引用上下文对象的方式

    — 返回值

上下文对象:this 还是 it

  在作用域函数的 lambda 表达式里,上下文对象可以不使用其实际名称而是使用一个更简短的引用来访问。每 个作用域函数都使用以下两种方式之一来访问上下文对象:作为 lambda 表达式的接收者( this )或者作为 lambda 表达式的参数( it )。两者都提供了同样的功能,因此我们将针对不同的场景描述两者的优缺点,并提供使用建议

fun main() {
val str = "Hello" // this
str.run {
println("The receiver string length: $length")
//println("The receiver string length: ${this.length}") // 和上句效果相同
} // it
str.let {
println("The receiver string's length is ${it.length}")
}
}

  

  run 、with 以及 apply 通过关键字 this 引用上下文对象。因此,在它们的 lambda 表达式中可以像在普 通的类函数中一样访问上下文对象。在大多数场景,当你访问接收者对象时你可以省略 this,来让你的代码更 简短。相对地,如果省略了 this,就很难区分接收者对象的成员及外部对象或函数。因此,对于主要对对象成员 进行操作(调用其函数或赋值其属性)的 lambda 表达式,建议将上下文对象作为接收者( this )

val adam = Person("Adam").apply {
age = 20 // 和 this.age = 20 或者 adam.age = 20 一样
city = "London"
} println(adam)

  

  反过来,let 及 also 将上下文对象作为 lambda 表达式参数。如果没有指定参数名,对象可以用隐式默认名 称 it 访问。it 比 this 简短,带有 it 的表达式通常更容易阅读。然而,当调用对象函数或属性时,不能像this 这样隐式地访问对象。因此,当上下文对象在作用域中主要用作函数调用中的参数时,使用 it 作为上下 文对象会更好。若在代码块中使用多个变量,则 it 也更好

fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")
}
}
val i = getRandomInt()

  此外,当将上下文对象作为参数传递时,可以为上下文对象指定在作用域内的自定义名称

fun getRandomInt(): Int {
return Random.nextInt(100).also { value ->
writeToLog("getRandomInt() generated value $value")
}
} val i = getRandomInt()

  

返回值

  根据返回结果,作用域函数可以分为以下两类

    — apply 及 also 返回上下文对象。

    — let 、run 及 with 返回 lambda 表达式结果.

  这两个选项使你可以根据在代码中的后续操作来选择适当的函数。

  apply 及 also 的返回值是上下文对象本身。因此,它们可以作为辅助步骤包含在调用链中:你可以继续在同 一个对象上进行链式函数调用

val numberList = mutableListOf<Double>()
numberList.also { println("Populating the list") }
.apply {
add(2.71)
add(3.14)
add(1.0) }
.also {
println("Sorting the list")
}
.sort()

  它们还可以用在返回上下文对象的函数的 return 语句中

fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")
}
}
val i = getRandomInt()

  

  let 、run 及 with 返回 lambda 表达式的结果。所以,在需要使用其结果给一个变量赋值,或者在需要对其结果进行链式操作等情况下,可以使用它们

val numbers = mutableListOf("one", "two", "three")
val countEndsWithE = numbers.run {
add("four")
add("five")
count { it.endsWith("e") }
} println("There are $countEndsWithE elements that end with e.")

  此外,还可以忽略返回值,仅使用作用域函数为变量创建一个临时作用域。

val numbers = mutableListOf("one", "two", "three") 

with(numbers) {
val firstItem = first()
val lastItem = last()
println("First item: $firstItem, last item: $lastItem")
}

  

几个函数

  为了帮助你为你的场景选择合适的作用域函数,我们会详细地描述它们并且提供一些使用建议。从技术⻆度来 说,作用域函数在很多场景里是可以互换的,所以这些示例展示了定义通用使用⻛格的约定用法

let

  上下文对象作为 lambda 表达式的参数( it )来访问。返回值是 lambda 表达式的结果。

  let 可用于在调用链的结果上调用一个或多个函数。例如,以下代码打印对集合的两个操作的结果

val numbers = mutableListOf("one", "two", "three", "four", "five")
val resultList = numbers.map { it.length }.filter { it > 3 }
println(resultList)

  使用 let,可以写成这样

val numbers = mutableListOf("one", "two", "three", "four", "five") 

numbers.map { it.length }.filter { it > 3 }.let {
println(it)
// 如果需要可以调用更多函数
}

  若代码块仅包含以 it 作为参数的单个函数,则可以使用方法引用( :: )代替 lambda 表达式

val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let(::println)

  let 经常用于仅使用非空值执行代码块。如需对非空对象执行操作,可对其使用安全调用操作符 ?. 并调用 let 在 lambda 表达式中执行操作

val str: String? = "Hello"
//processNonNullString(str) // 编译错误:str 可能为空
val length = str?.let {
println("let() called on $it")
processNonNullString(it) // 编译通过:'it' 在 '?.let { }' 中必不为空
it.length
}

  使用 let 的另一种情况是引入作用域受限的局部变量以提高代码的可读性。如需为上下文对象定义一个新变 量,可提供其名称作为 lambda 表达式参数来替默认的 it

val numbers = listOf("one", "two", "three", "four")
val modifiedFirstItem = numbers.first().let { firstItem ->
println("The first item of the list is '$firstItem'")
if (firstItem.length >= 5) firstItem else "!" + firstItem + "!"
}.toUpperCase()
println("First item after modifications: '$modifiedFirstItem'")

  

with

  一个非扩展函数:上下文对象作为参数传递,但是在 lambda 表达式内部,它可以作为接收者( this )使用。返 回值是 lambda 表达式结果

  我们建议使用 with 来调用上下文对象上的函数,而不使用 lambda 表达式结果。在代码中,with 可以理解 为“对于这个对象,执行以下操作

val numbers = mutableListOf("one", "two", "three")

with(numbers) {
println("'with' is called with argument $this")
println("It contains $size elements")
}

  with 的另一个使用场景是引入一个辅助对象,其属性或函数将用于计算一个值

val numbers = mutableListOf("one", "two", "three")

val firstAndLast = with(numbers) {
"The first element is ${first()}," +
" the last element is ${last()}"
} println(firstAndLast)

  

run

  上下文对象 作为接收者( this )来访问。返回值 是 lambda 表达式结果。

  run 和 with 做同样的事情,但是调用方式和 let 一样——作为上下文对象的扩展函数

  当 lambda 表达式同时包含对象初始化和返回值的计算时,run 很有用。

val service = MultiportService("https://example.kotlinlang.org", 80)

val result = service.run {
port = 8080
query(prepareRequest() + " to port $port")
} // 同样的代码如果用 let() 函数来写:
val letResult = service.let {
it.port = 8080
it.query(it.prepareRequest() + " to port ${it.port}")
}

  除了在接收者对象上调用 run 之外,还可以将其用作非扩展函数。非扩展 run 可以使你在需要表达式的地方 执行一个由多个语句组成的块

val hexNumberRegex = run {
val digits = "0-9"
val hexDigits = "A-Fa-f"
val sign = "+-" Regex("[$sign]?[$digits$hexDigits]+")
} for (match in hexNumberRegex.findAll("+1234 -FFFF not-a-number")) {
println(match.value)
}

  

apply

  上下文对象 作为接收者( this )来访问。返回值 是上下文对象本身。

  对于不返回值且主要在接收者(this)对象的成员上运行的代码块使用 apply。apply 的常⻅情况是对象配置。这样的调用可以理解为“将以下赋值操作应用于对象

val adam = Person("Adam").apply {
age = 32
city = "London"
} println(adam)

  将接收者作为返回值,你可以轻松地将 apply 包含到调用链中以进行更复杂的处理

also

  上下文对象作为 lambda 表达式的参数( it )来访问。返回值是上下文对象本身

  also 对于执行一些将上下文对象作为参数的操作很有用。对于需要引用对象而不是其属性与函数的操作,或者不想屏蔽来自外部作用域的 this 引用时,请使用 also

  当你在代码中看到 also 时,可以将其理解为“并且用该对象执行以下操作”。

val numbers = mutableListOf("one", "two", "three")
numbers
.also { println("The list elements before adding new one: $it") }
.add("four")

  

函数选择

  为了帮助你选择合适的作用域函数,我们提供了它们之间的主要区别表

函数 对象引用 返回值 是否是扩展函数
let it Lambda表达式结果
run this Lambda表达式结果
run - Lambda表达式结果 不是:调用无需上下文对象
with this Lambda表达式结果 不是:把上下文对象当作参数
apply this 上下文对象
also it 上下文对象

  以下是根据预期目的选择作用域函数的简短指南:

    — 对一个非空(non-null)对象执行lambda表达式:let — 将表达式作为变量引入为局部作用域中:let

    — 对象配置:apply

    — 对象配置并且计算结果:run

    — 在需要表达式的地方运行语句:非扩展的 run
— 附加效果:also

    — 一个对象的一组函数调用:with

  不同函数的使用场景存在重叠,你可以根据项目或团队中使用的特定约定选择函数

  尽管作用域函数是使代码更简洁的一种方法,但请避免过度使用它们:这会降低代码的可读性并可能导致错误。 避免嵌套作用域函数,同时链式调用它们时要小心:此时很容易对当前上下文对象及 this 或 it 的值感到困 惑。

takeIf 与 takeUnless

  除了作用域函数外,标准库还包含函数 takeIf 及 takeUnless 。这俩函数使你可以将对象状态检查嵌入到调用链中。

  当以提供的谓词在对象上进行调用时,若该对象与谓词匹配,则 takeIf 返回此对象。否则返回 null 。因 此,takeIf 是单个对象的过滤函数。反之,takeUnless 如果不匹配谓词,则返回对象,如果匹配则返回null 。该对象作为 lambda 表达式参数( it )来访问。

val number = Random.nextInt(100)

val evenOrNull = number.takeIf { it % 2 == 0 }
val oddOrNull = number.takeUnless { it % 2 == 0 }
println("even: $evenOrNull, odd: $oddOrNull")

  当在 takeIf 及 takeUnless 之后链式调用其他函数,不要忘记执行空检查或安全调用(?.),因为他们的 返回值是可为空的

val str = "Hello"
val caps = str.takeIf { it.isNotEmpty() }?.toUpperCase()
//val caps = str.takeIf { it.isNotEmpty() }.toUpperCase() // 编译错误
println(caps)

  takeIf 及 takeUnless 与作用域函数一起特别有用。一个很好的例子是用 let 链接它们,以便在与给定 谓词匹配的对象上运行代码块。为此,请在对象上调用 takeIf,然后通过安全调用(?.)调用 let。对于与谓 词不匹配的对象,takeIf 返回 null,并且不调用 let

fun displaySubstringPosition(input: String, sub: String) {
input.indexOf(sub).takeIf { it >= 0 }?.let {
println("The substring $sub is found in $input.")
println("Its start position is $it.")
}
} displaySubstringPosition("010000011", "11")
displaySubstringPosition("010000011", "12")

  没有标准库函数时,相同的函数看起来是这样的

fun displaySubstringPosition(input: String, sub: String) {
val index = input.indexOf(sub)
if (index >= 0) {
println("The substring $sub is found in $input.")
println("Its start position is $index.")
}
} displaySubstringPosition("010000011", "11")
displaySubstringPosition("010000011", "12")

  

kotlin更多语言结构——>作用域函数的更多相关文章

  1. (转)PHP的语言结构和函数的区别

    相信大家经常看到对比一些PHP应用中,说用isset() 替换 strlen(),isset比strlen执行速度快等. 例子: if ( isset($user) ) { //do some thi ...

  2. PHP的语言结构和函数的区别

    相信大家经常看到对比一些PHP应用中,说用isset() 替换 strlen(),isset比strlen执行速度快等. 例子: if ( isset($user) ) { //do some thi ...

  3. php入门 数据类型 运算符 语言结构语句 函数 类与面向对象

    php PHP-enabled web pages are treated just like regular HTML pages and you can create and edit them ...

  4. PHP 语言结构(Language constructs)和函数的区别

    相信大家经常看到对比一些PHP应用中,说用isset() 替换 strlen(),isset比strlen执行速度快等. 例子: if ( isset($username[5]) ) { // The ...

  5. PHP:函数和语言结构(转)

    转自:https://www.cnblogs.com/fanqiechaodan/articles/5222366.html 什么是语言结构呢?它和函数有什么不同吗? 1.  什么是语言结构和函数 语 ...

  6. C语言笔记 08_函数指针&回调函数&字符串&结构体&位域

    函数指针 函数指针是指向函数的指针变量. 通常我们说的指针变量是指向一个整型.字符型或数组等变量,而函数指针是指向函数. 函数指针可以像一般函数一样,用于调用函数.传递参数. 函数指针变量的声明: / ...

  7. 【嵌入式开发】C语言 结构体相关 的 函数 指针 数组

    . 作者 : 万境绝尘 转载请注明出处 : http://www.hanshuliang.com/?post=30 . 结构体概述 : 结构体是 多个 变量的集合, 变量的类型可以不同; -- 可进行 ...

  8. 在C语言结构体中添加成员函数

    我们在使用C语言的结构体时,经常都是只定义几个成员变量,而学过面向对象的人应该知道,我们定义类时,不只是定义了成员变量,还定义了成员方法,而类的结构和结构体非常的相似,所以,为什么不想想如何在C语言结 ...

  9. C语言变量、函数的作用域及变量的存储方式

    一.变量的作用域和存储方式 在C语言中每个变量都有两种基本属性:数据类型.数据的存储类别. 数据类型很多人都已熟知,例如:字符型(char).整型(int).浮点型(float)等等.存储类别是指数据 ...

  10. C语言 结构体相关 函数 指针 数组

    . 作者 : 万境绝尘 转载请注明出处 : http://www.hanshuliang.com/?post=30 . 结构体概述 : 结构体是 多个 变量的集合, 变量的类型可以不同; -- 可进行 ...

随机推荐

  1. 【Tomcat】IDEA工程没有EE规范的jar包?

    发现了一个问题,我安装了2种版本的Tomcat 一个是8版本,另一个是10版本 我在已经使用8版本的工程中,更换成使用10版本,当然一开始部署运行正常 但是关闭了工程之后,再次打开就发现,这些EE规范 ...

  2. 【Hibernate】Re07 关系映射处理

    一.单向多对一关系映射处理 演示案例列举了员工与部门的关系,一个部门下具有多个员工,相反的一个员工只隶属于一个部门下面 Maven依赖坐标: <dependency> <groupI ...

  3. 【Redis】04 配置文件分析

    配置文件Redis.conf注释信息: 1.启动项: 启动Redis要求必须加上配置文件redis.conf路径作为第一参数加载 文档样例: ./redis-server /path/to/redis ...

  4. 神州笔记本(HASEE) win11 操作系统自动进入休眠状态,唤醒后自动关机 —— 神州笔记本总出现这种自动关机的问题怎么破解?

    前几日在某东上购入神州笔记本(HASEE),用着本来还好,但是最近只要用到电源模式的问题,这个笔记本就是会无端进入到自动关机的状态. 前文中也讨论过类似的问题: 神州笔记本 win11 节能模式 供电 ...

  5. tmux使用教程:终端神器tmux:多任务管理大师

    文字版教程: 阮一峰 Tmux 使用教程 视频教程: 终端神器tmux:多任务管理大师

  6. 【转载】 NFS服务器端的权限设置学习 Linux运维学习

    原文地址: http://blog.chinaunix.net/uid-31484238-id-5785140.html ======================================= ...

  7. HP笔记本电脑——暗夜精灵2pro继电池鼓包后出现无法充电的问题,最后电量显示:0%可用(电源已接通,未充电)

    问题如题,最近使用暗夜精灵2pro笔记本(自己17年5月1节日购买)使用了四年,使用了第二年的时候出现电池鼓包问题于是自己花了不到200元在某宝上购入电池进行替换同时更新bios,正常使用到今年8月2 ...

  8. 记录一次实验室linux系统的GPU服务器死机排查过程——某显卡满负荷导致内核进程超时导致系统死机

    在自己没有管理多台高负荷的ubuntu显卡服务器之前,我是万万想不到linux服务器居然也是如此容易死机的. 什么每个版本的TensorFlow调用显卡驱动时和内核不兼容,什么系统自动升级导致的显卡驱 ...

  9. C# 委托和闭包

    前言 本文只是为了复习,巩固,和方便查阅,一些知识点的详细知识会通过相关链接和文献标记出来. 委托是什么 大部分的解释是 委托是一个对方法的引用,可以不用自己执行,而是转交给其他对象.就好比每天都有一 ...

  10. GPL前世今生

    从事Linux开发的朋友一定都听过GPL,那么到底什么是GPL呢?他有什么作用呢?本文给大家做详细讲解. 一.GNU/GPL 在讲解GPL之前,我们必须先了解什么是GNU? 1. 什么是GNU GNU ...