应对kotlin.’S范围函数:让,运行,也适用,使用

应对kotlin.'s Scope Functions

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 standard library: , 跑步, , 申请.
你可能已经听说过他们,你也可能你甚至用过其中一些。大多数人往往存在区别于所有这些功能的问题,这鉴于他们的事实是不是非常出色的 名字可能有点令人困惑。该帖子打算展示可用范围职能之间的差异,也希望讨论相关用例。最后,一个例子将展示如何应用范围函数以及如何以更惯用的方式帮助构建Kotlin代码。

免责声明:通常经常在各种stackoverflow帖子中考虑范围函数的主题,我偶尔会引用本文。

功能的重要性

在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 高阶函数 since it "将一个或多个函数作为参数占用". 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中,如果它们充当函数的最后一个参数,可以将Lambdas从函数调用的括号中抬起。

请注意,如果函数将另一个函数作为最后一个参数,则可以在括号中的参数列表之外传递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.

功能文字与接收器

Kotlin促进了另一个非常重要的概念,使功能更加强大。如果你见过 内部域特定语言(DSL) 在行动中,您可能想知道它们是如何实现的。要了解最相关的概念被称为 功能文字与接收器 (还 Lambda与接收器)。由于此功能对范围函数至关重要,因此将讨论下一步。

Function literals with receiver are often used in combination with 高阶函数. As shown earlier, functions can be made parameters of other functions, which happens by defining parameters with the function type syntax (In) -> Out. Now imagine that these function types can even be boosted by adding a 接收者: Receiver.(In) -> Out. Such types are called 功能文字与接收器 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 功能文字与接收器 type. Now, let'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 with the signature () -> Unit, which will be callable on instances of StringBuilder. That's also 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 with receiver to other functions? Since the receiver is defined as StringBuilder, a client will be able to pass lambdas to createString that make use of that receiver. The receiver is exposed as this inside the lambda, which means that clients can access visible members (properties, functions etc.) without 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 receiver StringBuilder, is being invoked without 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 receiver within createString afterward. For another description of the concept, please consult the associated 文件。我也试图回答一个相关的问题 stackoverflow问题 a while ago.

范围功能

范围功能利用上述概念。它们被定义为更高阶函数,即它们将另一个函数作为其参数。在某些情况下,这些参数甚至可能与接收器一起出现的功能文字。范围函数采取任意对象, 上下文对象, 和 bring it to another scope. In that scope, the context object 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 scope functions is certainly . It's inspired by functional programming languages like Haskell and is used quite often in the Kotlin language, too。让我们检查它的签名:

  • Defined as an extension on T, the receiver/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 常规功能类型的参数 (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 by 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 context object text gets exposed as it inside , which is the default implicit name for single parameters of a lambda.

如果没有,请映射可用的值 null

The function is also often used for transformations, especially in combination with nullable types again, which is also 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 let: $it")
    "${it.length}$it"
}
//cannot access original string from here
}

The shown string "stringConfinedToLetScope" is made the context object of , which uses the value for some simple transformation 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 功能文字与接收器 as used for the block parameter。让我们检查它的签名:

  • Defined as an extension on T, the receiver/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 参数定义为带接收器的功能文字 T.() -> R

The 跑步 function is like except how block is defined.

用例

跑步 can basically serve the same use cases as , whereas the receiver T is exposed as 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

转型

It's also good to use 跑步 for transformations. The following shows an example that is even more readable than with since it accesses the context object's functions without qualifiers:

import java.util.Calendar

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

[]

文档: //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 receiver/context object
  • 返回接收器对象 T
  • block 常规功能类型的参数 (T) -> Unit
  • Receiver T is passed as argument to block

looks like , except that it returns the receiver 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!")
}

在这种情况下,代码几乎读取了正常句子:将某些东西分配给变量和 log to the console.

初始化对象

Another very common scenario that can be solved with is the initialization of objects. As opposed to the two previously introduced scope functions, 跑步, returns the receiver 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 receiver object itself, the expression can directly be 分配给变量 of type Bar.

C。 将计算值分配给字段

The fact that returns the receiver object after its execution can also be useful to assign calculated values to fields, as shown here:

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

A value is being calculated, which is assigned to a field with 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 receiver/context object
  • 返回接收器对象 T
  • block 参数定义为带接收器的功能文字 T.() -> R

The relation between 申请 is the same as between 跑步: 普通λ vs. 功能文字与接收器 parameter:

