Coping 与 Kotlin’s 范围功能: 让, 跑, 也, 应用, 与

Coping 与 Kotlin's 范围功能

Functions in Kotlin are very important and it's much fun() to use them. One special collection of relevant functions can be described as "scope functions" and they are part of the Kotlin 标准库: , , , 应用.
您可能已经听说过它们,并且可能甚至还使用了其中一些。大多数人倾向于在区分所有这些功能时遇到问题,考虑到他们 名称可能有点混乱。这篇文章旨在演示可用范围功能之间的差异,并希望讨论相关的用例。最后,一个示例将展示如何应用作用域函数以及它们如何以更加惯用的方式帮助构造Kotlin代码。

免责声明: The topic of 范围函数 is under consideration in various 堆栈溢出 posts very often, which I will occasionally refer to throughout this 文章.

功能的重要性

在Kotlin中,函数与整数或字符串一样重要。函数可以与类存在于同一级别,可以分配给变量,也可以传递给其他函数或从其他函数返回。 Kotlin发挥作用"first-class citizens"语言的 维基百科 描述如下:

一流的公民是支持以下活动的实体 其他实体通常可以使用的所有操作。这些操作通常包括 作为论点传递, 从函数返回, 改性分配给变量.

As already said, functions are as powerful and significant as any other type, e.g. Int. In addition to that, functions may appear as "高阶函数",其说明如下 维基百科:

在数学和计算机科学中, 高阶函数 (也是功能,功能形式或函子)是一种至少执行以下一项功能的功能:
- 将一个或多个函数作为参数 (即程序参数),
-返回一个函数作为其结果。

粗体打印的项目符号对本文而言是更重要的一点,因为范围函数还充当以其他函数为参数的高阶函数。在深入探讨这一点之前,让我们观察一个高阶函数的简单示例。

高阶函数的作用

A simple 高阶函数 that's commonly known in Kotlin is called repeat和it's defined in the 标准库:

inline fun repeat(times: Int, action: (Int) -> Unit)

As you can see, repeat takes two arguments: An ordinary integer times和also another function of type (Int) -> Unit. According to the previously depicted definition, repeat is a 高阶函数 自从"将一个或多个函数作为参数". In its implementation, the function simply invokes action as often as times indicates. Let's see how repeat can be called from a client's point of view:

repeat(3) { rep ->
    println("current repetition: $rep")
}

在Kotlin中,如果lambda充当函数的最后一个参数,则可以将其从函数调用的括号中删除。

请注意,如果一个函数将另一个函数用作最后一个参数,则可以将lambda表达式参数传递给带括号的参数列表之外。

官方 文件资料 对于所有lambda功能都非常清楚,我强烈建议您对其进行研究。

In the shown snippet, a regular lambda, which only prints the current repetition to the console, is passed to repeat. That's how 高阶函数 calls look like.

Function Literal 与 Receiver

Kotlin提出了另一个非常重要的概念,使功能更加强大。如果你曾经看过 内部域特定语言(DSL) 实际上,您可能想知道它们是如何实现的。最了解的概念称为 function literal 与 接收者 (也 lambda 与 接收者). Since this feature is 也 vital for 范围函数, it will be discussed next.

Function literals 与 接收者 are often used in combination 与 高阶函数. As shown earlier, functions can be made parameters of other functions, which happens 通过 defining parameters 与 the function type syntax (In) -> Out. Now imagine that these function types can even be boosted 通过 adding a 接收者: Receiver.(In) -> Out. Such types are called function literal 与 接收者 and are best understood if visualized as "临时扩展功能"。请看以下示例:

inline fun createString(block: StringBuilder.() -> Unit): String {
    val sb = StringBuilder()
    sb.block()
    return sb.toString()
}

