前言

这篇文章旨在对Kotlin中的作用域方法(Scope Function)——即run、with、also、apply、let五个方法进行学习和记录(还要两个额外的方法takeIf和takeUnless),以便自己复习和让其他人学习。全文分为两个部分:官方文档翻译和作者思考。

话不多说,入正题!

官方文档翻译

(PS:这里的翻译是纯人工翻译,并且不进行直译,而是在翻译的同时结合作者自己的理解转化为作者认为更贴切的中文描述。如有错误或不同意见欢迎友善指出!)

在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"// thisstr.run {println("The receiver string length: $length")//println("The receiver string length: ${this.length}") // 效果相同}// itstr.let {println("The receiver string's length is ${it.length}")}
}

this

run、with、apply都作为lambda的接收者来使用对象(this),因此在lambda中,可以像在普通类方法中使用该对象。在大多数情况下,你可以省略this来获取对象的成员,使得代码更加短。但相对地,会让人更难区分对象的成员和外部对象(或方法)。所以如果当你的lambda主要对对象的成员进行操作(调用成员方法或给属性赋值)时,推荐使用作为lambda的接收者来使用对象(this)。

val adam = Person("Adam").apply { age = 20                       //与this.age = 20或adam.age = 20相同city = "London"
}
println(adam)

it

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返回对象本身
  • run、with、let返回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()

它们也可以通过返回对象本身的形式作为方法的返回。

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

lambda结果

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结果。

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)      // 编译通过,在let{}中str不为空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

with是一个非拓展方法,需要将对象作为参数传递到with中。但在lambda中,对象作为lambda的接收者(this)。返回值为lambda结果。

我们建议在对象中不提供lambda返回地调用with方法。在代码中,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

对象作为lambda的接收者(this);返回值为lambda结果。

run与with的作用相同,但与let的调用方法相同——作为对象的扩展方法。

当lambda中同时包含对象初始化以及返回值计算的时候,run非常有用。

