Mastering Kotlin standard functions: run, with, let, also and apply

本文详细解析了Kotlin中的标准函数如run、with、let、also和apply的区别及使用场景,通过三个属性:正常函数与扩展函数、this与it参数、返回this或其他类型,帮助读者理解并掌握这些函数的正确应用。

https://medium.com/@elye.project/mastering-kotlin-standard-functions-run-with-let-also-and-apply-9cd334b0ef84

国内的文章基本上都是泛泛而谈,说不清楚,老外的这个文章讲的算不错的~

另外这里有讨论:https://stackoverflow.com/questions/45977011/example-of-when-should-we-use-run-let-apply-also-and-with-on-kotlin

Mastering Kotlin standard functions: run, with, let, also and apply

Some of the Kotlin’s standard functions are so similar that we are not sure which to use. Here I will introduce a simple way to clearly distinguish their differences and how to pick which to use.

Scoping functions

The functions that I’ll focus on is runwithT.runT.letT.also and T.apply. I call them scoping functions as I view their main functionality as provide an inner scope for the caller function.

The simplest way to illustrate scoping is the run function

fun test() {
    var mood = "I am sad"

    run {
        val mood = "I am happy"
        println(mood) // I am happy
    }
    println(mood)  // I am sad
}

With this, inside the test function, you could have a separate scope where mood is redefined to I am happy before printing, and it is fully enclosed within the run scope.

This scoping function itself seems not very useful. But there’s another nice bit it has than just the scope; it returns something i.e. the last object within the scope.

Hence the below would be neat, where by we can apply the show() to both views as below, without calling it twice.

    run {
        if (firstTimeView) introView else normalView
    }.show()

3 attributes of scoping functions

To make scoping functions more interesting, let me categorize their behavior with 3 attributes. I will use these attributes to distinguish them from each others.

1. Normal vs. extension function

If we look at with and T.run, both functions are actually pretty similar. The below does the same thing.

with(webview.settings) {
    javaScriptEnabled = true
    databaseEnabled = true
}
// similarly
webview.settings.run {
    javaScriptEnabled = true
    databaseEnabled = true
}

However, their different is one is a normal function i.e. with, while the other is an extension function i.e. T.run.

So the question is, what is the advantage of each?

Imagine if webview.settings could be null, they will look as below.

// Yack!
with(webview.settings) {
      this?.javaScriptEnabled = true
      this?.databaseEnabled = true
   }
}
// Nice.
webview.settings?.run {
    javaScriptEnabled = true
    databaseEnabled = true
}

In this case, clearly T.run extension function is better, as we could apply check for nullability before using it.

2. This vs. it argument

If we look at T.run and T.let, both functions are similar except for one thing, the way they accept the argument. The below shows the same logic for both functions.

stringVariable?.run {
      println("The length of this String is $length")
}
// Similarly.
stringVariable?.let {
      println("The length of this String is ${it.length}")
}

If you check the T.run function signature, you’ll notice the T.run is just made as extension function calling block: T.(). Hence all within the scope, the T could be referred as this.In programming, this could be omitted most of the time. Therefore in our example above, we could use $length in the println statement, instead of ${this.length}. I call this as sending in this as argument.

However for T.let function signature, you’ll notice that T.let is sending itself into the function i.e. block: (T). Hence this is like a lambda argument sent it. It could be referred within the scope function as it. So I call this as sending in it as argument.

From the above, it does seems like T.run is more superior over T.let as it is more implicit, but there are some subtle advantages of T.let function as below: -

  • The T.let does provide a clearer distinguish use the given variable function/member vs. the external class function/member
  • In the event that this can’t be omitted e.g. when it is sent as a parameter of a function, it is shorter to write than this and clearer.
  • The T.let allow better naming of the converted used variable i.e. you could convert it to some other name.
stringVariable?.let {
      nonNullString ->
      println("The non null string is $nonNullString")
}

3. Return this vs. other type

Now, let’s look at T.let and T.also, both are identical, if we look into the internal function scope of it.

stringVariable?.let {
      println("The length of this String is ${it.length}")
}
// Exactly the same as below
stringVariable?.also {
      println("The length of this String is ${it.length}")
}

However their subtle different is what they return. The T.let returns a different type of value, while T.also returns the T itself i.e. this.

Both are useful for chaining function, where byT.let let you evolve the operation, and T.also let you perform on the same variable i.e. this.

Simple illustration as below

