Kotlin 如何优雅地使用 Scope Functions

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

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

相关文章
|
Java
Kotlin里的Extension Functions实现原理分析
## Kotlin里的Extension Functions Kotlin里有所谓的扩展函数(Extension Functions),支持给现有的java类增加函数。 * https://kotlinlang.org/docs/reference/extensions.html 比如给`String`增加一个`hello`函数,可以这样子写: ```java
1488 0
|
2月前
|
JSON 调度 数据库
Android面试之5个Kotlin深度面试题:协程、密封类和高阶函数
本文首发于公众号“AntDream”,欢迎微信搜索“AntDream”或扫描文章底部二维码关注,和我一起每天进步一点点。文章详细解析了Kotlin中的协程、扩展函数、高阶函数、密封类及`inline`和`reified`关键字在Android开发中的应用,帮助读者更好地理解和使用这些特性。
40 1
|
3月前
|
Android开发 开发者 Kotlin
告别AsyncTask:一招教你用Kotlin协程重构Android应用,流畅度飙升的秘密武器
【9月更文挑战第13天】随着Android应用复杂度的增加,有效管理异步任务成为关键。Kotlin协程提供了一种优雅的并发操作处理方式,使异步编程更简单直观。本文通过具体示例介绍如何使用Kotlin协程优化Android应用性能,包括网络数据加载和UI更新。首先需在`build.gradle`中添加coroutines依赖。接着,通过定义挂起函数执行网络请求,并在`ViewModel`中使用`viewModelScope`启动协程,结合`Dispatchers.Main`更新UI,避免内存泄漏。使用协程不仅简化代码,还提升了程序健壮性。
113 1
|
5月前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
**Kotlin中的`by lazy`和`lateinit`都是延迟初始化技术。`by lazy`用于只读属性,线程安全,首次访问时初始化;`lateinit`用于可变属性,需手动初始化,非线程安全。`by lazy`支持线程安全模式选择,而`lateinit`适用于构造函数后初始化。选择依赖于属性特性和使用场景。**
182 5
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
|
4月前
|
调度 Android开发 开发者
【颠覆传统!】Kotlin协程魔法:解锁Android应用极速体验,带你领略多线程优化的无限魅力!
【8月更文挑战第12天】多线程对现代Android应用至关重要,能显著提升性能与体验。本文探讨Kotlin中的高效多线程实践。首先,理解主线程(UI线程)的角色,避免阻塞它。Kotlin协程作为轻量级线程,简化异步编程。示例展示了如何使用`kotlinx.coroutines`库创建协程,执行后台任务而不影响UI。此外,通过协程与Retrofit结合,实现了网络数据的异步加载,并安全地更新UI。协程不仅提高代码可读性,还能确保程序高效运行,不阻塞主线程,是构建高性能Android应用的关键。
66 4
|
5月前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin中常见作用域函数
**Kotlin作用域函数概览**: `let`, `run`, `with`, `apply`, `also`. `let`安全调用并返回结果; `run`在上下文中执行代码并返回结果; `with`执行代码块,返回结果; `apply`配置对象后返回自身; `also`附加操作后返回自身
62 8
|
5月前
|
安全 Java Android开发
探索Android应用开发中的Kotlin语言
【7月更文挑战第19天】在移动应用开发的浩瀚宇宙中,Kotlin这颗新星以其简洁、安全与现代化的特性,正迅速在Android开发者之间获得青睐。从基本的语法结构到高级的编程技巧,本文将引导读者穿梭于Kotlin的世界,揭示其如何优化Android应用的开发流程并提升代码的可读性与维护性。我们将一起探究Kotlin的核心概念,包括它的数据类型、类和接口、可见性修饰符以及高阶函数等特性,并了解这些特性是如何在实际项目中得以应用的。无论你是刚入门的新手还是寻求进阶的开发者,这篇文章都将为你提供有价值的见解和实践指导。
|
5月前
|
SQL 安全 Java
Android经典面试题之Kotlin中object关键字实现的是什么类型的单例模式?原理是什么?怎么实现双重检验锁单例模式?
Kotlin 单例模式概览 在 Kotlin 中,`object` 关键字轻松实现单例,提供线程安全的“饿汉式”单例。例如: 要延迟初始化,可使用 `companion object` 和 `lazy` 委托: 对于参数化的线程安全单例,结合 `@Volatile` 和 `synchronized`
70 6
|
5月前
|
存储 前端开发 测试技术
Android Kotlin中使用 LiveData、ViewModel快速实现MVVM模式
使用Kotlin实现MVVM模式是Android开发的现代实践。该模式分离UI和业务逻辑,借助LiveData、ViewModel和DataBinding增强代码可维护性。步骤包括创建Model层处理数据,ViewModel层作为数据桥梁,以及View层展示UI。添加相关依赖后,Model类存储数据,ViewModel类通过LiveData管理变化,而View层使用DataBinding实时更新UI。这种架构提升代码可测试性和模块化。
208 2
|
6月前
|
安全 Java 编译器
Android面试题之Java 泛型和Kotlin泛型
**Java泛型是JDK5引入的特性,用于编译时类型检查和安全。泛型擦除会在运行时移除类型参数,用Object或边界类型替换。这导致几个限制:不能直接创建泛型实例,不能使用instanceof,泛型数组与协变冲突,以及在静态上下文中的限制。通配符如<?>用于增强灵活性,<? extends T>只读,<? super T>只写。面试题涉及泛型原理和擦除机制。
43 3
Android面试题之Java 泛型和Kotlin泛型