关系 (申请,) == 关系 (跑步,)

用例

一个。 初始化对象

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 context object, the Bar instance in this case, is exposed as this. The difference got answered in this 堆栈溢出 post.

返回单位的方法的建筑风格使用

如上所述 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 = apply { a = aArg }
    fun second(bArg: String): FooBar = apply { 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> with(receiver: T, block: T.() -> R): R

The function is the last scope function that will be discussed here. It's a very common feature in many even older languages like Visual Basic and Delphi.
它从其他四个函数变化 未定义为扩展功能。让我们检查它的签名:

  • Defined as an independent function that takes a receiver/context object T as its first argument
  • Result R of block will be the result of itself, i.e. it can be an 任意价值
  • block 参数定义为带接收器的功能文字 T.() -> R

This function aligns with 跑步 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)。

用例

一个。 在一个受限范围内使用对象

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

val s: String = with(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 without exposing the instance itself to the outer scope.

使用类的成员扩展

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

object Foo {
    fun ClosedRange<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.

比较和概述

在讨论了五种不同的范围函数之后,有必要彼此相邻地看到它们:

//return receiver T
fun  T.also(block: (T) -> Unit): T //T exposed as it
fun  T.apply(block: T.() -> Unit): T //T exposed as this

//return arbitrary value R
fun <T, R> T.let(block: (T) -> R): R //T exposed as it
fun <T, R> T.run(block: T.() -> R): R //T exposed as this

//return arbitrary value R, not an extension function 
fun <T, R> with(receiver: T, block: T.() -> R): R //T exposed as this

The scope functions 申请 both return the receiver object after their execution. In 申请, the block parameter is defined as a 功能文字与接收器 and T gets exposed as this, whereas in , it's a regular function type and T gets exposed as it.

The scope functions 跑步 on the other hand both return an arbitrary result R, i.e. the result of the block itself. Again, 跑步 works with a 功能文字与接收器, whereas uses the simple function type.

Last but not least, is kind of a misfit amongst the scope functions since it's not defined as an extension on T. It defines two parameters, one of which represents the receiver object of this scope function. Same as 申请跑步, works with 功能文字与接收器.

返回接收器对象返回任意结果
暴露为 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端点。目标是提供要求有关贡献者信息的功能 Jetbrains / Kotlin. GitHub project. Therefore we define the appropriate GitHub endpoint and a simplified representation of a Contributor that is 注释 for Jackson:

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 Okhttp.Client (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
  • 过滤器搜索的贡献者
  • 缓存结果并将其返回给客户端

在我看来,这段代码非常全面,每个人都能够利用它。尽管如此,它可以作为一点重构作为一个好的基础。

审核代码

我们现在尝试在先前所示的功能中找到一些适当的用例。

重构1号

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 here is that is defined with 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 receiver, i.e. the cached result, directly from the block. Additionally, it should still expose the receiver 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 Elvis operator, shown before, is very often used for handling the else case, i.e. when the receiver 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 with an easy invocation. A more idiomatic solution.

重构2号

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 with the function:

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

The request has been made the context object of 和directly gets executed in a straightforward transformation. It makes the local request variable obsolete without affecting readability negatively.

重构3号

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

// 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 Elvis operator 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 Jackson 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 with the help of the Okhttp.Client, it makes sense to make the client the context object of that scope:

private fun requestContributor(name: String): Contributor {
    val contributors =
        with(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 here, 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 since it 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 apply 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 with the filtered Contributor as its receiver. That receiver is put in the cache and also logged for debugging purposes. Of course, since returns its receiver directly, the whole statement can be made the return of the function.

整个函数如下所示:

private fun requestContributor(name: String): Contributor {
    val contributors =
        with(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 article, I discussed the powerful set of scope functions of the Kotlin standard library. Many situations can be solved in a very idiomatic way with 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 scope functions that can return arbitrary values (, 跑步, ) and those that return the receiver itself (申请, ). Then there are functions, which expose their receiver as it (, ) and others, which expose their receiver as this (跑步, 申请, ). The concluding example demonstrated how easily scope functions 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 scope functions. 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 repo..

随意联系我并跟进 推特。另外,看看我的 kotlin作弊表入门 这里.

如果您想了解更多关于Kotlin的美丽功能,我强烈推荐这本书 kotlin在行动中和my 其他文章 to you.

7 thoughts on “应对kotlin.’S范围函数:让,运行,也适用,使用

发表评论

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