阅读 832

kotlin 实战之 let、with、run、apply、also、takeIf、takeUnless、repeat 源码总结

工匠若水可能会迟到,但是从来不会缺席,最终还是觉得将自己的云笔记分享出来吧 ~

特别说明,kotlin 系列文章均以 Java 差异为核心进行提炼,与 Java 相同部分不再列出。随着 kotlin 官方版本的迭代,文中有些语法可能会发生变化,请务必留意,语言领悟精髓即可,差异只是语法层面的事情,建议不要过多精力投入语法,要理解背后原理。

背景

这一篇中这些知识点都是 kotlin 相对于 java 特有的新能力机制,所以不存在与 java 的差异对比,这些新能力都是 kotlin 源码Standard.kt中定义的函数或者扩展函数,官方叫做范围工具函数(https://kotlinlang.org/docs/reference/scope-functions.html)。如下直接对这些知识点进行深入分析使用。

let

let 扩展函数实际是一个作用域函数,当我们需要定义变量在一个特定的作用域范围内时,let 函数就是一个不错的选择。此外 let 函数配合?.运算符的另一个作用是可以避免写一些判 null 操作。如下是使用案例:

//使用案例一:定义变量在一个特定的作用域范围内
testObj.let {
   it.func() //此处 it 就是 testObj 实例
   ...
   var localValue = 1 //定义变量在一个特定的作用域范围内
   ...
}

//使用案例二:避免写一些判 null 操作
var obj: TestObjClass? = null
val ret = obj?.let{ //表示 obj 不为 null 的条件下,才会去执行 let 函数体,否则返回 null
   it.func() //obj 不为 null 时 func 返回值返回给 ret
}
复制代码

kotlin 中 let 扩展函数的实现原理源码:

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】

//let 是一个 inline 内联扩展函数,扩展任意类型,此处范型表示
//接收一个函数类型参数(lambada 表达式),返回函数类型参数函数调用的返回值
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
	//契约
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    //可以看到调用了函数参数的函数,传递参数为调用 let 函数的对象实例,返回值为函数参数调用返回值
    return block(this)
}
复制代码

with

with 普通函数实际是一个简写封装,适用于调用同一个类的多个方法或者属性场景,这时可以省去类名重复,直接调用类的方法即可。典型使用场景如下:

with(obj) {
    prop //等价于 obj.prop
    fun() //等价于 obj.fun()
}

//案例
class User(var name: String, var age: Int)

val user = User("gongjiangruoshui", 18)
val ret = with(user) {
        println("name is $name, age is $age") //不需要 user. 的前缀
        1000 //返回值
    }
复制代码

kotlin 中 with 普通函数实现源码如下:

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】

//let 是一个 inline 普通函数
//特别注意!block 参数是一个带接收者的函数类型,T.()->R 里的 this 代表的是自身实例,所以可以省略;而 ()->R 里的 this 代表的是外部类的实例,所以不能省略。
@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    //契约
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    //receiver参数调用其自己的带接收者的函数,所以内部可以省略 this,最后返回带接收者的函数的返回值
    return receiver.block()
}
复制代码

run

kotlin 对 run 提供了一个扩展函数实现和一个普通函数实现;普通函数的实现可以说也是为了解决定义变量在一个特定的作用域范围内场景;而扩展函数实现实际上可以说是 let 和 with 两个函数的结合体,run 扩展函数弥补了 let 函数在函数体内必须使用 it 参数替代对象问题,可以像 with 函数一样省略 it,直接访问实例的公有属性和方法;另一个角度来说,run 扩展函数弥补了 with 函数传入对象判空问题。典型使用场景如下:

//run 扩展函数使用案例
class MultiportService(var url: String, var port: Int) {
    fun prepareRequest(): String = "Default request"
    fun query(request: String): String = "Result for query '$request'"
}

fun main() {
    val service = MultiportService("https://example.kotlinlang.org", 80)

	//service.此处是可空类型的话替换为 service?.也可以达到省略判空的目的
    val result = service.run {
        port = 8080 //直接访问属性
        query(prepareRequest() + " to port $port")
    }

    //使用 let 达到同样效果
    val letResult = service.let {
        it.port = 8080 //let 内部访问对象属性必须指定 it.
        it.query(it.prepareRequest() + " to port ${it.port}")
    }
    println(result)
    println(letResult)
}

//run 普通函数使用案例
fun main() {
    val hexNumberRegex = run {
        //run内定义局部变量使用
        val digits = "0-9"
        val hexDigits = "A-Fa-f"
        val sign = "+-"
        Regex("[$sign]?[$digits$hexDigits]+")
    }

    for (match in hexNumberRegex.findAll("+1234 -FFFF not-a-number")) {
        println(match.value)
    }
}
复制代码

kotlin 中 run 普通函数和扩展函数实现源码如下:

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】

//run 普通函数
@kotlin.internal.InlineOnly
//可以看到和 let 函数完成的事情完全一样,只是它不是扩展函数,可以随意使用
public inline fun <R> run(block: () -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

//run 扩展函数
@kotlin.internal.InlineOnly
//可以看到和 let 函数很像,但是参数是一个带接收者的函数类型,T.()->R 里的 this 代表的是自身实例,所以可以省略;而 ()->R 里的 this 代表的是外部类的实例,所以不能省略。
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    //等价于 let
    return block()
}
复制代码

apply