val service = MultiportService("https://example.kotlinlang.org", 80)val result = service.run {port = 8080query(prepareRequest() + " to port $port")
}// let的写法
val letResult = service.let {it.port = 8080it.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

对象作为lambda接收者;返回值为对象本身。

在不返回值而且主要对对象的成员进行操作的情况下使用apply。通常情况为使用apply对对象进行配置。所以apply可以看做“对对象应用以下赋值”。

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

因为返回对象本身,你可以轻易地使用链式调用进行复杂的处理。

also

对象作为lambda的参数;返回值为对象本身。

also在一些将对象视为参数的操作中有好的表现。also倾向于引用对象本身而不是对象的属性或方法。或者当你不想屏蔽掉外部作用域的this引用时,可以使用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 对象本身

下面是一些根据预期目的选择方法的小建议:

  • 在非空值对象上执行lambda:let
  • 在作用域中引进表达式作为变量:let
  • 对象配置:apply
  • 对象配置并且要计算结果:run
  • 在需要表达式的地方执行语句:非扩展run
  • 额外效果:also
  • 为同一个对象的方法调用进行分组:with

不同作用域方法在使用情景下会有重叠,所以可以根据你的项目或团队的习惯选择相应的方法。

尽管作用域方法可以让你的代码更简洁,但不可过度使用,因为这会导致代码的可读性降低和导致出现错误。需要避免作用域方法的嵌套,并且当你链式调用它们的时候需要多加小心,这样很容易将对象本身和this(或it)的值。

takeIf和takeUnless

除了作用域方法外,Kotlin标准库还提供了takeIf和takeUnless这两个方法。它们可以让你在调用链中检查对象的状态。

当在对象上调用带有条件判断的takeIf方法时,如果条件判断成立takeIf返回该对象,否则返回null。所以takeIf是单个对象的过滤方法。takeUnless与takeIf相反。对象作为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会返回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")

作者理解

  • 在翻译作用域方法的功能时我还是选择了直译,想了很久没有想出来比较贴切的表达,但其实这个表述在我看来并不那么准确,作用域方法更像是围绕一个对象做一连串紧密联系的操作。
  • 我认为作用域方法的好处有(不限于):
    • 减少因为需要简单调用而定义出来的变量名。
    • 提升代码的条理性。这个好处主要对应with的用法,利用with可以将对同一个对象一系列操作进行整合。
    • 为初始化后要马上利用该对象的情景提供了一个很好的写法。
    • 得益于Kotlin的隐式类型判断,可以很顺畅地接收lambda的各种返回值。
  • 个人看来,这五个作用域方法并不需要全部掌握,只需要熟悉其中两到三个即可,毕竟所有的方法都是可以互相转换的。这样做降低了学习成本以及相对一致的代码条理。
  • 毫无疑问作用域方法确实为编写Kotlin代码提供了很多便利,但不能“为用而用”。与官方文档中的“过度使用”不同,我的观点是,只要不是为了使用而使用都不属于过度使用。只要你的代码逻辑确实合适就可以放心使用。
  • takeIf和takeUnless是非常非常实用的方法,不要局限于和作用域方法一起使用。

总结

这篇技术文章是我的技术处女作,从前就一直有记录知识点的习惯,但是从来没有完完整整地写一篇技术文章。希望往后的日子里也会继续有文章产出吧!

中nextint函数_Kotlin中的作用域方法(Scope Function)相关推荐

  1. php的email函数发送失败,php中mail函数发送邮件失败的解决方法_php技巧

    本文实例讲述了php中mail函数发送邮件失败的解决方法.分享给大家供大家参考.具体分析如下: php中mail函数是一个自带的发邮件的函数,但是如果你真的要使用mail函数来发邮件那必须是要给你系统 ...

  2. 几何画板中作函数图像的几种方法

    随着社会的发展,现代教学很多的地方都有了多媒体教学,这就需要一些教学软件的辅助了,几何画板就是其中之一.一些老师在使用几何画板的过程中,常常涉及到函数图象的绘制.因此,很多用户对这方面教程是非常的感兴 ...

  3. python dict(zip)函数_python中zip()函数遍历多个列表方法

    在对列表的元素进行找寻时,会频繁的说到遍历的理念.对于复杂的遍历要求,如多个列表中查找就显然不适合用for循环.本篇所要带来的是zip() 函数的方法,能够对多个迭代器进行遍历.下面我们就python ...

  4. scala 函数中嵌套函数_Scala中的嵌套函数 用法和示例

    scala 函数中嵌套函数 Scala中的嵌套函数 (Nested functions in Scala) A nested function is defined as a function whi ...

  5. Halcon中MIN宏定义与自己定义的C++中MIN函数--重命名冲突问题解决方法

    想要实现一个取得最小值的函数,发现一直出问题,报错 看上面MIN显示为一个宏定义,但是下面才是我们要的函数 看来是重名了,尝试了加括号的方法发现还是不行 后来,考虑到HALCON中的MIN我们不用,解 ...

  6. html中text函数,Excel中text函数的使用方法

    说到Excel,相信大家都再熟悉不过了,但说到Excel中text函数的使用方法,可能很多人都不太熟悉,下面随学习啦小编一起看看吧. Excel中text函数的使用方法 首先解释一下text函数的基本 ...

  7. Dev-C++中关于函数 was not declared in this scope报错的解决方法

    1.先报错在哪一行看一下这行的上下行有没有错有时候这个提示可能是告诉你错误可能是出现在这个附近 2.看传入这个函数的实参是否定义了,有没有写错字符的情况.这个参数是否超过了它的"生存范围&q ...

  8. python中choice函数_Python中choice函数的实现方法

    Python中choice函数的实现方法 发布时间:2020-12-15 09:28:06 来源:亿速云 阅读:82 作者:小新 这篇文章主要介绍Python中choice函数的实现方法,文中介绍的非 ...

  9. java中instr函数_Oracle中instr函数使用方法

    Oracle中instr函数使用方法 更新时间:2012年11月03日 00:53:51   作者: 在Oracle/PLSQL中,instr函数返回要截取的字符串在源字符串中的位置.只检索一次,就是 ...

最新文章

  1. 爬虫之selenium控制浏览器执行js代码
  2. Node.js + Express 4.x + MongoDB 构建登录注册-简易用户管理(四)
  3. 今天有点时间,想写一个小说,说说面向对象的故事,主人是人类!(一)
  4. lua去掉字符串中的UTF-8的BOM三个字节
  5. PWN-PRACTICE-BUUCTF-25
  6. query builder python-elasticsearch返回指定字段
  7. 计算SharePoint两个日期和时间字段之间的时间差值
  8. ZT 为什么pthread_cond_t要和pthread_mutex_t同时使用 || pthread/Linux多线程编程
  9. python中class_【机器学习基础】数学推导+纯Python实现机器学习算法11:朴素贝叶斯...
  10. 【 D3.js 入门系列 --- 7 】 理解 update, enter, exit 的使用
  11. 网页编码utf8 gb2312 gbk的区别
  12. 18年7月最新可用QQ坦白说解密方法
  13. pdf免费在线解密方法(无需密码)
  14. web前端开发技术实验与实践(第三版)储久良编著 项目6 文本与段落标记的应用
  15. 有道云笔记分享_有道云笔记的使用分享
  16. SQL存储过程对象名无效
  17. divgrad怎么求_[怎样理解圆柱坐标系和球坐标系求梯度.散度]球坐标系梯度如何求...
  18. ui设计培训课程是哪些
  19. 25 个超棒的 Python 脚本合集(迷你项目)
  20. Node.js 被分叉出一个项目 — Ayo.js,肿么了

热门文章

  1. 星露谷服务器一直没有空闲位置,星露谷物语小镇地图全npc住址位置 经验告诉你该这样...
  2. 2020成考C语言答案,2020年_优学院_C语言程序设计_章节答案
  3. fedora 33 topbar_31省区市新增确诊33例,天津新增本地确诊1例
  4. 计算机二级安装64位的还是,电脑操作系统安装,该选择32位还是64位?
  5. centos mysql无法启动 sock_【零基础学云计算】MYSQL的主从复制、读写分离
  6. for循环和while循环
  7. 速读《文献管理与信息分析》笔记
  8. MOOS学习笔记1——HelloWorld
  9. sqlserver limit
  10. 邮件营销的三个基本要素讲解