Kotlin 如何优雅地使用 Scope Functions

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容