apply 扩展函数和 run 扩展函数很像,唯一的不同点就是它们各自返回的值不一样,run 扩展函数是以闭包形式返回最后一行代码的值,而 apply 扩展函数的返回的是传入对象的自身。所以 apply 扩展函数一般用于对象实例属性初始化场景(类似 builder 模式),此外还可以用多层级判空场景。典型使用场景如下:

//对象实例化场景
data class Person(var name: String, var age: Int = 0, var city: String = "") {
    fun test() { println "666" }
}

fun main() {
	//构造对象同时属性赋值
    val adam = Person("Adam").apply {
        age = 32
        city = "London"        
    }
    println(adam)
}

//多层级判空场景
class Dog {
    fun testDog() = println "dog"
}

class Cat {
	var dog: Dog? = null
    fun testCat() = println "cat"
}

val cat: Cat? = null
cat?.apply {
    //cat不为空时调用testCat
	testCat()
}?.dog?.apply{
    //cat对象里面的dog属性实例不为空时调用testDog
	testDog()
}
复制代码

kotlin 中 apply 扩展函数实现源码如下:

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】

//apply 是一个 inline 扩展函数
//特别注意!block 参数是一个带接收者的函数类型,T.()->R 里的 this 代表的是自身实例,所以可以省略;而 ()->R 里的 this 代表的是外部类的实例,所以不能省略。
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    //返回的调用对象自己
    return this
}
复制代码

also

also 扩展函数和 let 很像,唯一的区别就是返回值不一样,let 是以闭包的形式返回函数体内最后一行的值,而 also 扩展函数返回的是传入对象的自身。also 扩展函数适用于 let 函数的任何场景,此外还能用作链式编程中的介入操作(不破坏链式解构)。如下是使用案例:

fun main() {
    val numbers = mutableListOf("one", "two", "three")
    //插入元素前先打印下元素内容
    numbers
        .also { //链式插入
            println("The list elements before adding new one: $it")
        }
        .add("four")
}
复制代码

kotlin 中 also 扩展函数的实现原理源码:

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】

//和 let 如出一辙,只是返回值不同
@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.also(block: (T) -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    //返回调用对象自己
    return this
}
复制代码

takeIf 和 takeUnless

takeIf 和 takeUnless 扩展函数算是一对过滤器操作,这两扩展函数允许我们在链式调用中加入对象的状态检查,根据代码块里面的返回值决定返回空还是调用对象自身,配合?.作用域函数来做简单的 if 逻辑判断效果。如下是使用案例:

//案例一
val number = Random.nextInt(100)
val evenOrNull = number.takeIf { it % 2 == 0 } //偶数则返回偶数自己,否则返回 null
val oddOrNull = number.takeUnless { it % 2 == 0 } //偶数则返回 null,否则返回自己

//案例二
val str = "Hello"
val caps = str.takeIf { it.isNotEmpty() }?.toUpperCase()
//val caps = str.takeIf { it.isNotEmpty() }.toUpperCase() //compilation error
println(caps)

//案例三
fun displaySubstringPosition(input: String, sub: String) {
    //等价于 val index = input.indexOf(sub); if (index >=0) { println ... }
    input.indexOf(sub).takeIf { it >= 0 }?.let {
        println("The substring $sub is found in $input.")
        println("Its start position is $it.")
    }
}
displaySubstringPosition("010000011", "11")
displaySubstringPosition("010000011", "12")
复制代码

kotlin 中 takeIf、takeUnless 扩展函数的实现原理源码:

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】

//两个函数完全一样,只是判断互斥而已
@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? {
    contract {
        callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
    }
    return if (predicate(this)) this else null
}

@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T? {
    contract {
        callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
    }
    return if (!predicate(this)) this else null
}
复制代码

repeat

repeat 内联函数其实就是一个工具函数,何其名字一样,重复执行,本质就是一个 for 循环,没啥多说的,使用也很简单。如下是使用案例:

//重复打印三次,it为次数索引
repeat(3) {
    println("exec $it")
}
复制代码

kotlin 中 repeat 函数的实现原理源码:

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】

@kotlin.internal.InlineOnly
public inline fun repeat(times: Int, action: (Int) -> Unit) {
    contract { callsInPlace(action) }
	//就一循环,传递的参数是当前索引
    for (index in 0 until times) {
        action(index)
    }
}
复制代码

到此 kotlin 源码Standard.kt中定义的函数或者扩展函数我们都分析完毕了,你应该也能知道怎么选择使用了。

搭车分析(Timing.kt源码工具方法)

上面分析的都是Standard.kt相关函数,这里搭车说下 kotlin 提供的另一个很好用和实用的工具方法,那就是Timing.kt源码中的代码执行时间测量函数。下面直接看源码分析:

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】

//测量block的MillisTime执行时间并返回
public inline fun measureTimeMillis(block: () -> Unit): Long {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    //很简单,就是做差封装
    val start = System.currentTimeMillis()
    block()
    return System.currentTimeMillis() - start
}

//测量block的NanoTime执行时间并返回
public inline fun measureNanoTime(block: () -> Unit): Long {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    //很简单,就是做差封装
    val start = System.nanoTime()
    block()
    return System.nanoTime() - start
}
复制代码

其使用场景案例如下:

fun main() {
	//测量内部代码执行时长
    val cost = measureTimeMillis {
        testRun()
    }
    println(cost) //2000
}

fun testRun() {
    Thread.sleep(2000)
}
复制代码

到此,真的 over!

【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题 未经允许严禁转载 blog.csdn.net/yanbober