val original = "abc"
// Evolve the value and send to the next chain
original.let {
    println("The original String is $it") // "abc"
    it.reversed() // evolve it as parameter to send to next let
}.let {
    println("The reverse String is $it") // "cba"
    it.length  // can be evolve to other type
}.let {
    println("The length of the String is $it") // 3
}
// Wrong
// Same value is sent in the chain (printed answer is wrong)
original.also {
    println("The original String is $it") // "abc"
    it.reversed() // even if we evolve it, it is useless
}.also {
    println("The reverse String is ${it}") // "abc"
    it.length  // even if we evolve it, it is useless
}.also {
    println("The length of the String is ${it}") // "abc"
}
// Corrected for also (i.e. manipulate as original string
// Same value is sent in the chain 
original.also {
    println("The original String is $it") // "abc"
}.also {
    println("The reverse String is ${it.reversed()}") // "cba"
}.also {
    println("The length of the String is ${it.length}") // 3
}

The T.also may seems meaningless above, as we could easily combine them into a single block of function. Thinking carefully, it has some good advantages

  1. It can provide a very clear separation process on the same objects i.e. making smaller functional section.
  2. It can be very powerful for self manipulation before being used, making a chaining builder operation.

When both combine the chain, i.e. one evolve itself, one retain itself, it becomes something powerful e.g. below

// Normal approach
fun makeDir(path: String): File  {
    val result = File(path)
    result.mkdirs()
    return result
}
// Improved approach
fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }

Looking at all attributes

By looking at the 3 attributes, we could pretty much know the function behavior. Let me illustrate on the T.apply function, as it has not be mentioned above. The 3 attributes of T.apply is as below…

  1. It is an extension function
  2. It send this as it’s argument.
  3. It returns this (i.e. itself)

Hence using it, one could imagine, it could be be used as

// Normal approach
fun createInstance(args: Bundle) : MyFragment {
    val fragment = MyFragment()
    fragment.arguments = args
    return fragment
}
// Improved approach
fun createInstance(args: Bundle) 
              = MyFragment().apply { arguments = args }

Or we could also making unchained object creation chain-able.

// Normal approach
fun createIntent(intentData: String, intentAction: String): Intent {
    val intent = Intent()
    intent.action = intentAction
    intent.data=Uri.parse(intentData)
    return intent
}
// Improved approach, chaining
fun createIntent(intentData: String, intentAction: String) =
        Intent().apply { action = intentAction }
                .apply { data = Uri.parse(intentData) }

Function selections

Hence clearly, with the 3 attributes, we could now categorize the functions accordingly. And based on that, we could form a decision tree below that could help decide what function we want to use pending on what we need.


Hopefully the decision tree above clarifies the functions clearer, and also simplifies your decision making, enable you to master these functions usage appropriately.

Feel free to provide some good real example of how you use these functions as response to this blog. I would love to hear from you. This may benefits others.


I hope you appreciate this post and it’s helpful for you. Do share with others.

You could check out my other interesting topics here.

【从高压输电线的架空地线中汲取电能】一个25千瓦受控电源从735千伏线路的架空地线中汲取电能的SimPowerSystems模型(Simulink仿真实现)内容概要:本文介绍了一个基于SimPowerSystems的Simulink仿真模型,用于模拟从735千伏高压输电线的架空地线中汲取25千瓦电能的受控电源系统。该模型聚焦于高压输电线路中架空地线的能量回收技术,通过仿真手段实现对电能采集过程的建模与控制策略验证,体现了电力系统中新型能源获取方式的技术可行性与工程应用潜力。文中还提及该资源属于一系列电力系统仿真研究的一部分,涵盖微电网、储能优化、碳流追踪、鲁棒调度等多个前沿方向,配套提供Matlab/Simulink代码及网盘资料链接,便于科研人员复现与拓展研究。; 适合人群:具备电力系统基础知识、熟悉Matlab/Simulink仿真环境,从事电力工程、能源回收或智能电网相关研究的科研人员及研究生;有一定编程与建模仿真经验的高年级本科生或工程技术人员。; 使用场景及目标:①研究高压输电线路中架空地线的能量回收机制与建模方法;②掌握基于Simulink的电力系统仿真技术,特别是受控电源与电网交互的动态特性分析;③为开展能源 harvesting、分布式供能、电力电子变换器控制等相关课题提供参考模型与技术支撑; 阅读建议:建议结合提供的仿真模型文件进行实操演练,重点理解系统结构设计、参数设置与控制逻辑实现;同时可延伸学习文档中提到的其他电力系统优化与仿真案例,以拓宽研究视野和技术积累。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值