Kotlin 如何优雅地使用 Scope Functions

一. Scope Functions

Scope Functions :The Kotlin standard library contains several functions whose sole purpose is to execute a block of code within the context of an object. When you call such a function on an object with a lambda expression provided, it forms a temporary scope. In this scope, you can access the object without its name.

作用域函数:它是 Kotlin 标准库的函数,其唯一目的是在对象的上下文中执行代码块。当您在提供了 lambda 表达式的对象上调用此类函数时,它会形成一个临时范围。在此范围内,您可以在不使用其名称的情况下访问该对象。

Kotlin 的 Scope Functions 包含:let、run、with、apply、also 等。本文着重介绍其中最常用的 let、run、apply,以及如何优雅地使用他们。

1.1 apply 函数的使用

apply 函数是指在函数块内可以通过 this 指代该对象,返回值为该对象自己。在链式调用中,我们可以考虑使用它,从而不用破坏链式。


   
   
  1. /**

  2. * Calls the specified function [block] with `this` value as its receiver and returns `this` value.

  3. */

  4. @kotlin.internal.InlineOnly

  5. public inline fun <T> T.apply(block: T.() -> Unit): T {

  6. contract {

  7. callsInPlace(block, InvocationKind.EXACTLY_ONCE)

  8. }

  9. block()

  10. return this

  11. }

举个例子:


   
   
  1. object Test {

  2. @JvmStatic

  3. fun main(args: Array<String>) {

  4. val result ="Hello".apply {

  5. println(this+" World")

  6. this+" World" // apply 会返回该对象自己,所以 result 的值依然是“Hello”

  7. }

  8. println(result)

  9. }

  10. }

执行结果:


   
   
  1. Hello World

  2. Hello

第一个字符串是在闭包中打印的,第二个字符串是result的结果,它仍然是“Hello”。

1.2 run 函数的使用

run 函数类似于 apply 函数,但是 run 函数返回的是最后一行的值。


   
   
  1. /**

  2. * Calls the specified function [block] and returns its result.

  3. */

  4. @kotlin.internal.InlineOnly

  5. public inline fun <R> run(block: () -> R): R {

  6. contract {

  7. callsInPlace(block, InvocationKind.EXACTLY_ONCE)

  8. }

  9. return block()

  10. }

举个例子:


   
   
  1. object Test {

  2. @JvmStatic

  3. fun main(args: Array<String>) {

  4. val result ="Hello".run {

  5. println(this+" World")

  6. this + " World" // run 返回的是最后一行的值

  7. }

  8. println(result)

  9. }

  10. }

执行结果:


   
   
  1. Hello World

  2. Hello World

第一个字符串是在闭包中打印的,第二个字符串是 result 的结果,它返回的是闭包中最后一行的值,所以也打印了“Hello World”。

1.3 let 函数的使用

let 函数把当前对象作为闭包的 it 参数,返回值是函数里面最后一行,或者指定 return。

它看起来有点类似于 run 函数。let 函数跟 run 函数的区别是:let 函数在函数内可以通过 it 指代该对象。


   
   
  1. /**

  2. * Calls the specified function [block] with `this` value as its argument and returns its result.

  3. */

  4. @kotlin.internal.InlineOnly

  5. public inline fun <T, R> T.let(block: (T) -> R): R {

  6. contract {

  7. callsInPlace(block, InvocationKind.EXACTLY_ONCE)

  8. }

  9. return block(this)

  10. }

通常情况下,let 函数跟?结合使用:


   
   
  1. obj?.let {

  2. ....

  3. }

可以在 obj 不为 null 的情况下执行 let 函数块的代码,从而避免了空指针异常的出现。

二. 如何优雅地使用 Scope Functions ?

Kotlin 的新手经常会这样写代码:


   
   
  1. fun test(){

  2. name?.let { name ->

  3. age?.let { age ->

  4. doSth(name, age)

  5. }

  6. }

  7. }

这样的代码本身没问题。然而,随着 let 函数嵌套过多之后,会导致可读性下降及不够优雅。在本文的最后,会给出优雅地写法。

下面结合工作中遇到的情形,总结出一些方法以便我们更好地使用 Scope Functions。

2.1 借助 Elvis 操作符

Elvis 操作符是三目条件运算符的简略写法,对于 x = foo() ? foo() : bar() 形式的运算符,可以用 Elvis 操作符写为 x = foo() ?: bar() 的形式。

在 Kotlin 中借助 Elvis 操作符配合安全调用符,实现简单清晰的空检查和空操作。


   
   
  1. //根据client_id查询

  2. request.deviceClientId?.run {

  3. //根据clientId查询设备id

  4. orgDeviceSettingsRepository.findByClientId(this)?:run{

  5. throw IllegalArgumentException("wrong clientId")

  6. }

  7. }

上述代码,其实已经使用了 Elvis 操作符,那么可以省略掉 run 函数的使用,直接抛出异常。


   
   
  1. //根据client_id查询

  2. request.deviceClientId?.run {

  3. //根据clientId查询设备id

  4. orgDeviceSettingsRepository.findByClientId(this)?:throw IllegalArgumentException("wrong clientId")

  5. }

2.2 利用高阶函数