The function createString can be called a 高阶函数 as it takes another function block as its argument. This argument is defined as a function literal 与 接收者 type. Now, 让's think of it as an extension function defined for StringBuilder that will be passed to the createString function. Clients will hand on arbitrary functions 与 the signature () -> Unit, which will be callable on instances of StringBuilder. That's 也 shown in the implementation: An instance of StringBuilder is being created and block gets invoked on it. Eventually, the method transforms StringBuilder to an ordinaryString` and to the caller.

What does that mean for the client of such a method? How can you create and pass function literals 与 接收者 to other functions? Since the 接收者 is defined as StringBuilder, a client will be able to pass lambdas to createString that make use of that 接收者. The 接收者 is 暴露为 this inside the lambda, which means that clients can access visible members (properties, functions etc.) 与out additional qualifiers:


val s = createString { //here we're in the context of a StringBuilder append(4) append("hello") }

The example shows that append, a function defined for the 接收者 StringBuilder, is being invoked 与out any qualifiers (e.g. it). The same is possible in the definition of extension functions, which is why I used it as an analogy earlier. The client defines a temporary extension function which gets invoked on the corresponding 接收者 与in createString afterward. For another description of the concept, please consult the associated 文件资料. I 也 tried to answer a related 堆栈溢出问题 不久以前。

范围功能

范围功能利用了上述概念。它们被定义为高阶函数,即它们以另一个函数作为参数。在某些情况下,这些参数甚至可能与接收方一起显示为函数文字。范围函数采用任意对象, 上下文对象和 bring it to another scope. In that scope, the 上下文对象 is either accessible as it (or custom name) or this, depending on the type of function. In the following, the functions , , , 应用 will be introduced and explained.

[]

说明文件: //kotlinlang.org/api/latest/jvm/stdlib/kotlin/let.html

public inline fun <T, R> T.let(block: (T) -> R): R 

One of the most famous 范围函数 is certainly . It's inspired 通过 functional programming languages like Haskell and is used quite often in the Kotlin language, too。让我们检查其签名:

  • Defined as an extension on T, the 接收者/context object
  • Generic type R defines the function's return value
  • Result R of block will be the result of itself, i.e. it can be an 任意值
  • block argument 与 regular function type (T) -> R
  • Receiver T is passed as argument to block

用例

一种。 Idiomatic replacement for if (object != null) blocks

如您所读 Kotlin成语 section, is supposed to be used to execute blocks if a certain object is not null.

val len = text?.let {
    println("get length of $it")
    it.length
} ?: 0

The nullable text variable is brought into a new scope 通过 if it isn't null. Its value then gets mapped to its length. Otherwise, the null value is mapped to a default length 0 与 the help of the 猫王算子. As you can see, the 上下文对象 text gets 暴露为 it inside , which is the default implicit name for single parameters of a lambda.

b。 如果不是,则映射可为空的值 null

The function is 也 often used for 转型s, especially in combination 与 nullable types again, which is 也 defined as an 成语.

val mapped = value?.let { transform(it) } ?: defaultValue

C。 限制变量/计算的范围

If a certain variable or computation is supposed to be available only in a confined scope and should not pollute the outer scope, again can be helpful:

val transform = "stringConfinedToLetScope".let {
    println("variable can be accessed in 让: $it")
    "${it.length}$it"
}
//cannot access original string from 这里
}

The shown string "stringConfinedToLetScope" is made the 上下文对象 of , which uses the value for some simple 转型 that is returned as the result of . The outer scope only uses the transformed value and does not access the temporarily needed string. There's no variable polluting the outer scope due to confining it to the relevant scope.

[]

说明文件: //kotlinlang.org/api/latest/jvm/stdlib/kotlin/run.html

inline fun <T, R> T.run(block: T.() -> R): R

As an alternative to , the function makes use of a function literal 与 接收者 as used for the block parameter。让我们检查其签名:

  • Defined as an extension on T, the 接收者/context object
  • Generic type R defines the function's return value
  • Result R of block will be the result of itself, i.e. it can be an 任意值
  • block argument defined as function literal 与 接收者 T.() -> R

The function is like except how block is defined.

用例

can basically serve the same use cases as , whereas the 接收者 T is 暴露为 this inside the lambda argument:

一种。 Idiomatic replacement for if (object != null) blocks

val len = text?.run {
    println("get length of $this")
    length //this can be omitted
} ?: 0

b。 转型

It's 也 good to use for 转型s. The following shows an example that is even more readable than 与 自从accesses the 上下文对象's functions 与out qualifiers:

import java.util.Calendar

val date: Int = Calendar.getInstance().run {
    set(Calendar.YEAR, 2030)
    get(Calendar.DAY_OF_YEAR) //return value of 跑
}

[]

说明文件: //kotlinlang.org/api/latest/jvm/stdlib/kotlin/also.html

inline fun  T.also(block: (T) -> Unit): T

The function is the scope function that got lastly added to the Kotlin language, which happened in 版本1.1。让我们检查其签名:

  • Defined as an extension on T, the 接收者/context object
  • 返回接收者对象 T
  • block argument 与 regular function type (T) -> Unit
  • Receiver T is passed as argument to block

looks like , except that it returns the 接收者 T as its result.

用例

一种。 块内未使用接收器

可能需要执行一些与上下文对象相关的任务,但实际上不需要在lambda参数内部使用它。一个示例可以是日志记录。如官方Kotlin中所述 编码约定, using is the recommended way to solve scenarios like the one shown next:

val num = 1234.also {
    log.debug("the function did its job!")
}

在这种情况下,代码几乎像普通句子一样读取:给变量分配一些内容,然后 登录到控制台。

b。 初始化对象

Another very common scenario that can be solved 与 is the initialization of objects. As opposed to the two previously introduced 范围函数, , returns the 接收者 object after the block execution. This fact can be very handy:

val bar: Bar = Bar().also {
    it.foo = "another value"
}

As shown, a Bar instance is created and is utilized in order to directly initialize one of the instance's properties. Since returns the 接收者 object itself, the expression can directly be 分配给变量 of type Bar.

C。 将计算值分配给字段

The fact that returns the 接收者 object after its execution can 也 be useful to assign calculated values to fields, as shown 这里:

fun getThatBaz() = calculateBaz().also { baz = it }

A value is being calculated, which is assigned to a field 与 the help of . Since returns that calculated value, it can even be made the direct inline result of the surrounding function.

[应用]

说明文件: //kotlinlang.org/api/latest/jvm/stdlib/kotlin/apply.html

inline fun  T.apply(block: T.() -> Unit): T

The 应用 function is another scope function that was added because the community asked for it. Its main use case is the initialization of objects, similar to what does. The difference will be shown next. Let's inspect its signature first:

  • Defined as an extension on T, the 接收者/context object
  • 返回接收者对象 T
  • block argument defined as function literal 与 接收者 T.() -> R

The relation between 应用 is the same as between : 常规lambdaFunction literal 与 接收者 参数:

关系 (应用,) == 关系 (,)

用例

一种。 初始化对象

The ultimate use case for 应用 is object initialization. The community actually asked for this function in relatively late stage of the language. You can find the corresponding feature request 这里.

val bar: Bar = Bar().apply {
    foo1 = Color.RED
    foo2 = "Foo"
}

Although was already shown as a tool for solving these scenarios, it's obvious that 应用 has a big advantage: There's no need to use "it" as a qualifier since the 上下文对象, the Bar instance in this case, is 暴露为 this. The difference got answered in this 堆栈溢出 发布。

b。 返回Unit的方法的生成器样式用法

如中所述 Kotlin成语 section, 应用 can be used for wrapping methods that would normally result in Unit responses.

data class FooBar(var a: Int = 0, var b: String? = null) {
    fun first(aArg: Int): FooBar = 应用 { a = aArg }
    fun second(bArg: String): FooBar = 应用 { b = bArg }
}

fun main(args: Array<String>) {
    val bar = FooBar().first(10).second("foobarValue")
    println(bar)
}

In the example, 应用 is used to wrap simple property assignments that would usually simply result in Unit. Since the class wants to expose a builder-style API to the client, this approach is very useful as the setter-like methods return the surrounding object itself.

[]

说明文件: //kotlinlang.org/api/latest/jvm/stdlib/kotlin/with.html

inline fun <T, R> 与(receiver: T, block: T.() -> R): R

The function is the last scope function that will be discussed 这里. It's a very common feature in many even older languages like Visual Basic and Delphi.
它与其他四个功能的不同之处在于 未定义为扩展功能。让我们检查其签名:

  • Defined as an independent function that takes a 接收者/context object T as its first argument
  • Result R of block will be the result of itself, i.e. it can be an 任意值
  • block argument defined as function literal 与 接收者 T.() -> R

This function aligns 与 in regards to its return value R. It's often said to be similar to 应用; the difference got described 这里. Another simple description of can be found 这里 (都是StackOverflow)。

用例

一种。 Working 与 an object in a confined scope

也定义为 成语, is supposed to be used when an object is only needed in a certain confined scope.

val s: String = 与(StringBuilder("init")) {
    append("some").append("thing")
    println("current value: $this")
    toString()
}

The StringBuilder passed to is only acting as an intermediate instance that helps in creating the more relevant String that gets created in . It's obvious that is utilized for wrapping the calls to StringBuilder 与out exposing the instance itself to the outer scope.

b。 使用类的成员扩展

扩展功能通常在程序包级别上定义,以便可以轻松地从其他任何位置导入和访问它们。也可以在类或对象级别上定义它们,然后称为"成员扩展功能"。这些扩展功能可以在该类内部轻松使用,而不能从外部使用。为了使它们可以从封闭类之外的任何地方访问,必须将该类引入"into scope". The function is very useful 这里:

object Foo {
    fun 关dRange<Int>.random() =
        Random().nextInt(endInclusive - start) + start
}

// random() can only be used in context of Foo与(Foo) {
    val rnd = (0..10).random()
    println(rnd)
}

The shown object Foo defines a sweet 成员扩展功能 random(), which can be used only in the scope of that object. With the help of , this can easily be achieved. Note that this strategy is especially recommendable if particular extension functions are to be grouped meaningfully.

比较和概述

After the five different 范围函数 have been discussed, it's necessary to see them all next to each other:

//return 接收者 T
fun  T.also(block: (T) -> Unit): T //T 暴露为 it
fun  T.apply(block: T.() -> Unit): T //T 暴露为 this

//return 任意值 R
fun <T, R> T.let(block: (T) -> R): R //T 暴露为 it
fun <T, R> T.run(block: T.() -> R): R //T 暴露为 this

//return 任意值 R, not an extension function 
fun <T, R> 与(receiver: T, block: T.() -> R): R //T 暴露为 this

The 范围函数 应用 both return the 接收者 object after their execution. In 应用, the block parameter is defined as a function literal 与 接收者 and T gets 暴露为 this, whereas in , it's a regular function type and T gets 暴露为 it.

The 范围函数 on the other hand both return an arbitrary result R, i.e. the result of the block itself. Again, works 与 a function literal 与 接收者, whereas uses the simple function type.

Last but not least, is kind of a misfit amongst the 范围函数 since it's not defined as an extension on T. It defines two parameters, one of which represents the 接收者 object of this scope function. Same as 应用, works 与 function literal 与 接收者.

返回接收者对象返回任意结果
暴露为 it
暴露为 this应用 & 1

1不是扩展。

IDE支持

As of version 1.2.30, the IntelliJ IDEA Kotlin plugin offers intentions that can convert between 和also between 应用 calls. Read more about it 这里.

示例:请求REST API

在本节中,我将展示一个示例,该示例将前面讨论的范围函数应用于一个非常基本的用例:调用HTTP REST端点。目的是提供用于请求有关贡献者信息的功能 喷气脑/科特林 GitHub project. Therefore we define the appropriate GitHub endpoint and a simplified representation of a Contributor that is 带注释 对于杰克逊:

const val ENDPOINT = "//api.github.com/repos/jetbrains/kotlin/contributors"

@JsonIgnoreProperties(ignoreUnknown = true)
data class Contributor(
    val login: String, 
    val contributions: Int
)

下面显示了以初始形式提供所需功能的代码:

object GitHubApiCaller {
    private val client = OkHttpClient()
    private var cachedLeadResults =
        mutableMapOf<String, Contributor>()
    private val mapper = jacksonObjectMapper()

    @Synchronized
    fun getKotlinContributor(name: String): Contributor {
        val cachedLeadResult = cachedLeadResults[name]
        if (cachedLeadResult != null) {
            LOG.debug("return cached: $cachedLeadResult")
            return cachedLeadResult
        }
        val request = Request.Builder().url(ENDPOINT).build()

        val response = client.newCall(request).execute()

        val responseAsString = response.use {
            val responseBytes = it.body()?.source()?.readByteArray()
            if (responseBytes != null) {
                String(responseBytes)
            } else throw IllegalStateException("No response from server!")
        }

        LOG.debug("response from git api: $responseAsString\n")

        val contributors =
             mapper.readValue(responseAsString)

        val match = contributors.first { it.login == name }
        this.cachedLeadResults[name] = match
        LOG.debug("found kotlin contributor: $match")
        return match
    }
}

The depicted snippet shows a singleton object GitHubApiCaller 与 an OkHttpClient (OkHttp), 一种 杰克逊 mapper and a simple Map that's used for caching results. The code of getKotlinContributor can be decomposed into the following sub-tasks:

  • 结果已经缓存后,立即返回并跳过其余的结果
  • 使用创建一个请求对象 ENDPOINT
  • 通过对请求执行请求来获取响应 client
  • 从响应对象中提取JSON数据(省略错误处理)
  • 将JSON反序列化为 Array
  • 筛选要搜索的贡献者
  • 缓存结果并将其返回给客户端

我认为,该代码非常全面,每个人都可以使用它。但是,它可以作为少量重构的良好基础。

审查守则

Let's now try to find some appropriate use cases for 范围函数 in the previously shown function.

重构第一

The first thing that we can improve is the if block in the very beginning:

val cachedLeadResult = cachedLeadResults[name]
if (cachedLeadResult != null) {
    println("return cached: $cachedLeadResult")
    return cachedLeadResult
}

As shown earlier, the function is normally used for resolving these kinds of if blocks. Applied to the concrete example, we get the following:

return cachedLeadResults[name]?.let {
    LOG.debug("return cached: $it")
    it
}

The problem 这里 is that is defined 与 a generic return type R so that the it needs to be written at the end in order to make it the return value of the expression. Another obvious insufficiency is the missing else statement. The first problem can be addressed pretty easily. We just need to use a scope function that returns its 接收者, i.e. the cached result, directly from the block. Additionally, it should still expose the 接收者 as it, which makes the best suitable candidate:

@Synchronized
fun getKotlinContributor(name: String): Contributor {
    return cachedLeadResults[name]?.also {
        LOG.debug("return cached: $it")
    } ?: requestContributor(name)
}

The 猫王算子, shown before, is very often used for handling the else case, i.e. when the 接收者 is null. In order to make the code more readable, a private function requestContributor now handles the cache miss.

That's it, the if block was replaced 与 an easy invocation. A more 成语atic solution.

重构第二

The next portion that is worth reconsidering contains an unnecessary local variable that represents the request object:

val request = Request.Builder().url(ENDPOINT).build()
val response = client.newCall(request).execute()

It's literally only used for getting a response object from the client和could therefore simply be inlined. Alternatively, the shown actions can be thought of as a basic 转型, which we learned to express 与 the function:

val response = 
    Request.Builder().url(ENDPOINT).build().let { client.newCall(it).execute() }

The request has been made the 上下文对象 of 和directly gets executed in a straightforward 转型. It makes the local request variable obsolete 与out affecting readability negatively.

重构3

The following snippet points at another if (obj != null) block, which in this case can actually be solved 与 :

// Before Refactoring
val responseAsString = response.use {
    val responseBytes = it.body()?.source()?.readByteArray()
    if (responseBytes != null) {
        String(responseBytes)
    } else throw IllegalStateException("No response from server!")
}

// With Scope Function
 val responseAsString = response.use {
    it.body()?.source()?.readByteArray()?.let { String(it) }
        ?: throw IllegalStateException("No response from server!")
}

Again, the 猫王算子 handles the null scenario very nicely.

重构号4

Moving on to the next two statements of the code, we observe that the responseAsString variable is being logged and finally used for the 杰克逊 de-serialization. Let's group them together:

// Before Refactoring
LOG.debug("response from git api: $responseAsString\n")
val contributors =
     mapper.readValue(responseAsString)

// With Scope Function
val contributors = responseAsString.let {
    LOG.debug("response from git api: $it\n")
     mapper.readValue(it)
}

重构5

After the first few refactorings, the situation looks as follows: We have a response, a responseAsString和a contributors variable and still need to filter the Contributors for the desired entry. Basically, the whole requesting and response handling is not relevant for the last step of filtering and caching. We can smoothly group these calls and confine them to their own scope. Since these actions happen 与 the help of the OkHttpClient, it makes sense to make the client the 上下文对象 of that scope:

private fun requestContributor(name: String): Contributor {
    val contributors =
        与(client) {
            val response =
                Request.Builder().url(ENDPOINT).build().let { newCall(it).execute() }

            val responseAsString = response.use {
                it.body()?.source()?.readByteArray()?.let { String(it) }
                        ?: throw IllegalStateException("No response from server!")
            }

            responseAsString.let {
                LOG.debug("response from git api: $it\n")
                 mapper.readValue(it)
            }
        }
    //...
}

There isn't any new code 这里, the previous edits have simply be wrapped in a call of 和are therefore not visible to the surrounding scope (function requestContributors) anymore. It made sense to use in this case 自从exposes client as this和the newCall invocation can therefore omit its qualifier. As described earlier, can have an arbitrary result R. In this case, the last statement inside the lambda, the result of the last call, becomes that R.

重构号6

Now a single variable contributors is available in the outer scope and we can 应用 the filtering:

 return contributors.first { it.login == name }.also {
    cachedLeadResults[name] = it
    LOG.debug("found kotlin contributor: $it")
}

The previous version of the above code consisted of four independent statements that are now grouped in a simple call 与 the filtered Contributor as its 接收者. That 接收者 is put in the cache and 也 logged for debugging purposes. Of course, since returns its 接收者 directly, the whole statement can be made the return of the function.

整个功能如下所示:

private fun requestContributor(name: String): Contributor {
    val contributors =
        与(client) {
            val response =
                Request.Builder().url(ENDPOINT).build().let { newCall(it).execute() }

            val responseAsString = response.use {
                it.body()?.source()?.readByteArray()?.let { String(it) }
                        ?: throw IllegalStateException("No response from server!")
            }

            responseAsString.let {
                LOG.debug("response from git api: $it\n")
                 mapper.readValue(it)
            }
        }

    return contributors.first { it.login == name }.also {
        cachedLeadResults[name] = it
        LOG.debug("found kotlin contributor: $it")
    }
}

我认为该代码的结构看起来非常好,而且仍然可读。但是,我不想鼓励读者阅读本文后在每种情况下都应用范围函数。重要的是要知道这组功能是如此强大,以至于它们甚至可以用来链接无数个表达式并使它们成为单个表达式。您不想这样做,因为它会很快弄乱代码。尝试在此处找到平衡,不要将范围函数应用于所有地方。

结论

In this 文章, I discussed the powerful set of 范围函数 of the Kotlin 标准库. Many situations can be solved in a very 成语atic way 与 the help of these functions and it's vital to have a rough idea of the differences between them. Try to memorize that there are 范围函数 that can return 任意值s (, , ) and those that return the 接收者 itself (应用, ). Then there are functions, which expose their 接收者 as it (, ) and others, which expose their 接收者 as this (, 应用, ). The concluding example demonstrated how easily 范围函数 may be used for refactoring appropriate code sections according to the earlier learned concepts. You shouldn't get the impression that every single opportunity should actually be embraced; it's still necessary to reason about the application of 范围函数. Also, you shouldn't try to use all of the shown functions at any price since most of them can be used interchangeably sometimes. Try to find your own favorites 🙂

您可以在此找到显示的代码示例 GitHub回购.

请随时与我联系并继续 推特。另外,看看我的 Kotlin备忘单入门 这里.

如果您想进一步了解Kotlin的美丽功能,我强烈推荐这本书 行动中的科特林和my 其他文章 给你。

7个想法“Coping 与 Kotlin’s 范围功能: 让, 跑, 也, 应用, 与

发表评论

您的电子邮件地址不会被公开。 必需的地方已做标记 *