Как Kotlin элегантно использует Scope Functions

Kotlin

beauty.jpg

1. Функции области

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, единственной целью которой является выполнение блока кода в контексте объекта. Когда вы вызываете такую ​​функцию для объекта с лямбда-выражением, она формирует временную область. В пределах этой области вы можете получить доступ к объекту, не используя его имя.

Функции области действия Kotlin включают в себя: позволить, запустить, с, применить, также и т. д. В этой статье основное внимание уделяется наиболее часто используемым командам let, run, apply и их корректному использованию.

1.1 Использование функции применения

Функция 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

Первая строка печатается в замыкании, вторая строка является результатом результата, который по-прежнему «Привет».

1.2 Использование функции запуска

Функция запуска аналогична функции применения, но функция запуска возвращает значение последней строки.

/**
 * 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

Первая строка печатается в замыкании, а вторая строка является результатом результата, который возвращает значение последней строки в замыкании, поэтому «Hello World» также печатается.

1.3 Использование функции let

Функция let принимает текущий объект в качестве параметра it замыкания, а возвращаемое значение является последней строкой в ​​функции или указывает return.

Это чем-то похоже на функцию запуска. Разница между функцией let и функцией запуска заключается в том, что функция let может ссылаться на объект через него внутри функции.

/**
 * 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 {
   ....
}

Код функционального блока let может выполняться, если obj не равен null, что позволяет избежать возникновения исключений нулевого указателя.

2. Как изящно использовать Scope Functions?

Новички в Kotlin часто пишут такой код:

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

Такой код сам по себе в порядке. Однако, поскольку функции let слишком много вложены друг в друга, это становится менее читабельным и менее элегантным. В конце этой статьи будет дано изящное написание.

Следующее в сочетании с ситуацией, возникшей в работе, суммирует некоторые методы, чтобы мы могли лучше использовать функции области видимости.

2.1 Использование оператора Элвиса

Оператор Элвиса является сокращением для тернарного условного оператора.Для оператора вида x = foo() ?foo() : bar() оператор Элвиса может быть записан в форме x = foo() ?:bar().

С помощью оператора Elvis и оператора безопасного вызова в Kotlin реализована простая и понятная проверка null и операция null.

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

В приведенном выше коде фактически использовался оператор Элвиса, поэтому вы можете не использовать функцию запуска и напрямую вызвать исключение.

//根据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 Использование необязательного

При использовании JPA метод репозитория findById() сам возвращает необязательный объект.

    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;
    }

Следовательно, приведенный выше код может напрямую использовать функции Optional без функции let.

    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)
     }
}

3. Резюме

Kotlin сам по себе очень гибкий язык, использовать его для написания кода непросто, и он требует постоянного изучения и обобщения. Эта статья предназначена только для того, чтобы дать вам несколько идей, в надежде вдохновить вас.


Стек технологий Java и Android: еженедельно обновляйте и публикуйте оригинальные технические статьи, добро пожаловать, чтобы отсканировать QR-код общедоступной учетной записи ниже и подписаться, и с нетерпением ждем роста и развития вместе с вами.