多个地方使用 let 函数时,本身可读性不高。


   
   
  1. fun add(request: AppVersionRequestModel): AppVersion?{

  2. val appVersion = AppVersion().Builder().mergeFrom(request)

  3. val lastVersion = appVersionRepository.findFirstByAppTypeOrderByAppVersionNoDesc(request.appType);

  4. lastVersion?.let {

  5. appVersion.appVersionNo = lastVersion.appVersionNo!!.plus(1)

  6. }?:let{

  7. appVersion.appVersionNo = 1

  8. }

  9. return save(appVersion)

  10. }

下面,编写一个高阶函数 checkNull() 替换掉两个 let 函数的使用


   
   
  1. inline fun <T> checkNull(any: Any?, function: () -> T, default: () -> T): T = if (any!=null) function() else default()

于是,上述代码改成这样:


   
   
  1. fun add(request: AppVersionRequestModel): AppVersion?{

  2. val appVersion = AppVersion().Builder().mergeFrom(request)

  3. val lastVersion = appVersionRepository.findFirstByAppTypeOrderByAppVersionNoDesc(request.appType)

  4. checkNull(lastVersion, {

  5. appVersion.appVersionNo = lastVersion!!.appVersionNo.plus(1)

  6. },{

  7. appVersion.appVersionNo = 1

  8. })

  9. return save(appVersion)

  10. }

2.3 利用 Optional

在使用 JPA 时,Repository 的 findById() 方法本身返回的是 Optional 对象。


   
   
  1. fun update(requestModel: AppVersionRequestModel): AppVersion?{

  2. appVersionRepository.findById(requestModel.id!!)?.let {

  3. val appVersion = it.get()

  4. appVersion.appVersion = requestModel.appVersion

  5. appVersion.appType = requestModel.appType

  6. appVersion.appUrl = requestModel.appUrl

  7. appVersion.content = requestModel.content

  8. return save(appVersion)

  9. }

  10. return null;

  11. }

因此,上述代码可以不用 let 函数,直接利用 Optional 的特性。


   
   
  1. fun update(requestModel: AppVersionRequestModel): AppVersion?{

  2. return appVersionRepository.findById(requestModel.id!!)

  3. .map {

  4. it.appVersion = requestModel.appVersion

  5. it.appType = requestModel.appType

  6. it.appUrl = requestModel.appUrl

  7. it.content = requestModel.content

  8. save(it)

  9. }.getNullable()

  10. }

这里的 getNullable() 实际是一个扩展函数。


   
   
  1. fun <T> Optional<T>.getNullable() : T? = orElse(null)

2.4 使用链式调用

多个 run、apply、let 函数的嵌套,会大大降低代码的可读性。不写注释,时间长了一定会忘记这段代码的用途。


   
   
  1. /**

  2. * 推送各种报告事件给商户

  3. */

  4. fun pushEvent(appId:Long?, event:EraserEventResponse):Boolean{

  5. appId?.run {

  6. //根据appId查询app信息

  7. orgAppRepository.findById(appId)

  8. }?.apply {

  9. val app = this.get()

  10. this.isPresent().run {

  11. event.appKey = app.appKey

  12. //查询企业推送接口

  13. orgSettingsRepository.findByOrgId(app.orgId)

  14. }?.apply {

  15. this.eventPushUrl?.let {

  16. //签名之后发送事件

  17. val bodyMap = JSON.toJSON(event) as MutableMap<String, Any>

  18. bodyMap.put("sign",sign(bodyMap,this.accountSecret!!))

  19. return sendEventByHttpPost(it,bodyMap)

  20. }

  21. }

  22. }

  23. return false

  24. }

上述代码正好存在着嵌套依赖的关系,我们可以尝试改成链式调用。修改后,代码的可读性和可维护性都提升了。


   
   
  1. /**

  2. * 推送各种报告事件给商户

  3. */

  4. fun pushEvent(appId:Long?, event:EraserEventResponse):Boolean{

  5. appId?.run {

  6. //根据appId查询app信息

  7. orgAppRepository.findById(appId).getNullable()

  8. }?.run {

  9. event.appKey = this.appKey

  10. //查询企业信息设置

  11. orgSettingsRepository.findByOrgId(this.orgId)

  12. }?.run {

  13. this.eventPushUrl?.let {

  14. //签名之后发送事件

  15. val bodyMap = JSON.toJSON(event) as MutableMap<String, Any>

  16. bodyMap.put("sign",sign(bodyMap,this.accountSecret!!))

  17. return sendEventByHttpPost(it,bodyMap)

  18. }

  19. }

  20. return false

  21. }

2.5 应用

通过了解上述一些方法,最初的 test() 函数只需定义一个高阶函数 notNull() 来重构。


   
   
  1. inline fun <A, B, R> notNull(a: A?, b: B?,block: (A, B) -> R) {

  2. if (a != null && b != null) {

  3. block(a, b)

  4. }

  5. }

  6. fun test() {

  7. notNull(name, age) { name, age ->

  8. doSth(name, age)

  9. }

  10. }

notNull() 函数只能判断两个对象,如果有多个对象需要判断,怎么更好地处理呢?下面是一种方式。


   
   
  1. inline fun <R> notNull(vararg args: Any?, block: () -> R) {

  2. when {

  3. args.filterNotNull().size == args.size -> block()

  4. }

  5. }

  6. fun test() {

  7. notNull(name, age) {

  8. doSth(name, age)

  9. }

  10. }

三. 总结

Kotlin 本身是一种很灵活的语言,用好它来写代码不是一件容易的事情,需要不断地去学习和总结。本文仅仅是抛砖引玉,希望能给大家带来更多的启发性。

关注【Java与Android技术栈】

更多精彩内容请关注扫码

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值