美文网首页kotlin随笔-生活工作点滴Kotlin 实战
Kotlin 如何优雅地使用 Scope Functions

Kotlin 如何优雅地使用 Scope Functions

作者: fengzhizi715 | 来源:发表于2019-07-13 17:24 被阅读21次
    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 本身是一种很灵活的语言,用好它来写代码不是一件容易的事情,需要不断地去学习和总结。本文仅仅是抛砖引玉,希望能给大家带来更多的启发性。

    相关文章

      网友评论

        本文标题:Kotlin 如何优雅地使用 Scope Functions

        本文链接:https://www.haomeiwen.com/subject/vcrlkctx.html