11

Kotlin 如何优雅地使用 Scope Functions - 简书

 4 years ago
source link: https://www.jianshu.com/p/ddde2a1a8e2a?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
0.8362019.07.13 17:24:11字数 1,165阅读 550
webp
beauty.jpg

一. 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 指代该对象,返回值为该对象自己。在链式调用中,我们可以考虑使用它,从而不用破坏链式。

/**
 * Calls the specified function [block] with `this` value as its receiver and returns `this` value.
 */
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}

举个例子:

object Test {

    @JvmStatic
    fun main(args: Array<String>) {
        val result ="Hello".apply {
            println(this+" World")

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

        println(result)
    }
}

执行结果:

Hello World
Hello

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

1.2 run 函数的使用

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

/**
 * Calls the specified function [block] and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

举个例子:

object Test {

    @JvmStatic
    fun main(args: Array<String>) {
        val result ="Hello".run {
            println(this+" World")

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

        println(result)
    }
}

执行结果:

Hello World
Hello World

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

1.3 let 函数的使用

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

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

/**
 * Calls the specified function [block] with `this` value as its argument and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}

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

obj?.let {
   ....
}

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

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

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

fun test(){
    name?.let { name ->
        age?.let { age ->
            doSth(name, age) 
        }
    }
 }

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

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

2.1 借助 Elvis 操作符

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

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

//根据client_id查询
request.deviceClientId?.run {
      //根据clientId查询设备id
       orgDeviceSettingsRepository.findByClientId(this)?:run{
              throw IllegalArgumentException("wrong clientId")
      }
}

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

//根据client_id查询
request.deviceClientId?.run {
     //根据clientId查询设备id
    orgDeviceSettingsRepository.findByClientId(this)?:throw IllegalArgumentException("wrong clientId")
}

2.2 利用高阶函数

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

    fun add(request:  AppVersionRequestModel): AppVersion?{
        val appVersion = AppVersion().Builder().mergeFrom(request)
        val lastVersion = appVersionRepository.findFirstByAppTypeOrderByAppVersionNoDesc(request.appType);
        lastVersion?.let {
            appVersion.appVersionNo = lastVersion.appVersionNo!!.plus(1)
        }?:let{
            appVersion.appVersionNo = 1
        }
        return save(appVersion)
    }

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

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

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

    fun add(request:  AppVersionRequestModel): AppVersion?{

        val appVersion = AppVersion().Builder().mergeFrom(request)
        val lastVersion = appVersionRepository.findFirstByAppTypeOrderByAppVersionNoDesc(request.appType)

        checkNull(lastVersion, {
            appVersion.appVersionNo = lastVersion!!.appVersionNo.plus(1)
        },{
            appVersion.appVersionNo = 1
        })

        return save(appVersion)
    }

2.3 利用 Optional

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

    fun update(requestModel:  AppVersionRequestModel): AppVersion?{
        appVersionRepository.findById(requestModel.id!!)?.let {
            val appVersion = it.get()
            appVersion.appVersion = requestModel.appVersion
            appVersion.appType = requestModel.appType
            appVersion.appUrl = requestModel.appUrl
            appVersion.content = requestModel.content
            return  save(appVersion)

        }

        return null;
    }

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

    fun update(requestModel:  AppVersionRequestModel): AppVersion?{

        return appVersionRepository.findById(requestModel.id!!)
                .map {
                      it.appVersion = requestModel.appVersion
                      it.appType = requestModel.appType
                      it.appUrl = requestModel.appUrl
                      it.content = requestModel.content

                      save(it)
                }.getNullable()
    }

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

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

2.4 使用链式调用

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

    /**
     * 推送各种报告事件给商户
     */
    fun pushEvent(appId:Long?, event:EraserEventResponse):Boolean{
        appId?.run {
            //根据appId查询app信息
            orgAppRepository.findById(appId)
        }?.apply {
            val app = this.get()
            this.isPresent().run {
                event.appKey = app.appKey
                //查询企业推送接口
                orgSettingsRepository.findByOrgId(app.orgId)
            }?.apply {
                this.eventPushUrl?.let {

                    //签名之后发送事件
                    val bodyMap = JSON.toJSON(event) as MutableMap<String, Any>
                    bodyMap.put("sign",sign(bodyMap,this.accountSecret!!))
                    return sendEventByHttpPost(it,bodyMap)
                }
            }

        }
        return  false
    }

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

    /**
     * 推送各种报告事件给商户
     */
    fun pushEvent(appId:Long?, event:EraserEventResponse):Boolean{
       appId?.run {
            //根据appId查询app信息
            orgAppRepository.findById(appId).getNullable()
        }?.run {
            event.appKey = this.appKey
            //查询企业信息设置
            orgSettingsRepository.findByOrgId(this.orgId)
        }?.run {
            this.eventPushUrl?.let {
                //签名之后发送事件
                val bodyMap = JSON.toJSON(event) as MutableMap<String, Any>
                bodyMap.put("sign",sign(bodyMap,this.accountSecret!!))
                return sendEventByHttpPost(it,bodyMap)
            }
        }
        return  false
    }

2.5 应用

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

inline fun <A, B, R> notNull(a: A?, b: B?,block: (A, B) -> R) {
    if (a != null && b != null) {
        block(a, b)
    }
}

fun test() {
      notNull(name, age) { name, age ->
          doSth(name, age)
      }
 }

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

inline fun <R> notNull(vararg args: Any?, block: () -> R) {
    when {
        args.filterNotNull().size == args.size -> block()
    }
}

fun test() {
     notNull(name, age) {
          doSth(name, age)
     }
}

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK